Skip to content

Commit

Permalink
Handle incomplete type definitions better (erlang-ls#1237)
Browse files Browse the repository at this point in the history
  • Loading branch information
gomoripeti committed Mar 8, 2022
1 parent f9ce810 commit dc5ece1
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 7 deletions.
45 changes: 39 additions & 6 deletions apps/els_lsp/src/els_erlfmt_ast.erl
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,25 @@ erlfmt_to_st(Node) ->
%% Representation for types is in general the same as for
%% corresponding values. The `type` node is not used at all. This
%% means new binary operators `|`, `::`, and `..` inside types.
{attribute, Pos, {atom, _, Tag} = Name, [Def]} when Tag =:= type; Tag =:= opaque ->
{attribute, Pos, {atom, _, Tag} = Name, [{op, OPos, '::', Type, Definition}]}
when Tag =:= type; Tag =:= opaque ->
put('$erlfmt_ast_context$', type),
{op, OPos, '::', Type, Definition} = Def,
{TypeName, Args} =
case Type of
{call, _CPos, TypeName0, Args0} ->
{TypeName0, Args0};
{macro_call, CPos, {_, MPos, _} = MacroName, Args0} ->
EndLoc = maps:get(end_location, MPos),
TypeName0 = {macro_call, CPos#{end_location => EndLoc}, MacroName, none},
{TypeName0, Args0}
{macro_call, _, _, _} ->
%% Note: in the following example the arguments belong to the macro
%% so we set empty type args.
%% `-type ?M(A) :: A.'
%% The erlang preprocessor also prefers the M/1 macro if both M/0
%% and M/1 are defined, but it also allows only M/0. Unfortunately
%% erlang_ls doesn't know what macros are defined.
{Type, []};
_ ->
%% whatever stands at the left side of '::', let's keep it.
%% erlfmt_parser allows atoms and vars too
{Type, []}
end,
Tree =
update_tree_with_meta(
Expand All @@ -133,6 +141,31 @@ erlfmt_to_st(Node) ->
Pos),
erase('$erlfmt_ast_context$'),
Tree;
{attribute, Pos, {atom, _, Tag} = Name, [Def]} when Tag =:= type; Tag =:= opaque ->
%% an incomplete attribute, where `::` operator and the definition missing
%% eg "-type t()."
put('$erlfmt_ast_context$', type),
OPos = element(2, Def),
{TypeName, Args} =
case Def of
{call, _CPos, TypeName0, Args0} ->
{TypeName0, Args0};
_ ->
{Def, []}
end,
%% Set definition as an empty tuple for which els_parser generates no POIs
EmptyDef = erl_syntax:tuple([]),
Tree =
update_tree_with_meta(
erl_syntax:attribute(erlfmt_to_st(Name),
[update_tree_with_meta(
erl_syntax:tuple([erlfmt_to_st(TypeName),
EmptyDef,
erl_syntax:list([erlfmt_to_st(A) || A <- Args])]),
OPos)]),
Pos),
erase('$erlfmt_ast_context$'),
Tree;
{attribute, Pos, {atom, _, RawName} = Name, Args} when RawName =:= callback;
RawName =:= spec ->
put('$erlfmt_ast_context$', type),
Expand Down
39 changes: 39 additions & 0 deletions apps/els_lsp/test/els_parser_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
, parse_invalid_code/1
, parse_incomplete_function/1
, parse_incomplete_spec/1
, parse_incomplete_type/1
, parse_no_tokens/1
, define/1
, underscore_macro/1
Expand Down Expand Up @@ -106,6 +107,44 @@ parse_incomplete_spec(_Config) ->
?assertMatch([#{id := aa}], parse_find_pois(Text, atom)),
ok.

-spec parse_incomplete_type(config()) -> ok.
parse_incomplete_type(_Config) ->
Text = "-type t(A) :: {A aa bb cc}\n.",

%% type range ends where the original dot ends, including ignored parts
?assertMatch([#{id := {t, 1}, range := #{from := {1, 1}, to := {2, 2}}}],
parse_find_pois(Text, type_definition)),
%% only first var is found
?assertMatch([#{id := 'A'}], parse_find_pois(Text, variable)),

Text2 = "-type t",
?assertMatch({ok, [#{ kind := type_definition, id := {t, 0}}]},
els_parser:parse(Text2)),
Text3 = "-type ?t",
?assertMatch({ok, [#{ kind := macro, id := t}]},
els_parser:parse(Text3)),
%% this is not incomplete - there is no way this will become valid erlang
%% but erlfmt can parse it
Text4 = "-type T",
?assertMatch({ok, [#{ kind := variable, id := 'T'}]},
els_parser:parse(Text4)),
Text5 = "-type [1, 2]",
{ok, []} = els_parser:parse(Text5),

%% no type args - assume zero args
Text11 = "-type t :: 1.",
?assertMatch({ok, [#{ kind := type_definition, id := {t, 0}}]},
els_parser:parse(Text11)),
%% no macro args - this is 100% valid code
Text12= "-type ?t :: 1.",
?assertMatch({ok, [#{ kind := macro, id := t}]},
els_parser:parse(Text12)),
Text13 = "-type T :: 1.",
?assertMatch({ok, [#{ kind := variable, id := 'T'}]},
els_parser:parse(Text13)),

ok.

%% Issue #1171
parse_no_tokens(_Config) ->
%% scanning text containing only whitespaces returns an empty list of tokens,
Expand Down
2 changes: 1 addition & 1 deletion apps/els_lsp/test/els_parser_macros_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ type_name_macro(_Config) ->
Text1 = "-type ?M() :: integer() | t().",
?assertMatch({ok, [#{kind := type_application, id := {t, 0}},
#{kind := type_application, id := {erlang, integer, 0}},
#{kind := macro, id := 'M'}]},
#{kind := macro, id := {'M', 0}}]},
els_parser:parse(Text1)),

%% The macro is parsed as (?M()), rather than (?M)()
Expand Down

0 comments on commit dc5ece1

Please sign in to comment.