diff --git a/apps/els_lsp/src/els_erlfmt_ast.erl b/apps/els_lsp/src/els_erlfmt_ast.erl index 130f3b404..bbff5e112 100644 --- a/apps/els_lsp/src/els_erlfmt_ast.erl +++ b/apps/els_lsp/src/els_erlfmt_ast.erl @@ -403,6 +403,10 @@ erlfmt_to_st(Node) -> {clause, _, _, _, _} = Clause -> %% clauses of case/if/receive/try erlfmt_clause_to_st(Clause); + {else_clause, Pos, Clauses} -> + %% The else clause of a maybe expression - in OTP it is just called + %% 'else' but has the same format and content + erlfmt_to_st_1({'else', Pos, Clauses}); %% Lists are represented as a `list` node instead of a chain of `cons` %% and `nil` nodes, similar to the `tuple` node. The last element of %% the list can be a `cons` node representing explicit consing syntax. diff --git a/apps/els_lsp/src/els_parser.erl b/apps/els_lsp/src/els_parser.erl index e3a533446..16ed65367 100644 --- a/apps/els_lsp/src/els_parser.erl +++ b/apps/els_lsp/src/els_parser.erl @@ -36,6 +36,12 @@ -dialyzer([{nowarn_function, parse_text/1}]). -dialyzer([{nowarn_function, fix_erlfmt/1}]). +%% Spec of erlfmt_parse:parse_node/1 is wrong, +%% error location can be returned in various formats +%% see https://github.com/WhatsApp/erlfmt/pull/352 + +-dialyzer([{nowarn_function, loc_to_pos/1}]). + %%============================================================================== %% API %%============================================================================== @@ -149,25 +155,36 @@ parse_incomplete_tokens(Tokens) -> {ok, Form} -> {ok, Form}; {error, {ErrorLoc, erlfmt_parse, _Reason}} -> - TrimmedTokens = tokens_until(Tokens, ErrorLoc), + ErrorPos = loc_to_pos(ErrorLoc), + TrimmedTokens = tokens_until(Tokens, ErrorPos), parse_incomplete_tokens(TrimmedTokens) end. +%% Convert location in various formats to a consistent position that can always be compared +-spec loc_to_pos(erlfmt_scan:anno() | erl_anno:location()) -> pos(). +loc_to_pos(#{location := Loc}) -> + %% erlfmt_scan:anno() + loc_to_pos(Loc); +loc_to_pos({Line, Col} = Loc) when is_integer(Line), is_integer(Col) -> + Loc; +loc_to_pos(Line) when is_integer(Line) -> + {Line, 0}. + %% @doc Drop tokens after given location but keep final dot, to preserve its %% location --spec tokens_until([erlfmt_scan:token()], erl_anno:location()) -> +-spec tokens_until([erlfmt_scan:token()], pos()) -> [erlfmt_scan:token()]. -tokens_until([_Hd, {dot, _} = Dot], _Loc) -> +tokens_until([_Hd, {dot, _} = Dot], _Pos) -> %% We need to drop at least one token before the dot. %% Otherwise if error location is at the dot, we cannot just drop the dot and %% add a dot again, because it would result in an infinite loop. [Dot]; -tokens_until([Hd | Tail], Loc) -> - case erlfmt_scan:get_anno(location, Hd) < Loc of +tokens_until([Hd | Tail], Pos) -> + case erlfmt_scan:get_anno(location, Hd) < Pos of true -> - [Hd | tokens_until(Tail, Loc)]; + [Hd | tokens_until(Tail, Pos)]; false -> - tokens_until(Tail, Loc) + tokens_until(Tail, Pos) end. %% `erlfmt_scan' does not support start location other than {1,1} diff --git a/apps/els_lsp/test/els_parser_SUITE.erl b/apps/els_lsp/test/els_parser_SUITE.erl index 552dcccbd..7993c59b1 100644 --- a/apps/els_lsp/test/els_parser_SUITE.erl +++ b/apps/els_lsp/test/els_parser_SUITE.erl @@ -4,7 +4,8 @@ -export([ all/0, init_per_suite/1, - end_per_suite/1 + end_per_suite/1, + init_per_testcase/2 ]). %% Test cases @@ -32,6 +33,9 @@ opaque_recursive/1, record_def_recursive/1, var_in_application/1, + comprehensions/1, + map_comprehensions/1, + maybe_expr/1, unicode_clause_pattern/1, latin1_source_code/1, record_comment/1, @@ -61,6 +65,23 @@ end_per_suite(_Config) -> ok. -spec all() -> [atom()]. all() -> els_test_utils:all(?MODULE). +init_per_testcase(map_comprehensions, Config) -> + case list_to_integer(erlang:system_info(otp_release)) < 26 of + true -> + {skip, "Map comprehensions are only supported from OTP 26"}; + false -> + Config + end; +init_per_testcase(maybe_expr, Config) -> + case list_to_integer(erlang:system_info(otp_release)) < 25 of + true -> + {skip, "Maybe expressions are only supported from OTP 25"}; + false -> + Config + end; +init_per_testcase(_, Config) -> + Config. + %%============================================================================== %% Testcases %%============================================================================== @@ -443,6 +464,42 @@ var_in_application(_Config) -> ), ok. +comprehensions(_Config) -> + Text1 = "[X || X <- L]", + ?assertMatch( + [#{id := 'X'}, #{id := 'X'}, #{id := 'L'}], + parse_find_pois(Text1, variable) + ), + + Text2 = "<< <> || <> <= B >>", + ?assertMatch( + [#{id := 'Y'}, #{id := 'X'}, #{id := 'X'}, #{id := 'Y'}, #{id := 'B'}], + parse_find_pois(Text2, variable) + ), + ok. + +map_comprehensions(_Config) -> + Text3 = "#{ Y => X || X := Y <- M }", + ?assertMatch( + [#{id := 'Y'}, #{id := 'X'}, #{id := 'X'}, #{id := 'Y'}, #{id := 'M'}], + parse_find_pois(Text3, variable) + ), + ok. + +maybe_expr(_Config) -> + Text1 = "maybe {ok, X} ?= f(), {ok, Y} ?= g() end", + ?assertMatch( + [#{id := 'X'}, #{id := 'Y'}], + parse_find_pois(Text1, variable) + ), + + Text2 = "maybe {ok, X} ?= f() else {error, Err} -> Err end", + ?assertMatch( + [#{id := 'X'}, #{id := 'Err'}, #{id := 'Err'}], + parse_find_pois(Text2, variable) + ), + ok. + -spec unicode_clause_pattern(config()) -> ok. unicode_clause_pattern(_Config) -> %% From OTP compiler's bs_utf_SUITE.erl diff --git a/apps/els_lsp/test/els_parser_macros_SUITE.erl b/apps/els_lsp/test/els_parser_macros_SUITE.erl index be4dccf25..4ded8d361 100644 --- a/apps/els_lsp/test/els_parser_macros_SUITE.erl +++ b/apps/els_lsp/test/els_parser_macros_SUITE.erl @@ -19,7 +19,9 @@ macro_in_application/1, record_def_field_macro/1, module_macro_as_record_name/1, - other_macro_as_record_name/1 + other_macro_as_record_name/1, + macro_guards/1, + macro_as_case_clause/1 ]). %%============================================================================== @@ -229,6 +231,30 @@ other_macro_as_record_name(_Config) -> ?assertMatch([_], parse_find_pois(Text4, macro, 'M')), ok. +macro_guards(_Config) -> + Text1 = "?foo(Expr when Guard1)", + ?assertMatch([#{id := 'Expr'}, #{id := 'Guard1'}], parse_find_pois(Text1, variable)), + + Text2 = "?foo(Expr when Guard1; Guard2)", + ?assertMatch( + [#{id := 'Expr'}, #{id := 'Guard1'}, #{id := 'Guard2'}], + parse_find_pois(Text2, variable) + ), + ok. + +%% Supperted by erlfmt since erlfmt#350 +macro_as_case_clause(_Config) -> + Text1 = "case X of ?M1(Y); ?M2(2) end", + ?assertMatch( + [#{id := {'M1', 1}}, #{id := {'M2', 1}}], + parse_find_pois(Text1, macro) + ), + ?assertMatch( + [#{id := 'X'}, #{id := 'Y'}], + parse_find_pois(Text1, variable) + ), + ok. + %%============================================================================== %% Helper functions %%============================================================================== diff --git a/rebar.config b/rebar.config index 57ede8575..b0a3a51c7 100644 --- a/rebar.config +++ b/rebar.config @@ -15,11 +15,7 @@ {docsh, "0.7.2"}, {elvis_core, "~> 1.3"}, {rebar3_format, "0.8.2"}, - %%, {erlfmt, "1.0.0"} - - %% Temp until erlfmt PR 325 is merged (commit d4422d1) - {erlfmt, - {git, "https://github.com/gomoripeti/erlfmt.git", {tag, "erlang_ls_parser_error_loc"}}}, + {erlfmt, "1.3.0"}, {ephemeral, "2.0.4"}, {tdiff, "0.1.2"}, {uuid, "2.0.1", {pkg, uuid_erl}}, diff --git a/rebar.lock b/rebar.lock index 1938be647..fd3fe3a4e 100644 --- a/rebar.lock +++ b/rebar.lock @@ -3,10 +3,7 @@ {<<"docsh">>,{pkg,<<"docsh">>,<<"0.7.2">>},0}, {<<"elvis_core">>,{pkg,<<"elvis_core">>,<<"1.3.1">>},0}, {<<"ephemeral">>,{pkg,<<"ephemeral">>,<<"2.0.4">>},0}, - {<<"erlfmt">>, - {git,"https://github.com/gomoripeti/erlfmt.git", - {ref,"d4422d1fd79a73ef534c2bcbe5b5da4da5338833"}}, - 0}, + {<<"erlfmt">>,{pkg,<<"erlfmt">>,<<"1.3.0">>},0}, {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},2}, {<<"gradualizer">>, {git,"https://github.com/josefs/Gradualizer.git", @@ -31,6 +28,7 @@ {<<"docsh">>, <<"F893D5317A0E14269DD7FE79CF95FB6B9BA23513DA0480EC6E77C73221CAE4F2">>}, {<<"elvis_core">>, <<"844C339300DD3E9F929A045932D25DC5C99B4603D47536E995198143169CDF26">>}, {<<"ephemeral">>, <<"B3E57886ADD5D90C82FE3880F5954978222A122CB8BAA123667401BBAAEC51D6">>}, + {<<"erlfmt">>, <<"672994B92B1A809C04C46F0B781B447BF9AB7A515F5856A96177BC1962F100A9">>}, {<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}, {<<"jsx">>, <<"20A170ABD4335FC6DB24D5FAD1E5D677C55DADF83D1B20A8A33B5FE159892A39">>}, {<<"katana_code">>, <<"B2195859DF57D8BEBF619A9FD3327CD7D01563A98417156D0F4C5FAB435F2630">>}, @@ -46,6 +44,7 @@ {<<"docsh">>, <<"4E7DB461BB07540D2BC3D366B8513F0197712D0495BB85744F367D3815076134">>}, {<<"elvis_core">>, <<"7A8890BF8185A3252CD4EBD826FE5F8AD6B93024EDF88576EB27AE9E5DC19D69">>}, {<<"ephemeral">>, <<"4B293D80F75F9C4575FF4B9C8E889A56802F40B018BF57E74F19644EFEE6C850">>}, + {<<"erlfmt">>, <<"2A84AA1EBA2F4FCD7DD31D5C57E9DE2BC2705DDA18DA4553F27DF7114CFAA052">>}, {<<"getopt">>, <<"53E1AB83B9CEB65C9672D3E7A35B8092E9BDC9B3EE80721471A161C10C59959C">>}, {<<"jsx">>, <<"37BECA0435F5CA8A2F45F76A46211E76418FBEF80C36F0361C249FC75059DC6D">>}, {<<"katana_code">>, <<"8448AD3F56D9814F98A28BE650F7191BDD506575E345CC16D586660B10F6E992">>},