%% A bunch of helpers to help to deal with errors in Elixir source code. %% This is not exposed in the Elixir language. %% %% Notice this is also called by the Erlang backend, so we also support %% the line number to be none (as it may happen in some erlang errors). -module(elixir_errors). -export([compile_error/3, compile_error/4, warning_prefix/0, form_error/4, form_warn/4, parse_error/4, erl_warn/3, io_warn/4]). -include("elixir.hrl"). %% Low-level warning, should be used only from Erlang passes. -spec erl_warn(non_neg_integer() | none, unicode:chardata(), unicode:chardata()) -> ok. erl_warn(none, File, Warning) -> erl_warn(0, File, Warning); erl_warn(Line, File, Warning) when is_integer(Line), is_binary(File) -> io_warn(Line, File, Warning, [Warning, "\n ", file_format(Line, File), $\n]). %% Low-level warning, all other warnings are built on top of it. -spec io_warn(non_neg_integer() | nil, unicode:chardata() | nil, unicode:chardata(), unicode:chardata()) -> ok. io_warn(Line, File, LogMessage, PrintMessage) when is_integer(Line) or (Line == nil), is_binary(File) or (File == nil) -> send_warning(Line, File, LogMessage), print_warning(PrintMessage). -spec warning_prefix() -> binary(). warning_prefix() -> case application:get_env(elixir, ansi_enabled) of {ok, true} -> <<"\e[33mwarning: \e[0m">>; _ -> <<"warning: ">> end. %% General forms handling. -spec form_error(list(), binary() | #{file := binary()}, module(), any()) -> no_return(). form_error(Meta, #{file := File}, Module, Desc) -> compile_error(Meta, File, Module:format_error(Desc)); form_error(Meta, File, Module, Desc) -> compile_error(Meta, File, Module:format_error(Desc)). -spec form_warn(list(), binary() | #{file := binary()}, module(), any()) -> ok. form_warn(Meta, File, Module, Desc) when is_list(Meta), is_binary(File) -> do_form_warn(Meta, File, #{}, Module:format_error(Desc)); form_warn(Meta, #{file := File} = E, Module, Desc) when is_list(Meta) -> do_form_warn(Meta, File, E, Module:format_error(Desc)). do_form_warn(Meta, GivenFile, E, Warning) -> {File, Line} = meta_location(Meta, GivenFile), Location = case E of #{function := {Name, Arity}, module := Module} -> [file_format(Line, File), ": ", 'Elixir.Exception':format_mfa(Module, Name, Arity)]; #{module := Module} when Module /= nil -> [file_format(Line, File), ": ", elixir_aliases:inspect(Module)]; #{} -> file_format(Line, File) end, io_warn(Line, File, Warning, [Warning, "\n ", Location, $\n]). %% Compilation error. -spec compile_error(list(), binary(), binary() | unicode:charlist()) -> no_return(). -spec compile_error(list(), binary(), string(), list()) -> no_return(). compile_error(Meta, File, Message) when is_binary(Message) -> {MetaFile, MetaLine} = meta_location(Meta, File), raise(MetaLine, MetaFile, 'Elixir.CompileError', Message); compile_error(Meta, File, Message) when is_list(Message) -> {MetaFile, MetaLine} = meta_location(Meta, File), raise(MetaLine, MetaFile, 'Elixir.CompileError', elixir_utils:characters_to_binary(Message)). compile_error(Meta, File, Format, Args) when is_list(Format) -> compile_error(Meta, File, io_lib:format(Format, Args)). %% Tokenization parsing/errors. -spec parse_error(non_neg_integer(), binary() | {binary(), binary()}, binary(), binary()) -> no_return(). parse_error(Line, File, Error, <<>>) -> Message = case Error of <<"syntax error before: ">> -> <<"syntax error: expression is incomplete">>; _ -> Error end, raise(Line, File, 'Elixir.TokenMissingError', Message); %% Show a nicer message for end of line parse_error(Line, File, <<"syntax error before: ">>, <<"eol">>) -> raise(Line, File, 'Elixir.SyntaxError', <<"unexpectedly reached end of line. The current expression is invalid or incomplete">>); %% Show a nicer message for keywords pt1 (Erlang keywords show up wrapped in single quotes) parse_error(Line, File, <<"syntax error before: ">>, Keyword) when Keyword == <<"'not'">>; Keyword == <<"'and'">>; Keyword == <<"'or'">>; Keyword == <<"'when'">>; Keyword == <<"'after'">>; Keyword == <<"'catch'">>; Keyword == <<"'end'">> -> raise_keyword(Line, File, binary_part(Keyword, 1, byte_size(Keyword) - 2)); %% Show a nicer message for keywords pt2 (Elixir keywords show up as is) parse_error(Line, File, <<"syntax error before: ">>, Keyword) when Keyword == <<"fn">>; Keyword == <<"else">>; Keyword == <<"rescue">>; Keyword == <<"true">>; Keyword == <<"false">>; Keyword == <<"nil">>; Keyword == <<"in">> -> raise_keyword(Line, File, Keyword); %% Produce a human-readable message for errors before a sigil parse_error(Line, File, <<"syntax error before: ">>, <<"{sigil,", _Rest/binary>> = Full) -> {sigil, _, Sigil, [Content | _], _, _} = parse_erl_term(Full), Content2 = case is_binary(Content) of true -> Content; false -> <<>> end, Message = <<"syntax error before: sigil \~", Sigil, " starting with content '", Content2/binary, "'">>, raise(Line, File, 'Elixir.SyntaxError', Message); %% Binaries (and interpolation) are wrapped in [<<...>>] parse_error(Line, File, Error, <<"[", _/binary>> = Full) when is_binary(Error) -> Term = case parse_erl_term(Full) of [H | _] when is_binary(H) -> <<$", H/binary, $">>; _ -> <<$">> end, raise(Line, File, 'Elixir.SyntaxError', <>); %% Given a string prefix and suffix to insert the token inside the error message rather than append it parse_error(Line, File, {ErrorPrefix, ErrorSuffix}, Token) when is_binary(ErrorPrefix), is_binary(ErrorSuffix), is_binary(Token) -> Message = <>, raise(Line, File, 'Elixir.SyntaxError', Message); %% Misplaced char tokens (e.g., {char, _, 97}) are translated by Erlang into %% the char literal (i.e., the token in the previous example becomes $a), %% because {char, _, _} is a valid Erlang token for an Erlang char literal. We %% want to represent that token as ?a in the error, according to the Elixir %% syntax. parse_error(Line, File, <<"syntax error before: ">>, <<$$, Char/binary>>) -> Message = <<"syntax error before: ?", Char/binary>>, raise(Line, File, 'Elixir.SyntaxError', Message); %% Everything else is fine as is parse_error(Line, File, Error, Token) when is_binary(Error), is_binary(Token) -> Message = <>, raise(Line, File, 'Elixir.SyntaxError', Message). parse_erl_term(Term) -> {ok, Tokens, _} = erl_scan:string(binary_to_list(Term)), {ok, Parsed} = erl_parse:parse_term(Tokens ++ [{dot, 1}]), Parsed. raise_keyword(Line, File, Keyword) -> raise(Line, File, 'Elixir.SyntaxError', <<"syntax error before: ", Keyword/binary, ". \"", Keyword/binary, "\" is a " "keyword in Elixir and therefore its usage is limited. For instance, it can't " "be used as a variable or be defined nor invoked as a regular function">>). %% Helpers print_warning(Message) -> io:put_chars(standard_error, [warning_prefix(), Message, $\n]), ok. send_warning(Line, File, Message) -> CompilerPid = get(elixir_compiler_pid), if CompilerPid =/= undefined -> CompilerPid ! {warning, File, Line, Message}, elixir_code_server:cast({register_warning, CompilerPid}); true -> ok end, ok. file_format(0, File) -> io_lib:format("~ts", [elixir_utils:relative_to_cwd(File)]); file_format(Line, File) -> io_lib:format("~ts:~w", [elixir_utils:relative_to_cwd(File), Line]). meta_location(Meta, File) -> case elixir_utils:meta_keep(Meta) of {F, L} -> {F, L}; nil -> {File, ?line(Meta)} end. raise(none, File, Kind, Message) -> raise(0, File, Kind, Message); raise({Line, _, _}, File, Kind, Message) when is_integer(Line) -> raise(Line, File, Kind, Message); raise(Line, File, Kind, Message) when is_integer(Line), is_binary(File), is_binary(Message) -> Stacktrace = try throw(ok) catch _:_:Stack -> Stack end, Exception = Kind:exception([{description, Message}, {file, File}, {line, Line}]), erlang:raise(error, Exception, tl(Stacktrace)).