261 lines
9.3 KiB
Erlang
261 lines
9.3 KiB
Erlang
% A bunch of helpers to help to deal with errors in Elixir source code.
|
|
% This is not exposed in the Elixir language.
|
|
-module(elixir_errors).
|
|
-export([syntax_error/3, syntax_error/4,
|
|
compile_error/3, compile_error/4, inspect/1,
|
|
form_error/4, parse_error/4, assert_module_scope/3,
|
|
assert_no_function_scope/3, assert_function_scope/3,
|
|
assert_no_match_scope/3, assert_no_guard_scope/3,
|
|
assert_no_match_or_guard_scope/3, warn/1,
|
|
handle_file_warning/2, handle_file_warning/3, handle_file_error/2,
|
|
deprecation/3, deprecation/4]).
|
|
-include("elixir.hrl").
|
|
|
|
warn(Warning) ->
|
|
elixir_code_server:cast(register_warning),
|
|
io:put_chars(standard_error, Warning).
|
|
|
|
%% Handle inspecting for exceptions modules
|
|
|
|
inspect(Atom) when is_atom(Atom) ->
|
|
'Elixir.Inspect.Atom':inspect(Atom).
|
|
|
|
%% Raised during macros translation.
|
|
|
|
-spec syntax_error(non_neg_integer() | list(), file:filename_all(), binary() | string()) -> no_return().
|
|
-spec syntax_error(non_neg_integer() | list(), file:filename_all(), binary() | string(), list()) -> no_return().
|
|
|
|
syntax_error(Meta, File, Message) when is_list(Message) ->
|
|
syntax_error(Meta, File, iolist_to_binary(Message));
|
|
|
|
syntax_error(Meta, File, Message) when is_binary(Message) ->
|
|
raise(Meta, File, 'Elixir.SyntaxError', Message).
|
|
|
|
syntax_error(Meta, File, Format, Args) ->
|
|
Message = io_lib:format(Format, Args),
|
|
raise(Meta, File, 'Elixir.SyntaxError', elixir_utils:characters_to_binary(Message)).
|
|
|
|
compile_error(Meta, File, Message) when is_list(Message) ->
|
|
compile_error(Meta, File, iolist_to_binary(Message));
|
|
|
|
compile_error(Meta, File, Message) when is_binary(Message) ->
|
|
raise(Meta, File, 'Elixir.CompileError', Message).
|
|
|
|
compile_error(Meta, File, Format, Args) ->
|
|
Message = io_lib:format(Format, Args),
|
|
raise(Meta, File, 'Elixir.CompileError', elixir_utils:characters_to_binary(Message)).
|
|
|
|
%% Raised on tokenizing/parsing
|
|
|
|
-spec parse_error(non_neg_integer() | list(), file:filename_all(), iolist() | atom(), [] | iolist()) -> no_return().
|
|
|
|
parse_error(Meta, File, Error, []) ->
|
|
Message = case Error of
|
|
"syntax error before: " -> <<"syntax error: expression is incomplete">>;
|
|
_ -> iolist_to_binary(Error)
|
|
end,
|
|
raise(Meta, File, 'Elixir.TokenMissingError', Message);
|
|
|
|
parse_error(Meta, File, "syntax error before: ", "'end'") ->
|
|
raise(Meta, File, 'Elixir.SyntaxError', <<"unexpected token: end">>);
|
|
|
|
parse_error(Meta, File, Error, Token) ->
|
|
BinError = if
|
|
is_atom(Error) -> atom_to_binary(Error, utf8);
|
|
true -> iolist_to_binary(Error)
|
|
end,
|
|
|
|
BinToken = if
|
|
Token == [] -> <<>>;
|
|
true -> elixir_utils:characters_to_binary(Token)
|
|
end,
|
|
|
|
Message = <<BinError / binary, BinToken / binary >>,
|
|
raise(Meta, File, 'Elixir.SyntaxError', Message).
|
|
|
|
%% Raised during compilation
|
|
|
|
-spec form_error(non_neg_integer() | list(), file:filename_all(), module(), any()) -> no_return().
|
|
|
|
form_error(Meta, File, Module, Desc) ->
|
|
Message = iolist_to_binary(format_error(Module, Desc)),
|
|
raise(Meta, File, 'Elixir.CompileError', Message).
|
|
|
|
%% Shows a deprecation message
|
|
|
|
deprecation(Meta, File, Message) -> deprecation(Meta, File, Message, []).
|
|
|
|
deprecation(Meta, File, Message, Args) ->
|
|
io:format(file_format(?line(Meta), File, io_lib:format(Message, Args))).
|
|
|
|
%% Handle warnings and errors (called during module compilation)
|
|
|
|
%% Ignore on bootstrap
|
|
handle_file_warning(true, _File, { _Line, sys_core_fold, nomatch_guard }) -> [];
|
|
handle_file_warning(true, _File, { _Line, sys_core_fold, { nomatch_shadow, _ } }) -> [];
|
|
|
|
%% Ignore always
|
|
handle_file_warning(_, _File, { _Line, sys_core_fold, useless_building }) -> [];
|
|
|
|
%% This is an Erlang bug, it considers { tuple, _ }.call to always fail
|
|
handle_file_warning(_, _File, { _Line, v3_kernel, bad_call }) -> [];
|
|
|
|
%% We handle unused local warnings ourselves
|
|
handle_file_warning(_, _File, { _Line, erl_lint, { unused_function, _ } }) -> [];
|
|
|
|
%% Make no_effect clauses pretty
|
|
handle_file_warning(_, File, { Line, sys_core_fold, { no_effect, { erlang, F, A } } }) ->
|
|
{ Fmt, Args } = case erl_internal:comp_op(F, A) of
|
|
true -> { "use of operator ~ts has no effect", [translate_comp_op(F)] };
|
|
false ->
|
|
case erl_internal:bif(F, A) of
|
|
false -> { "the call to :erlang.~ts/~B has no effect", [F,A] };
|
|
true -> { "the call to ~ts/~B has no effect", [F,A] }
|
|
end
|
|
end,
|
|
Message = io_lib:format(Fmt, Args),
|
|
warn(file_format(Line, File, Message));
|
|
|
|
%% Rewrite undefined behaviour to check for protocols
|
|
handle_file_warning(_, File, {Line,erl_lint,{undefined_behaviour_func,{Fun,Arity},Module}}) ->
|
|
{ DefKind, Def, DefArity } =
|
|
case atom_to_list(Fun) of
|
|
"MACRO-" ++ Rest -> { macro, list_to_atom(Rest), Arity - 1 };
|
|
_ -> { function, Fun, Arity }
|
|
end,
|
|
|
|
Kind = protocol_or_behaviour(Module),
|
|
Raw = "undefined ~ts ~ts ~ts/~B (for ~ts ~ts)",
|
|
Message = io_lib:format(Raw, [Kind, DefKind, Def, DefArity, Kind, inspect(Module)]),
|
|
warn(file_format(Line, File, Message));
|
|
|
|
handle_file_warning(_, File, {Line,erl_lint,{undefined_behaviour,Module}}) ->
|
|
case elixir_compiler:get_opt(internal) of
|
|
true -> [];
|
|
false ->
|
|
Message = io_lib:format("behaviour ~ts undefined", [inspect(Module)]),
|
|
warn(file_format(Line, File, Message))
|
|
end;
|
|
|
|
%% Ignore unused vars at "weird" lines (<= 0)
|
|
handle_file_warning(_, _File, {Line,erl_lint,{unused_var,_Var}}) when Line =< 0 ->
|
|
[];
|
|
|
|
%% Ignore shadowed vars as we guarantee no conflicts ourselves
|
|
handle_file_warning(_, _File, {_Line,erl_lint,{shadowed_var,_Var,_Where}}) ->
|
|
[];
|
|
|
|
%% Properly format other unused vars
|
|
handle_file_warning(_, File, {Line,erl_lint,{unused_var,Var}}) ->
|
|
Message = format_error(erl_lint, { unused_var, format_var(Var) }),
|
|
warn(file_format(Line, File, Message));
|
|
|
|
%% Default behavior
|
|
handle_file_warning(_, File, {Line,Module,Desc}) ->
|
|
Message = format_error(Module, Desc),
|
|
warn(file_format(Line, File, Message)).
|
|
|
|
handle_file_warning(File, Desc) ->
|
|
handle_file_warning(false, File, Desc).
|
|
|
|
-spec handle_file_error(file:filename_all(), {non_neg_integer(), module(), any()}) -> no_return().
|
|
|
|
handle_file_error(File, {Line,erl_lint,{unsafe_var,Var,{In,_Where}}}) ->
|
|
Translated = case In of
|
|
'orelse' -> 'or';
|
|
'andalso' -> 'and';
|
|
_ -> In
|
|
end,
|
|
Message = io_lib:format("cannot define variable ~ts inside ~ts", [format_var(Var), Translated]),
|
|
raise(Line, File, 'Elixir.CompileError', iolist_to_binary(Message));
|
|
|
|
handle_file_error(File, {Line,erl_lint,{spec_fun_undefined,{M,F,A}}}) ->
|
|
Message = io_lib:format("spec for undefined function ~ts.~ts/~B", [inspect(M), F, A]),
|
|
raise(Line, File, 'Elixir.CompileError', iolist_to_binary(Message));
|
|
|
|
handle_file_error(File, {Line,Module,Desc}) ->
|
|
form_error(Line, File, Module, Desc).
|
|
|
|
%% Assertions
|
|
|
|
assert_no_function_scope(_Meta, _Kind, #elixir_scope{function=nil}) -> [];
|
|
assert_no_function_scope(Meta, Kind, S) ->
|
|
syntax_error(Meta, S#elixir_scope.file, "cannot invoke ~ts inside a function", [Kind]).
|
|
|
|
assert_module_scope(Meta, Kind, #elixir_scope{module=nil,file=File}) ->
|
|
syntax_error(Meta, File, "cannot invoke ~ts outside module", [Kind]);
|
|
assert_module_scope(_Meta, _Kind, #elixir_scope{module=Module}) -> Module.
|
|
|
|
assert_function_scope(Meta, Kind, #elixir_scope{function=nil,file=File}) ->
|
|
syntax_error(Meta, File, "cannot invoke ~ts outside function", [Kind]);
|
|
assert_function_scope(_Meta, _Kind, #elixir_scope{function=Function}) -> Function.
|
|
|
|
assert_no_match_or_guard_scope(Meta, Kind, S) ->
|
|
assert_no_match_scope(Meta, Kind, S),
|
|
assert_no_guard_scope(Meta, Kind, S).
|
|
|
|
assert_no_match_scope(Meta, _Kind, #elixir_scope{context=match} = S) ->
|
|
compile_error(Meta, S#elixir_scope.file, "invalid pattern in match clause", []);
|
|
assert_no_match_scope(_Meta, _Kind, _S) -> [].
|
|
|
|
assert_no_guard_scope(Meta, _Kind, #elixir_scope{context=guard} = S) ->
|
|
compile_error(Meta, S#elixir_scope.file, "invalid pattern in guard", []);
|
|
assert_no_guard_scope(_Meta, _Kind, _S) -> [].
|
|
|
|
%% Helpers
|
|
|
|
raise(Meta, File, Kind, Message) when is_list(Meta) ->
|
|
raise(?line(Meta), File, Kind, Message);
|
|
|
|
raise(none, File, Kind, Message) ->
|
|
raise(0, File, Kind, Message);
|
|
|
|
raise(Line, File, Kind, Message) when is_integer(Line) ->
|
|
%% Populate the stacktrace so we can raise it
|
|
try
|
|
throw(ok)
|
|
catch
|
|
ok -> ok
|
|
end,
|
|
Stacktrace = erlang:get_stacktrace(),
|
|
Exception = Kind:new([{description, Message}, {file, iolist_to_binary(File)}, {line, Line}]),
|
|
erlang:raise(error, Exception, tl(Stacktrace)).
|
|
|
|
file_format(0, File, Message) ->
|
|
io_lib:format("~ts: ~ts~n", [elixir_utils:relative_to_cwd(File), Message]);
|
|
|
|
file_format(Line, File, Message) ->
|
|
io_lib:format("~ts:~w: ~ts~n", [elixir_utils:relative_to_cwd(File), Line, Message]).
|
|
|
|
format_var(Var) ->
|
|
list_to_atom(lists:takewhile(fun(X) -> X /= $@ end, atom_to_list(Var))).
|
|
|
|
format_error([], Desc) ->
|
|
io_lib:format("~p", [Desc]);
|
|
|
|
format_error(Module, Desc) ->
|
|
Module:format_error(Desc).
|
|
|
|
protocol_or_behaviour(Module) ->
|
|
case is_protocol(Module) of
|
|
true -> protocol;
|
|
false -> behaviour
|
|
end.
|
|
|
|
is_protocol(Module) ->
|
|
case code:ensure_loaded(Module) of
|
|
{ module, _ } ->
|
|
case erlang:function_exported(Module, '__protocol__', 1) of
|
|
true -> Module:'__protocol__'(name) == Module;
|
|
false -> false
|
|
end;
|
|
{ error, _ } ->
|
|
false
|
|
end.
|
|
|
|
translate_comp_op('/=') -> '!=';
|
|
translate_comp_op('=<') -> '<=';
|
|
translate_comp_op('=:=') -> '===';
|
|
translate_comp_op('=/=') -> '!==';
|
|
translate_comp_op(Other) -> Other.
|