Permalink
Browse files

Add hints on mismatched do/end and others pairs

When you are working on large files and you add new code,
you may forget to add a `do` or an `end`. In those cases,
the error message usually points to the first `do` or the
last `end` in the file, which are usually far away from
the source of the error.

This pull request adds a simple heuristic based on the
indentation of the tokens, to try to provide hints of
where the source may be. Those hints are not deterministic
but they may be able to point users to the source of the
problem.

For example, in this case:

    defmodule MyApp do
      def one do
      # end

      def two do
      end
    end

we know that we now that `def two do` is happening on the
same indentation as `def one do`, which may mean that
`def one do` was not closed properly. We store this as a
hint in case the terminators do not match later.

Similarly, in the case below:

    defmodule MyApp do
      def one
      end

      def two do
      end
    end

The `end` on line 3 will end-up closing the defmodule `do`,
on line 1. Because their indentation do not match, it may
be that there is a missing `do`, where the `end` was supposed
to align.

Some basic testing show those heuristics work on the majority
of the cases, but we will only be sure when we have enough
feedback from the community.
  • Loading branch information...
josevalim committed Jun 10, 2018
1 parent 3f18cec commit 5edb1d2739af074cf555f4300de9da71c214211d
View
@@ -304,25 +304,36 @@ string_to_tokens(String, StartLine, File, Opts) when is_integer(StartLine), is_b
case elixir_tokenizer:tokenize(String, StartLine, [{file, File} | Opts]) of
{ok, _Tokens} = Ok ->
Ok;
{error, {Line, {ErrorPrefix, ErrorSuffix}, Token}, _Rest, _SoFar} ->
{error, {Line, _, {ErrorPrefix, ErrorSuffix}, Token}, _Rest, _SoFar} ->
{error, {Line, {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)}};
{error, {Line, Error, Token}, _Rest, _SoFar} ->
{error, {Line, _, Error, Token}, _Rest, _SoFar} ->
{error, {Line, to_binary(Error), to_binary(Token)}}
end.
tokens_to_quoted(Tokens, File, Opts) ->
handle_parsing_opts(File, Opts),
try elixir_parser:parse(Tokens) of
{ok, Forms} -> {ok, Forms};
{error, {{Line, _, _}, _, [Error, Token]}} -> {error, {Line, to_binary(Error), to_binary(Token)}};
{error, {Line, _, [Error, Token]}} -> {error, {Line, to_binary(Error), to_binary(Token)}}
{ok, Forms} ->
{ok, Forms};
{error, {Line, _, [{ErrorPrefix, ErrorSuffix}, Token]}} ->
{error, {parser_line(Line), {to_binary(ErrorPrefix), to_binary(ErrorSuffix)}, to_binary(Token)}};
{error, {Line, _, [Error, Token]}} ->
{error, {parser_line(Line), to_binary(Error), to_binary(Token)}}
after
erase(elixir_parser_file),
erase(elixir_parser_columns),
erase(elixir_formatter_metadata)
end.
parser_line({Line, _, _}) ->
Line;
parser_line(Meta) ->
case lists:keyfind(line, 1, Meta) of
{line, L} -> L;
false -> 0
end.
'string_to_quoted!'(String, StartLine, File, Opts) ->
case string_to_tokens(String, StartLine, File, Opts) of
{ok, Tokens} ->
@@ -340,10 +351,8 @@ to_binary(List) when is_list(List) -> elixir_utils:characters_to_binary(List);
to_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
handle_parsing_opts(File, Opts) ->
FormatterMetadata =
lists:keyfind(formatter_metadata, 1, Opts) == {formatter_metadata, true},
Columns =
lists:keyfind(columns, 1, Opts) == {columns, true},
FormatterMetadata = lists:keyfind(formatter_metadata, 1, Opts) == {formatter_metadata, true},
Columns = lists:keyfind(columns, 1, Opts) == {columns, true},
put(elixir_parser_file, File),
put(elixir_parser_columns, Columns),
put(elixir_formatter_metadata, FormatterMetadata).
@@ -35,5 +35,7 @@
check_terminators=true,
existing_atoms_only=false,
preserve_comments=nil,
identifier_tokenizer=elixir_tokenizer
identifier_tokenizer=elixir_tokenizer,
indentation=0,
mismatch_hints=[]
}).
@@ -69,10 +69,6 @@ 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 missing end tokens
parse_error(Line, File, <<"syntax error before: ">>, <<"'end'">>) ->
raise(Line, File, 'Elixir.SyntaxError', <<"unexpected token: end">>);
%% 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),
@@ -19,8 +19,8 @@ extract(Line, Column, Raw, Interpol, String, Last) ->
extract(Line, Column, _Scope, _Interpol, [], Buffer, Output, []) ->
finish_extraction(Line, Column, Buffer, Output, []);
extract(Line, _Column, _Scope, _Interpol, [], _Buffer, _Output, Last) ->
{error, {string, Line, io_lib:format("missing terminator: ~ts", [[Last]]), []}};
extract(Line, Column, _Scope, _Interpol, [], _Buffer, _Output, Last) ->
{error, {string, Line, Column, io_lib:format("missing terminator: ~ts", [[Last]]), []}};
extract(Line, Column, _Scope, _Interpol, [Last | Remaining], Buffer, Output, Last) ->
finish_extraction(Line, Column + 1, Buffer, Output, Remaining);
@@ -55,13 +55,13 @@ extract(Line, Column, Scope, true, [$\\, $#, ${ | Rest], Buffer, Output, Last) -
extract(Line, Column, Scope, true, [$#, ${ | Rest], Buffer, Output, Last) ->
Output1 = build_string(Line, Buffer, Output),
case elixir_tokenizer:tokenize(Rest, Line, Column + 2, Scope) of
{error, {{EndLine, EndColumn, _}, _, "}"}, [$} | NewRest], Tokens} ->
{error, {EndLine, EndColumn, _, "}"}, [$} | NewRest], Tokens} ->
Output2 = build_interpol(Line, Column, EndLine, Tokens, Output1),
extract(EndLine, EndColumn + 1, Scope, true, NewRest, [], Output2, Last);
{error, Reason, _, _} ->
{error, Reason};
{ok, _} ->
{error, {string, Line, "missing interpolation terminator:}", []}}
{error, {string, Line, Column, "missing interpolation terminator: \"}\"", []}}
end;
extract(Line, Column, Scope, Interpol, [$\\, Char | Rest], Buffer, Output, Last) ->
@@ -973,21 +973,17 @@ unwrap_when(Args) ->
%% Warnings and errors
return_error(Meta, Error, Token) ->
Line =
case lists:keyfind(line, 1, Meta) of
{line, L} -> L;
false -> 0
end,
return_error(Line, [Error, Token]).
return_error(Meta, [Error, Token]).
error_invalid_stab(MetaStab) ->
return_error(MetaStab,
"unexpected operator ->. If you want to define multiple clauses, the first expression must use ->. "
"Syntax error before: ", "'->'").
error_bad_atom(Token) ->
return_error(meta_from_token(Token), "atom cannot be followed by an alias. If the '.' was meant to be "
"part of the atom's name, the atom name must be quoted. Syntax error before: ", "'.'").
return_error(meta_from_token(Token), "atom cannot be followed by an alias. "
"If the '.' was meant to be part of the atom's name, "
"the atom name must be quoted. Syntax error before: ", "'.'").
error_no_parens_strict(Token) ->
return_error(meta_from_token(Token), "unexpected parentheses. If you are making a "
@@ -1022,9 +1018,9 @@ error_no_parens_container_strict(Node) ->
"Elixir cannot compile otherwise. Syntax error before: ", "','").
error_invalid_kw_identifier({_, _, do} = Token) ->
return_error(meta_from_token(Token), elixir_tokenizer:invalid_do_error("unexpected keyword \"do:\""), "'do:'");
return_error(meta_from_token(Token), elixir_tokenizer:invalid_do_error("unexpected keyword: "), "do:");
error_invalid_kw_identifier({_, _, KW} = Token) ->
return_error(meta_from_token(Token), "syntax error before: ", "'" ++ atom_to_list(KW) ++ "':").
return_error(meta_from_token(Token), "syntax error before: ", "'" ++ atom_to_list(KW) ++ ":'").
%% TODO: Make this an error on Elixir v2.0.
warn_empty_paren({_, {Line, _, _}}) ->
Oops, something went wrong.

0 comments on commit 5edb1d2

Please sign in to comment.