Current section
Files
Jump to
Current section
Files
src/gradualizer_fmt.erl
%% @private
-module(gradualizer_fmt).
-export([format_location/2,
format_type_error/2,
print_errors/2,
handle_type_error/2,
form_info/1]).
-include("typelib.hrl").
-define(FMT_LOCATION_DEFAULT, verbose).
-define(color_type, "\e[35m"). % 35=magenta
-define(color_end, "\e[0m").
-spec format_location(any(), brief | verbose, any()) -> io_lib:chars().
format_location(Expr, FmtType, Opts) ->
case proplists:get_value(fmt_location, Opts, ?FMT_LOCATION_DEFAULT) of
FmtType ->
format_location(Expr, FmtType);
_ ->
""
end.
-spec format_location(any(), brief | verbose) -> io_lib:chars().
format_location(Anno, Config) when is_list(Anno); is_integer(Anno) ->
%% compatibility with old error types which contain bare
%% annotations instead of the whole expression
format_location({expr, Anno}, Config);
format_location(Anno = {L, C}, Config) when is_integer(L), is_integer(C) ->
%% compatibility with old error types which contain bare
%% annotations instead of the whole expression
format_location({expr, Anno}, Config);
format_location(Expr, brief) ->
case erl_anno:location(element(2, Expr)) of
{Line, Col} ->
io_lib:format("~p:~p: ", [Line, Col]);
Line when is_integer(Line) ->
io_lib:format("~p: ", [Line])
end;
format_location(Expr, verbose) ->
case erl_anno:location(element(2, Expr)) of
{Line, Col} ->
io_lib:format(" on line ~p at column ~p", [Line, Col]);
Line when is_integer(Line) ->
io_lib:format(" on line ~p", [Line])
end.
-spec format_type_error(any(), any()) -> io_lib:chars().
format_type_error({type_error, Expression, ActualType, ExpectedType}, Opts)
when is_tuple(Expression) ->
format_expr_type_error(Expression, ActualType, ExpectedType, Opts);
format_type_error({nonexhaustive, Anno, Example}, Opts) ->
FormattedExample =
case Example of
[X | Xs] ->
lists:reverse(
lists:foldl(fun(A, Acc) ->
[erl_pp:expr(A), "\n\t" | Acc]
end, [erl_pp:expr(X)], Xs)
);
X -> erl_pp:expr(X)
end,
io_lib:format(
"~sNonexhaustive patterns~s~s",
[format_location(Anno, brief, Opts),
format_location(Anno, verbose, Opts),
case proplists:get_value(fmt_location, Opts, ?FMT_LOCATION_DEFAULT) of
brief ->
io_lib:format(": ~s\n", FormattedExample);
verbose ->
io_lib:format("\nExample values which are not covered:~n\t~s~n", [FormattedExample])
end]);
format_type_error({call_undef, Anno, Module, Func, Arity}, Opts) ->
io_lib:format(
"~sCall to undefined function ~p:~p/~p~s~n",
[format_location(Anno, brief, Opts),
Module,
Func,
Arity,
format_location(Anno, verbose, Opts)]);
format_type_error({undef, record, Anno, {Module, RecName}}, Opts) ->
io_lib:format("~sUndefined record ~p:~p~s~n",
[format_location(Anno, brief, Opts),
Module,
RecName,
format_location(Anno, verbose, Opts)]);
format_type_error({undef, record_field, FieldName}, Opts) ->
io_lib:format(
"~sUndefined record field ~s~s~n",
[format_location(FieldName, brief, Opts),
pp_expr(FieldName, Opts),
format_location(FieldName, verbose, Opts)]);
format_type_error({undef, user_type, Anno, {Name, Arity}}, Opts) ->
io_lib:format(
"~sUndefined type ~p/~p~s~n",
[format_location(Anno, brief, Opts),
Name,
Arity,
format_location(Anno, verbose, Opts)]);
format_type_error({undef, Type, Anno, {Module, Name, Arity}}, Opts)
when Type =:= user_type; Type =:= remote_type ->
TypeS = case Type of user_type -> "type"; remote_type -> "remote type" end,
io_lib:format(
"~sUndefined ~s ~p:~p/~p~s~n",
[format_location(Anno, brief, Opts),
TypeS,
Module,
Name,
Arity,
format_location(Anno, verbose, Opts)]);
format_type_error({not_exported, remote_type, Anno, {Module, Name, Arity}}, Opts) ->
io_lib:format(
"~sThe type ~s:~s/~p~s is not exported~n",
[format_location(Anno, brief, Opts),
Module,
Name,
Arity,
format_location(Anno, verbose, Opts)]);
format_type_error({illegal_map_type, Type}, Opts) ->
io_lib:format(
"~sIllegal map type ~s~s~n",
[format_location(Type, brief, Opts),
pp_type(Type, Opts),
format_location(Type, verbose, Opts)]);
format_type_error({type_error, list, _Anno, Ty1, Ty}, Opts) ->
io_lib:format(
"~sThe type ~s cannot be an element of a list of type ~s~n",
[format_location(_Anno, brief, Opts),
pp_type(Ty1, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, list, Anno, Ty}, Opts) ->
io_lib:format(
"~sThe expression of type ~s~s is not a list type~n",
[format_location(Anno, brief, Opts),
pp_type(Ty, Opts),
format_location(Anno, verbose, Opts)]);
format_type_error({type_error, cons_pat, Anno, Cons, Ty}, Opts) ->
io_lib:format(
"~sThe pattern ~s~s does not have type:~n~s~n",
[format_location(Anno, brief, Opts),
pp_expr(Cons, Opts),
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({argument_length_mismatch, Anno, LenTy, LenArgs}, Opts) ->
io_lib:format(
"~sThe clause~s is expected to have ~p argument(s) "
"but it has ~p~n ",
[format_location(Anno, brief, Opts),
format_location(Anno, verbose, Opts),
LenTy,
LenArgs]);
format_type_error({type_error, unreachable_clauses, Clauses}, Opts) ->
Anno = element(2, hd(Clauses)),
io_lib:format(
"~sThe clause~s cannot be reached~n",
[format_location(Anno, brief, Opts),
format_location(Anno, verbose, Opts)]);
format_type_error({type_error, call_arity, Anno, Fun, TyArity, CallArity}, Opts) ->
io_lib:format(
"~sThe function ~s~s expects ~p argument~s, but is given ~p~n",
[format_location(Anno, brief, Opts),
pp_expr(Fun, Opts),
format_location(Anno, verbose, Opts),
TyArity,
["s" || TyArity /= 1],
CallArity]);
format_type_error({type_error, call_intersect, Anno, FunTy, Name}, Opts) ->
io_lib:format(
"~sThe type of the function ~s, called~s doesn't match "
"the surrounding calling context.~n"
"It has the following type~n~s~n",
[format_location(Anno, brief, Opts),
pp_expr(Name, Opts),
format_location(Anno, verbose, Opts),
pp_intersection_type(FunTy, Opts)]);
format_type_error({type_error, expected_fun_type, Anno, Func, FunTy}, Opts) ->
Name = pp_expr(Func, Opts),
io_lib:format(
"~sExpected function ~s~s to have a function type,~n"
"but it has the following type:~n~s~n",
[format_location(Anno, brief, Opts),
Name,
format_location(Anno, verbose, Opts),
pp_type(FunTy, Opts)]);
format_type_error({type_error, no_type_match_intersection, Anno, Func, FunTy}, Opts) ->
Name = pp_expr(Func, Opts),
io_lib:format(
"~sNone of the types of the function ~s~s matches the "
"call site. Here's the types of the function:~n~s~n",
[format_location(Anno, brief, Opts),
Name,
format_location(Anno, verbose, Opts),
pp_intersection_type(FunTy, Opts)]);
format_type_error({type_error, relop, RelOp, Anno, Ty1, Ty2}, Opts) ->
io_lib:format(
"~sThe operator ~p~s requires arguments of "
"compatible types.~nHowever, it has arguments "
"of type ~s and ~s~n",
[format_location(Anno, brief, Opts),
RelOp,
format_location(Anno, verbose, Opts),
pp_type(Ty1, Opts),
pp_type(Ty2, Opts)]);
format_type_error({type_error, op_type_too_precise, '/' = Op, Anno, Ty}, Opts) when ?is_int_type(Ty) ->
io_lib:format(
"~sThe operator ~p~s is expected to have type "
"~s which is not a supertype of float()~n",
[format_location(Anno, brief, Opts),
Op,
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, op_type_too_precise, Op, Anno, Ty}, Opts) ->
io_lib:format(
"~sThe operator ~p~s is expected to have type "
"~s which is too precise to be statically checked~n",
[format_location(Anno, brief, Opts),
Op,
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, arith_error, ArithOp, Anno, Ty1, Ty2}, Opts) ->
io_lib:format(
"~sThe operator ~p~s is requires numeric arguments, but "
"has arguments of type ~s and ~s~n",
[format_location(Anno, brief, Opts),
ArithOp,
format_location(Anno, verbose, Opts),
pp_type(Ty1, Opts),
pp_type(Ty2, Opts)]);
format_type_error({type_error, int_error, ArithOp, Anno, Ty1, Ty2}, Opts) ->
io_lib:format(
"~sThe operator ~p~s is requires integer arguments, but "
" has arguments of type ~s and ~s~n",
[format_location(Anno, brief, Opts),
ArithOp,
format_location(Anno, verbose, Opts),
pp_type(Ty1, Opts),
pp_type(Ty2, Opts)]);
format_type_error({type_error, arith_error, ArithOp, Anno, Ty}, Opts) ->
io_lib:format(
"~sThe operator ~p~s is expected to have type "
"~s which has no numeric subtypes~n",
[format_location(Anno, brief, Opts),
ArithOp,
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, int_error, IntOp, Anno, Ty}, Opts) ->
io_lib:format(
"~sThe operator ~p~s is expected to have type "
"~s which has no integer subtypes~n",
[format_location(Anno, brief, Opts),
IntOp,
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, non_number_argument_to_plus, Anno, Ty}, Opts) ->
io_lib:format(
"~sThe plus expression~s has a non-numeric argument "
"of type:~n~s~n",
[format_location(Anno, brief, Opts),
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, non_number_argument_to_minus, Anno, Ty}, Opts) ->
io_lib:format(
"~sThe negated expression~s has a non-numeric argument "
"of type:~n~s~n",
[format_location(Anno, brief, Opts),
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, unary_error, Op, Anno, TargetTy, Ty}, Opts) ->
io_lib:format(
"~sThe application of unary '~s'~s is expected to have type "
"~s, which has no shared subtype with ~s~n",
[format_location(Anno, brief, Opts),
Op,
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts),
pp_type(TargetTy, Opts)]);
format_type_error({type_error, rel_error, LogicOp, Anno, Ty1, Ty2}, Opts) ->
io_lib:format(
"~sThe operator ~p~s is given two arguments with "
"non-compatible types:~n~s~n~s~n",
[format_location(Anno, brief, Opts),
LogicOp,
format_location(Anno, verbose, Opts),
pp_type(Ty1, Opts),
pp_type(Ty2, Opts)]);
format_type_error({type_error, operator_pattern, Pat, Ty}, Opts) ->
io_lib:format(
"~sThe operator pattern ~s~s is expected to have type "
"~s~n",
[format_location(Pat, brief, Opts),
pp_expr(Pat, Opts),
format_location(Pat, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, pattern, Anno, Pat, Ty}, Opts) ->
io_lib:format(
"~sThe pattern ~s~s doesn't have the type ~s~n",
[format_location(Anno, brief, Opts),
pp_expr(Pat, Opts),
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, check_clauses}, _Opts) ->
%% TODO: Improve quality of type error
io_lib:format("Type error in clauses", []);
format_type_error({type_error, record_pattern, Anno, Record, Ty}, Opts) ->
io_lib:format(
"~sThe record pattern for record #~p~s is expected to have"
" type ~s~n",
[format_location(Anno, brief, Opts),
Record,
format_location(Anno, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({type_error, badkey, KeyExpr, MapType}, Opts) ->
%% Compare to the runtime error raised by maps:get(Key, Map) error:{badkey, Key}.
io_lib:format(
"~sThe expression ~s~s is not a valid key in the map type ~s~n",
[format_location(KeyExpr, brief, Opts),
pp_expr(KeyExpr, Opts),
format_location(KeyExpr, verbose, Opts),
pp_type(MapType, Opts)]);
format_type_error({type_error, receive_after, Anno, TyClauses, TyBlock}, Opts) ->
io_lib:format(
"~sThe types in the clauses and the after block are incompatible~n"
"in the receive statement~s.~n"
"The type of the clauses is : ~s~n"
"The type of the after block is : ~s~n",
[format_location(Anno, brief, Opts),
format_location(Anno, verbose, Opts),
pp_type(TyClauses, Opts),
pp_type(TyBlock, Opts)]);
format_type_error({type_error, cyclic_type_vars, _Anno, Ty, Xs}, Opts) ->
io_lib:format(
"~sThe type spec ~s has a cyclic dependency in variable~s ~s~n",
[format_location(_Anno, brief, Opts),
pp_type(Ty, Opts),
[ "s" || length(Xs) > 1 ],
string:join(lists:map(fun atom_to_list/1, lists:sort(Xs)), ", ")]);
format_type_error({type_error, mismatch, Ty, Expr}, Opts) ->
io_lib:format(
"~sThe expression ~s~s does not have type ~s~n",
[format_location(Expr, brief, Opts),
pp_expr(Expr, Opts),
format_location(Expr, verbose, Opts),
pp_type(Ty, Opts)]);
format_type_error({bad_type_annotation, TypeLit}, Opts) ->
io_lib:format(
"~sThe type annotation ~p~s is not a valid type~n",
[format_location(TypeLit, brief, Opts),
pp_expr(TypeLit, Opts),
format_location(TypeLit, verbose, Opts)]);
format_type_error({form_check_timeout, Form}, Opts) ->
io_lib:format(
"~sTimeout checking ~s~n",
[format_location(Form, brief, Opts),
case proplists:get_value(fmt_location, Opts, ?FMT_LOCATION_DEFAULT) of
brief ->
"form - please report on GitHub";
verbose ->
io_lib:format("~s~s~n"
"This is most likely a bug in Gradualizer.~n"
"Please report it at https://github.com/josefs/Gradualizer/issues",
[form_info(Form),
format_location(Form, verbose, Opts)])
end]);
format_type_error({unsupported_expression, Anno, Expr}, Opts) ->
io_lib:format(
"~sThe ~s expression~s is not supported yet~n",
[format_location(Anno, brief, Opts),
atom_to_list(element(1, Expr)),
format_location(Anno, verbose, Opts)]);
format_type_error({Location, Module, ErrorDescription}, Opts)
when is_integer(Location) orelse is_tuple(Location),
is_atom(Module) ->
%% OTP compiler style error descriptor
io_lib:format(
"~s~s~s~n",
[format_location(Location, brief, Opts),
Module:format_error(ErrorDescription),
format_location(Location, verbose, Opts)]);
format_type_error({none, Module, ErrorDescription}, _Opts)
when is_atom(Module) ->
%% OTP compiler style error descriptor, without location
io_lib:format("~s~n", [Module:format_error(ErrorDescription)]);
format_type_error(type_error, _) ->
io_lib:format("TYPE ERROR~n", []).
form_info({function, _, Name, Arity, _}) ->
io_lib:format("function ~s/~p", [Name, Arity]).
-spec format_expr_type_error(gradualizer_type:abstract_expr(),
typelib:extended_type(),
typelib:extended_type(),
proplists:proplist()) -> io_lib:chars().
format_expr_type_error(Expression, ActualType, ExpectedType, Opts) ->
FancyExpr = try_highlight_in_context(Expression, Opts),
InlineExpr = case FancyExpr of
"" -> " " ++ pp_expr(Expression, Opts);
_ -> ""
end,
io_lib:format(
"~sThe ~s~ts~s is expected "
"to have type ~ts but it has type ~ts~n~ts",
[format_location(Expression, brief, Opts),
describe_expr(Expression),
InlineExpr,
format_location(Expression, verbose, Opts),
pp_type(ExpectedType, Opts),
pp_type(ActualType, Opts),
FancyExpr]).
%% Returns the expression highlighted in its context, if possible. Otherwise,
%% the empty string is returned.
try_highlight_in_context(Expression, Opts) ->
case use_highlight_in_context(Opts) of
true ->
try
highlight_in_context(Expression, Opts)
catch error:_ ->
%% Log a warning here using logger, to allow debugging the
%% highlighter?
""
end;
false ->
""
end.
use_highlight_in_context(Opts) ->
case {proplists:get_value(fancy, Opts, true),
proplists:get_value(forms, Opts),
proplists:get_value(fmt_expr_fun, Opts)} of
{true, Forms, undefined} when is_list(Forms) ->
%% No formatting function is supplied, so we can use Erlang's
%% parser, lexer and pretty-printer
true;
_No ->
%% If we have pretty-printer, lexer and parser as options,
%% we could print highlighted in context for e.g. Elixir too.
false
end.
highlight_in_context(AstNode, Opts) ->
Color = use_color(Opts),
[$\n, % blank line before
case proplists:get_value(source_file, Opts, undefined) of
undefined ->
Forms = proplists:get_value(forms, Opts), % must be present here
gradualizer_highlight:prettyprint_and_highlight(AstNode, Forms, Color);
SourceFile ->
{ok, SourceBin} = file:read_file(SourceFile),
Source = binary_to_list(SourceBin),
gradualizer_highlight:highlight_in_source(AstNode, Source, Color)
end,
$\n]. % blank line after
%% Determines if colors (escape sequences) should be used in the output. If the
%% option for colors is 'auto' (the default), io:columns/0 is used to detect if
%% standard output is a TTY or not.
-spec use_color(gradualizer:options()) -> boolean().
use_color(Opts) ->
case proplists:get_value(color, Opts, auto) of
auto -> case io:columns() of {ok, _} -> true; _ -> false end;
always -> true; % auto/always/never
never -> false; % like for ls, grep, etc.
Bool when is_boolean(Bool) -> Bool % bool (undocumented)
end.
-spec describe_expr(gradualizer_type:abstract_expr()) -> io_lib:chars().
describe_expr({atom, _, _}) -> "atom";
describe_expr({bc, _, _, _}) -> "binary comprehension";
describe_expr({bin, _, _}) -> "bit expression";
describe_expr({block,_,_}) -> "block";
describe_expr({char, _, _}) -> "character";
describe_expr({call, _, _, _}) -> "function call";
describe_expr({'catch', _, _}) -> "catch expression";
describe_expr({'case', _, _, _}) -> "case expression";
describe_expr({cons, _, _, _}) -> "list";
describe_expr({float, _, _}) -> "float";
describe_expr({'fun', _, _}) -> "fun expression";
describe_expr({integer, _, _}) -> "integer";
describe_expr({'if', _, _}) -> "if expression";
describe_expr({lc, _, _, _}) -> "list comprehension";
describe_expr({map, _, _}) -> "map";
describe_expr({map, _, _, _}) -> "map update";
describe_expr({match, _, _, _}) -> "match";
describe_expr({named_fun, _, _, _}) -> "named fun expression";
describe_expr({nil, _}) -> "empty list";
describe_expr({op, _, 'not', _}) -> "negation";
describe_expr({op, _, '-', _}) -> "negation";
describe_expr({op, _, Op, _, _}) -> io_lib:format("~w expression", [Op]);
describe_expr({record, _, _, _}) -> "record";
describe_expr({'receive', _, _, _, _}) -> "receive expression";
describe_expr({record, _, _, _, _}) -> "record update";
describe_expr({record_field, _, _, _, _}) -> "record field";
describe_expr({record_index, _, _, _}) -> "record index";
describe_expr({string, _, _}) -> "string";
describe_expr({tuple, _, _}) -> "tuple";
describe_expr({'try', _, _, _, _, _}) -> "try expression";
describe_expr({var, _, _}) -> "variable";
describe_expr(_) -> "expression".
pp_expr(Expr, Opts) ->
case proplists:get_value(fmt_expr_fun, Opts) of
Fun when is_function(Fun) ->
Fun(Expr);
_ ->
erl_pp:expr(Expr)
end.
pp_intersection_type([], _) ->
"";
%% TODO: pp_type seems to have problems printing bounded types.
pp_intersection_type([{type, _, bounded_fun, [Ty, []]} | Tys], Opts) ->
pp_type(Ty, Opts) ++ ["\n" || Tys /= []] ++ pp_intersection_type(Tys, Opts);
pp_intersection_type([Ty|Tys], Opts) ->
pp_type(Ty, Opts) ++ ["\n" || Tys /= []] ++ pp_intersection_type(Tys, Opts).
pp_type(Ty, Opts) ->
PP = case proplists:get_value(fmt_type_fun, Opts) of
Fun when is_function(Fun) -> Fun(Ty);
_ -> typelib:pp_type(Ty)
end,
case use_color(Opts) of
true -> [?color_type, PP, ?color_end];
false -> PP
end.
print_errors(Errors, Opts) ->
[print_error(Error, Opts) || Error <- Errors],
ok.
print_error(Error, Opts) ->
File = proplists:get_value(filename, Opts),
FmtLoc = proplists:get_value(fmt_location, Opts, verbose),
case File of
undefined -> ok;
_ when FmtLoc =:= brief -> io:format("~s:", [File]);
_ -> io:format("~s: ", [File])
end,
handle_type_error(Error, Opts).
handle_type_error(Error, Opts) ->
io:put_chars(gradualizer_fmt:format_type_error(Error, Opts)).