diff --git a/apps/els_lsp/priv/code_navigation/src/hover_macro.erl b/apps/els_lsp/priv/code_navigation/src/hover_macro.erl index fefee3719..9b5eb7dec 100644 --- a/apps/els_lsp/priv/code_navigation/src/hover_macro.erl +++ b/apps/els_lsp/priv/code_navigation/src/hover_macro.erl @@ -5,3 +5,8 @@ f() -> ?LOCAL_MACRO, ?INCLUDED_MACRO_A. + +-define(WEIRD_MACRO, A when A > 1). + +g() -> + case foo of ?WEIRD_MACRO -> ok end. diff --git a/apps/els_lsp/src/els_bound_var_in_pattern_diagnostics.erl b/apps/els_lsp/src/els_bound_var_in_pattern_diagnostics.erl index 40002beaa..75e303409 100644 --- a/apps/els_lsp/src/els_bound_var_in_pattern_diagnostics.erl +++ b/apps/els_lsp/src/els_bound_var_in_pattern_diagnostics.erl @@ -52,7 +52,7 @@ source() -> -spec find_vars(uri()) -> [poi()]. find_vars(Uri) -> {ok, #{text := Text}} = els_utils:lookup_document(Uri), - {ok, Forms} = parse_file(Text), + {ok, Forms} = els_parser:parse_text(Text), lists:flatmap(fun find_vars_in_form/1, Forms). -spec find_vars_in_form(erl_syntax:forms()) -> [poi()]. @@ -126,13 +126,6 @@ find_vars_in_pattern(Tree, Acc) -> Acc end. --spec parse_file(binary()) -> {ok, erl_syntax:forms()}. -parse_file(Text) -> - IoDevice = els_io_string:new(Text), - {ok, Forms} = els_dodger:parse(IoDevice, {1, 1}), - ok = file:close(IoDevice), - {ok, Forms}. - -spec variable(tree()) -> poi(). variable(Tree) -> Id = erl_syntax:variable_name(Tree), diff --git a/apps/els_lsp/src/els_docs.erl b/apps/els_lsp/src/els_docs.erl index f71b81f9d..a786abc1e 100644 --- a/apps/els_lsp/src/els_docs.erl +++ b/apps/els_lsp/src/els_docs.erl @@ -50,9 +50,14 @@ docs(Uri, #{kind := Kind, id := {F, A}}) function_docs('local', M, F, A); docs(Uri, #{kind := macro, id := Name} = POI) -> case els_code_navigation:goto_definition(Uri, POI) of - {ok, _DefUri, #{data := [Value|_]}} -> + {ok, DefUri, #{data := #{value_range := ValueRange}}} -> NameStr = atom_to_list(Name), - Line = lists:flatten(["?", NameStr, " = ", erl_prettypr:format(Value)]), + + {ok, #{text := Text}} = els_utils:lookup_document(DefUri), + #{from := From, to := To} = ValueRange, + ValueText = els_utils:to_list(els_text:range(Text, From, To)), + + Line = lists:flatten(["?", NameStr, " = ", ValueText]), [{code_line, Line}]; _ -> [] diff --git a/apps/els_lsp/src/els_erlfmt_ast.erl b/apps/els_lsp/src/els_erlfmt_ast.erl new file mode 100644 index 000000000..1063925da --- /dev/null +++ b/apps/els_lsp/src/els_erlfmt_ast.erl @@ -0,0 +1,786 @@ +%% Copyright (c) Facebook, Inc. and its affiliates. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. + +%% @doc AST conversion between erlfmt and syntax_tools. + +-module(els_erlfmt_ast). + +-export([erlfmt_to_st/1]). + +%% syntax_tree -> erlfmt conversion is not used in erlang_ls, +%% removed to not have to update it with erlang_ls custom changes and +%% fix dialyzer warnings about erl_syntax:get_pos/set_pos +%%-export([st_to_erlfmt/1]). + +% dialyzer hates erlfmt_parse:abstract_node() +-type erlfmt() :: term(). +-type syntax_tools() :: erl_syntax:syntaxTree(). + +-spec erlfmt_to_st(Node :: erlfmt()) -> syntax_tools(). +%% @doc Convert from erlfmt ASTs to Syntax Tools ASTs. + +%% Note: the erl_syntax library still refers to the 2nd element as "pos" +%% even though it has morphed into a generic annotation in erl_parse trees +%% (represented as property lists, or as maps in the erlfmt +%% representation). Actual erl_syntax nodes have an additional annotation +%% field, separate from the position info, but this is not being used here. +%% Hence, the erl_syntax:set_pos() function is used for all annotations. + +erlfmt_to_st(Node) -> + Context = get('$erlfmt_ast_context$'), + case Node of + %% --------------------------------------------------------------------- + %% The following cases can be easily rewritten without losing information + + %% The special `match` node is encoded as a regular binary operator + {op, Pos, '=', Left, Right} -> + erlfmt_to_st_1({match, Pos, Left, Right}); + %% The special `catch` node is encoded as a regular unary operator + {op, Pos, 'catch', Expr} -> + erlfmt_to_st_1({'catch', Pos, Expr}); + %% Type annotations are represented as :: operators + {op, Pos, '::', Left, Right} -> + erlfmt_to_st_1({ann_type, Pos, [Left, Right]}); + %% --------------------------------------------------------------------- + %% Whenever simply rewriting the node to the corresponding standard + %% erl_parse form would discard information (such as annotations on + %% atoms which are stored naked in the erl_parse format), we must + %% construct the node using the erl_syntax API, which supports + %% preserving annotations on such sub-fields. + + %% raw strings only occur as forms, for when parsing the form failed + {raw_string, Pos, Text} -> + update_tree_with_meta(erl_syntax:text("\n>>>>\n" ++ Text ++ "\n<<<<\n"), Pos); + %% A new node `{macro_call, Anno, Name, Args}` is introduced, where + %% `Name` is either an `atom` or a `var` node and `Args` is a list of + %% expressions, types, or special `op` nodes with `'when'` operator. + {macro_call, Pos, Name, Args} -> + Args1 = + case Args of + none -> none; + _ -> [erlfmt_to_st(A) || A <- Args] + end, + update_tree_with_meta(erl_syntax:macro(erlfmt_to_st(Name), Args1), Pos); + %% The value of an attribute node is always a list of abstract term + %% formats instead of concrete terms. The name is always represented + %% as a full `atom` node. + {attribute, Pos, {atom, _, record} = Tag, [Name, Tuple]} -> + %% The record name is represented as node instead of a raw atom + %% and typed record fields are represented as '::' ops + {tuple, TPos, Fields} = Tuple, + Fields1 = [ + case F of + {op, FPos, '::', B, T} -> + B1 = erlfmt_to_st(B), + %% Convert field types in a type context + put('$erlfmt_ast_context$', type), + T1 = erlfmt_to_st(T), + erase('$erlfmt_ast_context$'), + update_tree_with_meta( + erl_syntax:typed_record_field(B1, T1), + FPos + ); + _ -> + erlfmt_to_st(F) + end + || F <- Fields + ], + Tuple1 = update_tree_with_meta(erl_syntax:tuple(Fields1), TPos), + update_tree_with_meta( + erl_syntax:attribute( + erlfmt_to_st(Tag), + [ + erlfmt_to_st(Name), + Tuple1 + ] + ), + Pos + ); + %% 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 -> + 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} + end, + Tree = + update_tree_with_meta( + erl_syntax:attribute(erlfmt_to_st(Name), + [update_tree_with_meta( + erl_syntax:tuple([erlfmt_to_st(TypeName), + erlfmt_to_st(Definition), + 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), + [{spec, SPos, FName, Clauses}] = Args, + {spec_clause, _, {args, _, ClauseArgs}, _, _} = hd(Clauses), + Arity = length(ClauseArgs), + Tree = + update_tree_with_meta( + erl_syntax:attribute(erlfmt_to_st(Name), + [update_tree_with_meta( + erl_syntax:tuple([erl_syntax:tuple([erlfmt_to_st(FName), erl_syntax:integer(Arity)]), + erl_syntax:list([erlfmt_to_st(C) || C <- Clauses])]), + SPos)]), + Pos), + erase('$erlfmt_ast_context$'), + Tree; + {spec_clause, Pos, {args, _HeadMeta, Args}, [ReturnType], empty} -> + update_tree_with_meta( + erl_syntax_function_type([erlfmt_to_st(A) || A <- Args], + erlfmt_to_st(ReturnType)), + Pos); + {spec_clause, Pos, {args, _HeadMeta, Args}, [ReturnType], GuardOr} -> + FunctionType = + update_tree_with_meta( + erl_syntax_function_type([erlfmt_to_st(A) || A <- Args], + erlfmt_to_st(ReturnType)), + Pos), + FunctionConstraint = erlfmt_guard_to_st(GuardOr), + + update_tree_with_meta( + erl_syntax:constrained_function_type(FunctionType, [FunctionConstraint]), + Pos); + {op, Pos, '|', A, B} when Context =:= type -> + update_tree_with_meta( + erl_syntax:type_union([erlfmt_to_st(A), + erlfmt_to_st(B)]), + Pos); + {op, Pos, '..', A, B} when Context =:= type -> + %% erlfmt_to_st_1({type, Pos, range, [A, B]}), + update_tree_with_meta( + erl_syntax:integer_range_type(erlfmt_to_st(A), + erlfmt_to_st(B)), + Pos); + %%{op, Pos, '::', A, B} when Context =:= type -> + %% update_tree_with_meta( + %% erl_syntax:annotated_type(erlfmt_to_st(A), + %% erlfmt_to_st(B)), + %% Pos); + {record, Pos, Name, Fields} when Context =:= type -> + %% The record name is represented as node instead of a raw atom + %% and typed record fields are represented as '::' ops + Fields1 = [ + case F of + {op, FPos, '::', B, T} -> + B1 = erlfmt_to_st(B), + T1 = erlfmt_to_st(T), + update_tree_with_meta( + erl_syntax:record_type_field(B1, T1), + FPos + ); + _ -> + erlfmt_to_st(F) + end + || F <- Fields + ], + + update_tree_with_meta( + erl_syntax:record_type( + erlfmt_to_st(Name), + Fields1 + ), + Pos + ); + {call, Pos, {remote, _, _, _} = Name, Args} when Context =:= type -> + update_tree_with_meta( + erl_syntax:type_application(erlfmt_to_st(Name), + [erlfmt_to_st(A) || A <- Args]), + Pos); + {call, Pos, Name, Args} when Context =:= type -> + TypeTag = + case Name of + {atom, _, NameAtom} -> + Arity = length(Args), + case erl_internal:is_type(NameAtom, Arity) of + true -> + type_application; + false -> + user_type_application + end; + _ -> + user_type_application + end, + update_tree_with_meta( + erl_syntax:TypeTag(erlfmt_to_st(Name), + [erlfmt_to_st(A) || A <- Args]), + Pos); + {attribute, Pos, {atom, _, define} = Tag, [Name, empty]} -> + %% the erlfmt parser allows defines with empty bodies (with the + %% closing parens following after the comma); we must turn the + %% atom 'empty' into a proper node here + Body = erl_syntax:set_pos(erl_syntax:text(""), dummy_anno()), + update_tree_with_meta( + erl_syntax:attribute( + erlfmt_to_st(Tag), + [ + erlfmt_to_st(Name), + Body + ] + ), + Pos + ); + {attribute, Pos, Name, no_parens} -> + %% a directive without parentheses, like -endif. + update_tree_with_meta(erl_syntax:attribute(erlfmt_to_st(Name)), Pos); + %% Attributes are not processed to convert the `fun/arity` syntax into + %% tuples, they are left as the `op` nodes with the `/` operator. + %% Additionally, the `import` and `export` attributes are not + %% processed to convert the `cons` node chains into lists and contain + %% `list` nodes. + {attribute, Pos, Name, Args} -> + %% general attributes -Name(Arg1, ... ArgN) + %% (Name is not a naked atom, so Node is not erl_parse compatible) + Args1 = [fold_arity_qualifiers(erlfmt_to_st(A)) || A <- Args], + update_tree_with_meta(erl_syntax:attribute(erlfmt_to_st(Name), Args1), Pos); + %% The `function` node has a different AST representation: `{function, + %% Anno, Clauses}`, where `Clauses` is a list of `clause` nodes or + %% `macro_call` nodes. Additionally it is less strict - it does not + %% enforce all clauses have the same name and arity. + {function, Pos, Clauses} -> + case get_function_name(Clauses) of + none -> + %% treat clauses as a list of regular nodes + %% (presumably macro calls) and use an empty text node + %% as the function name + Clauses1 = [erlfmt_to_st(C) || C <- Clauses], + Name = erl_syntax:set_pos(erl_syntax:text(""), dummy_anno()), + update_tree_with_meta( + erl_syntax:function( + Name, + Clauses1 + ), + Pos + ); + Name -> + Clauses1 = [erlfmt_clause_to_st(C) || C <- Clauses], + update_tree_with_meta( + erl_syntax:function( + erlfmt_to_st(Name), + Clauses1 + ), + Pos + ) + end; + {'try', Pos, {body, _, _} = Body, Clauses, Handlers, After} -> + %% TODO: preserving annotations on bodies and clause groups + Body1 = [erlfmt_to_st(Body)], + Clauses1 = case Clauses of + {clauses, _, CList} -> + [erlfmt_clause_to_st(C) || C <- CList]; + none -> + [] + end, + Handlers1 = case Handlers of + {clauses, _, HList} -> + [erlfmt_clause_to_st(C) || C <- HList]; + none -> + [] + end, + After1 = [erlfmt_to_st(E) || E <- After], + update_tree_with_meta( + erl_syntax:try_expr( + Body1, + Clauses1, + Handlers1, + After1 + ), + Pos + ); + {clause, Pos, {call, CPos, Name, Args}, Guard, Body} -> + %% free standing named clause - make a magic tuple to + %% hold both the name and the clause with the args + AAnno = dummy_anno(), + Clause = {clause, Pos, {args, CPos, Args}, Guard, Body}, + erlfmt_to_st_1({tuple, CPos, [{atom, AAnno, '*named_clause*'}, Name, Clause]}); + {clause, _, _, _, _} = Clause -> + %% clauses of case/if/receive/try + erlfmt_clause_to_st(Clause); + %% Lists are represented as a `list` node instead of a chain of `cons` + %% and `nil` nodes, similar to the `tuple` node. The last elemenent of + %% the list can be a `cons` node representing explicit consing syntax. + {list, Pos, Elements} -> + %% a "cons" node here means 'H | T' in isolation + %% and can only exist at the end of a list body + {Es, Tail} = + case lists:reverse(Elements) of + [{cons, _CPos, H, T} | Rest] -> + {lists:reverse([H | Rest]), erlfmt_to_st(T)}; + _ -> + {Elements, none} + end, + Es1 = [erlfmt_to_st(E) || E <- Es], + update_tree_with_meta(erl_syntax:list(Es1, Tail), Pos); + %% The record name is always represented as node instead of a raw atom + {record, Pos, Name, Fields} -> + % a new record instance + Fields1 = [erlfmt_to_st(F) || F <- Fields], + update_tree_with_meta( + erl_syntax:record_expr( + erlfmt_to_st(Name), + Fields1 + ), + Pos + ); + {record, Pos, Expr, Name, Fields} -> + % updating a record + Fields1 = [erlfmt_to_st(F) || F <- Fields], + update_tree_with_meta( + erl_syntax:record_expr( + erlfmt_to_st(Expr), + erlfmt_to_st(Name), + Fields1 + ), + Pos + ); + {record_field, Pos, Name} -> + %% a record field without value, just the field name + update_tree_with_meta(erl_syntax:record_field(erlfmt_to_st(Name)), Pos); + {record_field, Pos, Name, Value} -> + %% a record field "name = val" + update_tree_with_meta( + erl_syntax:record_field( + erlfmt_to_st(Name), + erlfmt_to_st(Value) + ), + Pos + ); + {record_field, Pos, Expr, Record, Field} -> + %% a record field access expression "expr#record.field" + update_tree_with_meta( + erl_syntax:record_access( + erlfmt_to_st(Expr), + erlfmt_to_st(Record), + erlfmt_to_st(Field) + ), + Pos + ); + {record_index, Pos, Record, Field} -> + %% a record field index "#record.field" + update_tree_with_meta( + erl_syntax:record_index_expr( + erlfmt_to_st(Record), + erlfmt_to_st(Field) + ), + Pos + ); + %% The `fun` node has a different AST representation: + %% `{'fun', Anno, Value}`, where `Value` is one of: + %% * `{function, Anno, Name, Arity}`, where `Name` and `Arity` are an + %% `atom` and `integer` node respectively or `var` or `macro_call` + %% nodes. + %% * `{function, Anno, Module, Name, Arity}`, where `Module`, `Name`, + %% and `Arity` are `atom`, `atom`, and `integer` nodes respectively + %% or a `var` or `macro_call` node. + %% * `{clauses, Anno, Clauses}`, where `Clauses` is a list of `clause` + %% nodes. Additionally it is less strict - the clauses aren't + %% checked for the same name or arity. + %% * `type` for the anonymous function type `fun()`. + %% * `{type, Anno, Args, Res}` for the anonymous function type + %% `fun((...Args) -> Res)` where `Args` is a `args` node. + %% * The `named_fun` node is not used - instead, clauses have a call + %% head, just as for plain functions. + {'fun', Pos, {clauses, _CPos, Clauses}} -> + %% TODO: can we preserve CPos in any useful way? + [{clause, _, Head, _, _} | _] = Clauses, + Clauses1 = [erlfmt_clause_to_st(C) || C <- Clauses], + case Head of + {call, _, Name, _} -> + %% if the head has function call shape, it's a named fun + update_tree_with_meta( + erl_syntax:named_fun_expr( + erlfmt_to_st(Name), + Clauses1 + ), + Pos + ); + _ -> + update_tree_with_meta(erl_syntax:fun_expr(Clauses1), Pos) + end; + {'fun', Pos, {function, FPos, Name, Arity}} -> + FName = update_tree_with_meta( + erl_syntax:arity_qualifier( + erlfmt_to_st(Name), + erlfmt_to_st(Arity) + ), + FPos + ), + update_tree_with_meta(erl_syntax:implicit_fun(FName), Pos); + {'fun', Pos, {function, FPos, Module, Name, Arity}} -> + %% note that the inner arity qualifier gets no annotation + FName = update_tree_with_meta( + erl_syntax:module_qualifier( + erlfmt_to_st(Module), + erl_syntax:arity_qualifier( + erlfmt_to_st(Name), + erlfmt_to_st(Arity) + ) + ), + FPos + ), + update_tree_with_meta(erl_syntax:implicit_fun(FName), Pos); + {'fun', Pos, type} -> + update_tree_with_meta(erl_syntax:fun_type(), Pos); + {'fun', Pos, {type, _, {args, _, Args}, Res}} -> + update_tree_with_meta( + erl_syntax_function_type( + [erlfmt_to_st(A) || A <- Args], + erlfmt_to_st(Res)), + Pos); + {'bin', Pos, Elements} when Context =:= type -> + %% Note: we loose a lot of Annotation info here + %% Note2: erl_parse assigns the line number (with no column) to the dummy zeros + {M, N} = + case Elements of + [{bin_element, _, {var, _, '_'}, {bin_size, _, {var, _, '_'}, NNode}, default}] -> + {{integer, dummy_anno(), 0}, NNode}; + [{bin_element, _, {var, _, '_'}, MNode, default}] -> + {MNode, {integer, dummy_anno(), 0}}; + [{bin_element, _, {var, _, '_'}, MNode, default}, + {bin_element, _, {var, _, '_'}, {bin_size, _, {var, _, '_'}, NNode}, default}] -> + {MNode, NNode}; + [] -> + {{integer, dummy_anno(), 0}, {integer, dummy_anno(), 0}}; + _ -> + %% No idea what this is - what ST should we create? + %% maybe just a binary(), or an empty text node + {{integer, dummy_anno(), 0}, {integer, dummy_anno(), 1}} + end, + update_tree_with_meta( + erl_syntax:bitstring_type( + erlfmt_to_st(M), + erlfmt_to_st(N)), + Pos); + %% Bit type definitions inside binaries are represented as full nodes + %% instead of raw atoms and integers. The unit notation `unit:Int` is + %% represented with a `{remote, Anno, {atom, Anno, unit}, Int}` node. + {bin_element, Pos, Expr, Size, Types} when Types =/= default -> + Types1 = lists:map( + fun + ({remote, QPos, {atom, _, _} = A, {integer, _, _} = I}) -> + update_tree_with_meta( + erl_syntax:size_qualifier( + erlfmt_to_st(A), + erlfmt_to_st(I) + ), + QPos + ); + (T) -> + erlfmt_to_st(T) + end, + Types + ), + Size1 = + case Size of + default -> none; + _ -> erlfmt_to_st(Size) + end, + update_tree_with_meta( + erl_syntax:binary_field( + erlfmt_to_st(Expr), + Size1, + Types1 + ), + Pos + ); + %% --------------------------------------------------------------------- + %% The remaining cases have been added by erlfmt and need special handling + %% (many are represented as magically-tagged tuples for now) + + %% A new operator node `{op, Anno, 'when', Expr, Guard}` is + %% introduced, which can occur as a body of a macro. It represents + %% "free-standing" `Expr when Guard` expressions as used, for + %% example, in the `assertMatch` macro. + {op, Pos, 'when', Expr, Guard} -> + AAnno = dummy_anno(), + erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*when*'}, Expr, Guard]}); + %% A new node `{exprs, Anno, Exprs}` represents a + %% "free-standing" comma separated sequence of expressions + {exprs, Pos, Exprs} -> + AAnno = dummy_anno(), + erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*exprs*'} | Exprs]}); + %% A new node `{body, Anno, Exprs}` represents a comma separated + %% sequence of expressions as in 'try ... of/catch' + {body, Pos, Exprs} -> + AAnno = dummy_anno(), + erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*body*'} | Exprs]}); + %% The erlfmt parser also accepts general guards (comma and + %% semicolon separated sequences of guard expressions) as the body + %% of a macro + {guard_or, _Pos, _Exprs} -> + erlfmt_guard_to_st(Node); + {guard_and, _Pos, _Exprs} -> + erlfmt_guard_to_st(Node); + %% Record name fragments "#name" may also occur as the body of a macro + {record_name, Pos, Name} -> + AAnno = dummy_anno(), + erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*record_name*'}, Name]}); + %% A new node `{concat, Anno, Concatables}`, where `Concatables` is a + %% list of `string`, `var`, and `macro_call` nodes. This is used to + %% represent implicit string concatenation, for example `"foo" "bar"`. + {concat, Pos, Subtrees} -> + AAnno = dummy_anno(), + erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*concat*'} | Subtrees]}); + %% A new node `{macro_string, Anno, Name}` is introduced, where `Name` + %% is either an `atom` or a `var` node. It represents `??Name`. + {macro_string, Pos, Name} -> + AAnno = dummy_anno(), + erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*stringify*'}, Name]}); + %% erlfmt preserves '...' tokens as nodes (which erl_parse doesn't) + {'...', Pos} -> + AAnno = dummy_anno(), + erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*...*'}]}); + %% sometimes erlfmt leaves comments as separate nodes + %% instead of attaching them to another node + {comment, Pos, Lines} -> + update_tree_with_meta(erl_syntax:comment(Lines), Pos); + %% erlfmt has a separate entry for shebang nodes; we use raw strings + {shebang, Pos, Text} -> + erlfmt_to_st({raw_string, Pos, Text}); + %% args nodes may (in macros) occur free floating + {args, Pos, Args} -> + AAnno = dummy_anno(), + erlfmt_to_st_1({tuple, Pos, [{atom, AAnno, '*args*'} | Args]}); + %% TODO: + %% New `{spec_clause, Anno, Head, Body, Guards}` node for clauses + %% inside `spec` and `callback` attributes, similar to the `clause` + %% node above. It reflects the fact that in specs guards come after + %% body. The `Head` element is always an `args` node. + + _ -> + %% all remaining cases can be handled by the default erl_syntax + %% subtree traversal + erlfmt_to_st_1(Node) + end. + +%% erl_parse format is compatible with erl_syntax +%% But since OTP 24 erl_syntax expects a proper erl_anno:anno() in pos. +%% So first replace the Meta from Node with proper erl_syntax pos+annotation to +%% make dialyzer happy. +-spec erlfmt_to_st_1(erlfmt() | syntax_tools()) -> syntax_tools(). +erlfmt_to_st_1(Node) when is_map(element(2, Node))-> + Node2 = convert_meta_to_anno(Node), + erlfmt_to_st_2(Node2); +erlfmt_to_st_1(Node) -> + erlfmt_to_st_2(Node). + +-spec erlfmt_to_st_2(syntax_tools()) -> syntax_tools(). +erlfmt_to_st_2(Node) -> + case erl_syntax:subtrees(Node) of + [] -> + % leaf node + Node; + Groups0 -> + %% recurse and replace the subtrees + Groups1 = erlfmt_subtrees_to_st(Groups0), + erl_syntax:update_tree(Node, Groups1) + end. + +-spec erlfmt_subtrees_to_st([[any()]]) -> [[any()]]. +erlfmt_subtrees_to_st(Groups) -> + [ + [ + erlfmt_to_st(Subtree) + || Subtree <- Group + ] + || Group <- Groups + ]. + +-spec get_function_name(maybe_improper_list()) -> any(). +get_function_name([{clause, _, {call, _, Name, _}, _, _} | _]) -> + %% take the name node of the first clause with a call shape + %% TODO: this loses info if not all clauses have the same name + Name; +get_function_name([_ | Cs]) -> + get_function_name(Cs); +get_function_name([]) -> + none. + +%% The `clause` node has a different AST representation: +%% `{clause, Anno, Head, Guards, Body}`, where the `Guards` element is either +%% an atom `empty` or a `guard_or` node, and `Head` element is one of: +%% * regular `call` node for functions and named funs; +%% * atom `empty` for `if` expressions; +%% * `{args, Anno, Args}` node for an list of expressions wrapped in parentheses; +%% * `{catch, Anno, Args}` node for clauses in `catch` clauses, where +%% 2 to 3 arguments represent the various `:` separated syntaxes; +%% * other expression for `case`, `receive`, "of" part of `try` expression +%% and simple `catch` clauses without `:`. + +%% TODO: can we preserve CPos/APos annotations here somehow? +-spec erlfmt_clause_to_st(_) -> any(). +erlfmt_clause_to_st({clause, Pos, empty, Guard, Body}) -> + erlfmt_clause_to_st(Pos, [], Guard, Body); +erlfmt_clause_to_st({clause, Pos, {call, _CPos, _, Args}, Guard, Body}) -> + Patterns = [erlfmt_to_st(A) || A <- Args], + erlfmt_clause_to_st(Pos, Patterns, Guard, Body); +erlfmt_clause_to_st({clause, Pos, {args, _APos, Args}, Guard, Body}) -> + Patterns = [erlfmt_to_st(A) || A <- Args], + erlfmt_clause_to_st(Pos, Patterns, Guard, Body); +erlfmt_clause_to_st({clause, Pos, {'catch', APos, Args}, Guard, Body}) -> + Pattern = + case [erlfmt_to_st(A) || A <- Args] of + [Class, Term] -> + update_tree_with_meta(erl_syntax:class_qualifier(Class, Term), APos); + [Class, Term, Trace] -> + update_tree_with_meta(erl_syntax:class_qualifier(Class, Term, Trace), APos) + end, + erlfmt_clause_to_st(Pos, [Pattern], Guard, Body); +erlfmt_clause_to_st({clause, Pos, Expr, Guard, Body}) -> + erlfmt_clause_to_st(Pos, [erlfmt_to_st(Expr)], Guard, Body); +erlfmt_clause_to_st(Other) -> + %% might be a macro call + erlfmt_to_st(Other). + +-spec erlfmt_clause_to_st(_,[any()],_,[any()]) -> any(). +erlfmt_clause_to_st(Pos, Patterns, Guard, Body) -> + Groups = [ + Patterns, + [erlfmt_guard_to_st(Guard)], + [erlfmt_to_st(B) || B <- Body] + ], + update_tree_with_meta(erl_syntax:make_tree(clause, Groups), Pos). + +%% New `{guard_or, Anno, GuardAndList}` and `{guard_and, Anno, Exprs}` nodes +%% are introduced to support annotating guard sequences, instead of a plain +%% nested list of lists structure. + +-spec erlfmt_guard_to_st(_) -> any(). +erlfmt_guard_to_st(empty) -> + none; +erlfmt_guard_to_st({guard_or, Pos, List}) -> + update_tree_with_meta( + erl_syntax:disjunction([ + erlfmt_guard_to_st(E) + || E <- List + ]), + Pos + ); +erlfmt_guard_to_st({guard_and, Pos, List}) -> + update_tree_with_meta( + erl_syntax:conjunction([ + erlfmt_guard_to_st(E) + || E <- List + ]), + Pos + ); +erlfmt_guard_to_st(Other) -> + erlfmt_to_st(Other). + +-spec fold_arity_qualifiers(_) -> any(). +fold_arity_qualifiers(Tree) -> + erl_syntax_lib:map(fun fold_arity_qualifier/1, Tree). + +-spec fold_arity_qualifier(_) -> any(). +fold_arity_qualifier(Node) -> + case erl_syntax:type(Node) of + infix_expr -> + Op = erl_syntax:infix_expr_operator(Node), + case erl_syntax:type(Op) of + operator -> + case erl_syntax:atom_value(Op) of + '/' -> + N = erl_syntax:infix_expr_left(Node), + A = erl_syntax:infix_expr_right(Node), + case + erl_syntax:type(N) =:= atom andalso + erl_syntax:type(A) =:= integer + of + true -> + Q = erl_syntax:arity_qualifier(N, A), + erl_syntax:copy_attrs(Op, Q); + false -> + Node + end; + _ -> + Node + end; + _ -> + Node + end; + _ -> + Node + end. + +-spec dummy_anno() -> erl_anno:anno(). +dummy_anno() -> + erl_anno:set_generated(true, erl_anno:new({0, 1})). + +%% erlfmt ast utilities + +-spec get_anno(tuple()) -> term(). +get_anno(Node) -> + element(2, Node). + +-spec set_anno(tuple(), term()) -> tuple(). +set_anno(Node, Loc) -> + setelement(2, Node, Loc). + +%% @doc Silence warning about breaking the contract +%% erl_syntax:function_type/2 has wrong spec before OTP 24 +-spec erl_syntax_function_type('any_arity' | [syntax_tools()], syntax_tools()) -> syntax_tools(). +erl_syntax_function_type(Arguments, Return) -> + apply(erl_syntax, function_type, [Arguments, Return]). + +%% Convert erlfmt_scan:anno to erl_syntax pos+annotation +%% +%% Note: nothing from meta is stored in annotation +%% as erlang_ls only needs start and end locations. +-spec update_tree_with_meta(syntax_tools(), erlfmt_scan:anno()) + -> syntax_tools(). +update_tree_with_meta(Tree, Meta) -> + Anno = meta_to_anno(Meta), + Tree2 = erl_syntax:set_pos(Tree, Anno), + %% erl_syntax:set_ann(Tree2, [{meta, Meta}]). + Tree2. + +-spec convert_meta_to_anno(erlfmt()) -> syntax_tools(). +convert_meta_to_anno(Node) -> + Meta = get_anno(Node), + Node2 = set_anno(Node, meta_to_anno(Meta)), + %% erl_syntax:set_ann(Node2, [{meta, Meta}]). + Node2. + +-spec meta_to_anno(erlfmt_scan:anno()) -> erl_anno:anno(). +meta_to_anno(Meta) -> + %% Recommenting can modify the start and end locations of certain trees + %% see erlfmt_recomment:put_(pre|post)_comments/1 + From = + case maps:is_key(pre_comments, Meta) of + true -> + maps:get(inner_location, Meta); + false -> + maps:get(location, Meta) + end, + To = + case maps:is_key(post_comments, Meta) of + true -> + maps:get(inner_end_location, Meta); + false -> + maps:get(end_location, Meta) + end, + erl_anno:from_term([{location, From}, {end_location, To}]). diff --git a/apps/els_lsp/src/els_lsp.app.src b/apps/els_lsp/src/els_lsp.app.src index 8198708f7..8738caa62 100644 --- a/apps/els_lsp/src/els_lsp.app.src +++ b/apps/els_lsp/src/els_lsp.app.src @@ -13,6 +13,7 @@ ephemeral, uuid, getopt, + erlfmt, els_core ]}, {env, []}, diff --git a/apps/els_lsp/src/els_parser.erl b/apps/els_lsp/src/els_parser.erl index f68a4d4f5..5fbfb4819 100644 --- a/apps/els_lsp/src/els_parser.erl +++ b/apps/els_lsp/src/els_parser.erl @@ -8,166 +8,71 @@ %%============================================================================== -export([ parse/1 , parse_file/1 + , parse_text/1 ]). %%============================================================================== %% Includes %%============================================================================== -include("els_lsp.hrl"). +-include_lib("kernel/include/logger.hrl"). + +-type deep_list(T) :: [T | deep_list(T)]. %%============================================================================== %% API %%============================================================================== -spec parse(binary()) -> {ok, [poi()]}. parse(Text) -> - IoDevice = els_io_string:new(Text), - parse_file(IoDevice). + String = els_utils:to_list(Text), + case erlfmt:read_nodes_string("nofile", String) of + {ok, Forms, _ErrorInfo} -> + {ok, lists:flatten(parse_forms(Forms))}; + {error, _ErrorInfo} -> + {ok, []} + end. + +-spec parse_file(file:name_all()) -> {ok, [tree()]} | {error, term()}. +parse_file(FileName) -> + forms_to_ast(erlfmt:read_nodes(FileName)). --spec parse_file(file:io_device()) -> {ok, [poi()]}. -parse_file(IoDevice) -> - {ok, NestedPOIs} = els_dodger:parse(IoDevice, {1, 1}, fun parse_form/3, []), - ok = file:close(IoDevice), - {ok, lists:flatten(NestedPOIs)}. +-spec parse_text(binary()) -> {ok, [tree()]} | {error, term()}. +parse_text(Text) -> + String = els_utils:to_list(Text), + forms_to_ast(erlfmt:read_nodes_string("nofile", String)). + +-spec forms_to_ast(tuple()) -> {ok, [tree()]} | {error, term()}. +forms_to_ast({ok, Forms, _ErrorInfo}) -> + TreeList = + [els_erlfmt_ast:erlfmt_to_st(Form) || Form <- Forms], + {ok, TreeList}; +forms_to_ast({error, _ErrorInfo} = Error) -> + Error. %%============================================================================== %% Internal Functions %%============================================================================== -%% Adapted from els_dodger --spec parse_form(file:io_device(), any(), [any()]) -> - {'ok', erl_syntax:forms() - | none, integer()} - | {'eof', integer()} - | {'error', any(), integer()}. -parse_form(IoDevice, Location, Options) -> - parse_form(IoDevice, Location, fun els_dodger:normal_parser/2, Options). - -%% Adapted from els_dodger --spec parse_form(file:io_device(), any(), function(), [any()]) -> - {'ok', erl_syntax:forms() | none, integer()} - | {'eof', integer()} - | {'error', any(), integer()}. -parse_form(IoDevice, StartLocation, Parser, _Options) -> - case io:scan_erl_form(IoDevice, "", StartLocation) of - {ok, Tokens, EndLocation} -> - try {ok, Parser(Tokens, undefined)} of - {ok, Tree} -> - POIs = [ find_attribute_pois(Tree, Tokens) - , points_of_interest(Tree, EndLocation) - ], - {ok, POIs, EndLocation} - catch - _:_ -> - {ok, find_attribute_tokens(Tokens), EndLocation} - end; - {error, _IoErr, _EndLocation} = Err -> Err; - {error, _Reason} -> {eof, StartLocation}; - {eof, _EndLocation} = Eof -> Eof - end. - -%% @doc Find POIs in attributes additionally using tokens to add location info -%% missing from the syntax tree. Other attributes which don't need tokens are -%% processed in `attribute/1'. --spec find_attribute_pois(erl_syntax:syntaxTree(), [erl_scan:token()]) -> - [poi()]. -find_attribute_pois(Tree, Tokens) -> - case erl_syntax:type(Tree) of - attribute -> - try analyze_attribute(Tree) of - {export, Exports} -> - %% The first atom is the attribute name, so we skip it. - [_|Atoms] = [T || {atom, _, _} = T <- Tokens], - ExportEntries = - [ poi(Pos, export_entry, {F, A}) - || {{F, A}, {atom, Pos, _}} <- lists:zip(Exports, Atoms) - ], - [find_attribute_tokens(Tokens), ExportEntries]; - {import, {M, Imports}} -> - %% The first two atoms are the attribute name and the imported - %% module, so we skip them. - [_, _|Atoms] = [T || {atom, _, _} = T <- Tokens], - [ poi(Pos, import_entry, {M, F, A}) - || {{F, A}, {atom, Pos, _}} <- lists:zip(Imports, Atoms)]; - {spec, {spec, {{F, A}, _FTs}}} -> - %% This location has to match that of spec in - %% find_attribute_tokens/1 - From = erl_scan:location(hd(Tokens)), - To = erl_scan:location(lists:last(Tokens)), - [poi({From, To}, spec, {F, A})]; - {export_type, {export_type, Exports}} -> - [_ | Atoms] = [T || {atom, _, _} = T <- Tokens], - ExportTypeEntries = - [ poi(Pos, export_type_entry, {F, A}) - || {{F, A}, {atom, Pos, _}} <- lists:zip(Exports, Atoms) - ], - [find_attribute_tokens(Tokens), ExportTypeEntries]; - {compile, {compile, CompileOpts}} -> - find_compile_options_pois(CompileOpts, Tokens); - _ -> [] - catch - throw:syntax_error -> - [] - end; - _ -> - [] - end. - -%% @doc Analyze an attribute node with special handling for type attributes. -%% -%% `erl_syntax_lib:analyze_attribute` can't handle macros in wild attribute -%% arguments. It also handles `callback', `spec', `type' and `opaque' as wild -%% attributes. Therefore `els_dodger' has to handle these forms specially and -%% here we have to adopt to the different output of `els_dodger'. -%% -%% @see els_dodger:subtrees/1 --spec analyze_attribute(tree()) -> {atom(), term()} | preprocessor. -analyze_attribute(Tree) -> - case attribute_name_atom(Tree) of - AttrName when AttrName =:= callback; - AttrName =:= spec -> - [ArgTuple] = erl_syntax:attribute_arguments(Tree), - [FATree | _] = erl_syntax:tuple_elements(ArgTuple), - Definition = [], %% ignore definition - %% concrete will throw an error if `FATRee' contains any macro - try erl_syntax:concrete(FATree) of - FA -> - {AttrName, {AttrName, {FA, Definition}}} - catch _:_ -> - throw(syntax_error) - end; - AttrName when AttrName =:= opaque; - AttrName =:= type -> - [ArgTuple] = erl_syntax:attribute_arguments(Tree), - [TypeTree, _, ArgsListTree] = erl_syntax:tuple_elements(ArgTuple), - Definition = [], %% ignore definition - %% concrete will throw an error if `TypeTree' is a macro - try erl_syntax:concrete(TypeTree) of - TypeName -> - {AttrName, {AttrName, {TypeName, - Definition, - erl_syntax:list_elements(ArgsListTree)}}} - catch _:_ -> - throw(syntax_error) - end; - _ -> - erl_syntax_lib:analyze_attribute(Tree) - end. - --spec find_compile_options_pois([any()] | tuple(), [erl_scan:token()]) -> - [poi()]. -find_compile_options_pois(CompileOpts, Tokens) when is_tuple(CompileOpts) -> - find_compile_options_pois([CompileOpts], Tokens); -find_compile_options_pois(CompileOpts, Tokens) when is_list(CompileOpts) -> - Fun = fun({parse_transform, PT}, Acc) -> - POIs = [poi(erl_syntax:get_pos(T), parse_transform, Name) || - {atom, _, Name} = T <- Tokens, Name =:= PT], - POIs ++ Acc; - (_, Acc) -> - Acc - end, - lists:foldl(Fun, [], CompileOpts); -find_compile_options_pois(_CompileOpts, _Tokens) -> - []. +-spec parse_forms([erlfmt_parse:abstract_node()]) -> deep_list(poi()). +parse_forms(Forms) -> + [try + parse_form(Form) + catch Type:Reason:St -> + ?LOG_WARNING("Please report error parsing form ~p:~p:~p~n~p~n", + [Type, Reason, St, Form]), + [] + end + || Form <- Forms]. + +-spec parse_form(erlfmt_parse:abstract_node()) -> deep_list(poi()). +parse_form({raw_string, Anno, Text}) -> + Start = erlfmt_scan:get_anno(location, Anno), + {ok, RangeTokens, _EndLocation} = erl_scan:string(Text, Start, [text]), + find_attribute_tokens(RangeTokens); +parse_form(Form) -> + Tree = els_erlfmt_ast:erlfmt_to_st(Form), + POIs = points_of_interest(Tree), + POIs. %% @doc Resolve POI for specific sections %% @@ -181,28 +86,39 @@ find_attribute_tokens([ {'-', Anno}, {atom, _, Name} | [_|_] = Rest]) when Name =:= export; Name =:= export_type -> From = erl_anno:location(Anno), - To = erl_scan:location(lists:last(Rest)), + To = token_end_location(lists:last(Rest)), [poi({From, To}, Name, From)]; find_attribute_tokens([ {'-', Anno}, {atom, _, spec} | [_|_] = Rest]) -> From = erl_anno:location(Anno), - To = erl_scan:location(lists:last(Rest)), + To = token_end_location(lists:last(Rest)), [poi({From, To}, spec, undefined)]; find_attribute_tokens(_) -> []. --spec points_of_interest(tree(), erl_anno:location()) -> [poi()]. -points_of_interest(Tree, EndLocation) -> - FoldFun = fun(T, Acc) -> [do_points_of_interest(T, EndLocation) | Acc] end, +%% Inspired by erlfmt_scan:dot_anno +-spec token_end_location(erl_scan:token()) -> erl_anno:location(). +token_end_location({dot, Anno}) -> + %% Special handling for dot tokens, which by definition contain a dot char + %% followed by a whitespace char. We don't want to count the whitespace (which + %% is usually a newline) as part of the form. + {Line, Col} = erl_anno:location(Anno), + {Line, Col + 1}; +token_end_location(Token) -> + erl_scan:end_location(Token). + +-spec points_of_interest(tree()) -> [[poi()]]. +points_of_interest(Tree) -> + FoldFun = fun(T, Acc) -> [do_points_of_interest(T) | Acc] end, fold(FoldFun, [], Tree). %% @doc Return the list of points of interest for a given `Tree'. --spec do_points_of_interest(tree(), erl_anno:location()) -> [poi()]. -do_points_of_interest(Tree, EndLocation) -> +-spec do_points_of_interest(tree()) -> [poi()]. +do_points_of_interest(Tree) -> try case erl_syntax:type(Tree) of application -> application(Tree); attribute -> attribute(Tree); - function -> function(Tree, EndLocation); + function -> function(Tree); implicit_fun -> implicit_fun(Tree); macro -> macro(Tree); record_access -> record_access(Tree); @@ -222,37 +138,28 @@ do_points_of_interest(Tree, EndLocation) -> application(Tree) -> case application_mfa(Tree) of undefined -> []; - {{F, A}, Pos} -> + {F, A} -> + Pos = erl_syntax:get_pos(erl_syntax:application_operator(Tree)), case erl_internal:bif(F, A) of %% Call to a function from the `erlang` module true -> [poi(Pos, application, {erlang, F, A}, #{imported => true})]; %% Local call false -> [poi(Pos, application, {F, A})] end; - {MFA, Pos} -> + MFA -> + Pos = erl_syntax:get_pos(erl_syntax:application_operator(Tree)), [poi(Pos, application, MFA)] end. -spec application_mfa(tree()) -> - {{module(), atom(), arity()}, pos()} | {{atom(), arity()}, pos()} | undefined. + {module(), atom(), arity()} | {atom(), arity()} | undefined. application_mfa(Tree) -> case erl_syntax_lib:analyze_application(Tree) of %% Remote call {M, {F, A}} -> - %% For remote calls we use the column position of the - %% module part. In OTP-24+ this is the same as doing - %% erl_syntax:get_pos(Tree), but in OTP-23 and earlier - %% the position of the tree is the ':' of the application. - Operator = erl_syntax:application_operator(Tree), - Pos = case erl_syntax:type(Operator) of - module_qualifier -> - erl_syntax:get_pos( - erl_syntax:module_qualifier_argument(Operator)); - _ -> - erl_syntax:get_pos(Tree) - end, - {{M, F, A}, Pos}; - {F, A} -> {{F, A}, erl_syntax:get_pos(Tree)}; + {M, F, A}; + {F, A} -> + {F, A}; A when is_integer(A) -> %% If the function is not explicitly named (e.g. a variable is %% used as the module qualifier or the function name), only the @@ -279,7 +186,7 @@ application_with_variable(Operator, A) -> ModuleName = node_name(Module), FunctionName = node_name(Function), case {ModuleName, FunctionName} of - {'MODULE', F} -> {{F, A}, erl_syntax:get_pos(Module)}; + {'MODULE', F} -> {F, A}; _ -> undefined end; _ -> undefined @@ -288,48 +195,195 @@ application_with_variable(Operator, A) -> -spec attribute(tree()) -> [poi()]. attribute(Tree) -> Pos = erl_syntax:get_pos(Tree), - try analyze_attribute(Tree) of + try {attribute_name_atom(Tree), erl_syntax:attribute_arguments(Tree)} of %% Yes, Erlang allows both British and American spellings for %% keywords. - {behavior, {behavior, Behaviour}} -> - [poi(Pos, behaviour, Behaviour)]; - {behaviour, {behaviour, Behaviour}} -> - [poi(Pos, behaviour, Behaviour)]; - {callback, {callback, {{F, A}, _}}} -> - [poi(Pos, callback, {F, A})]; - {module, {Module, _Args}} -> - [poi(Pos, module, Module)]; - {module, Module} -> - [poi(Pos, module, Module)]; - preprocessor -> - Name = erl_syntax:atom_value(erl_syntax:attribute_name(Tree)), - case {Name, erl_syntax:attribute_arguments(Tree)} of - {define, [Define|Value]} -> - [poi(Pos, define, define_name(Define), Value)]; - {include, [String]} -> - [poi(Pos, include, erl_syntax:string_value(String))]; - {include_lib, [String]} -> - [poi(Pos, include_lib, erl_syntax:string_value(String))]; + {AttrName, [Arg]} when AttrName =:= behaviour; + AttrName =:= behavior -> + case is_atom_node(Arg) of + {true, Behaviour} -> + [poi(Pos, behaviour, Behaviour)]; + false -> + [] + end; + {module, [Module, _Args]} -> + case is_atom_node(Module) of + {true, ModuleName} -> + [poi(erl_syntax:get_pos(Module), module, ModuleName)]; + _ -> + [] + end; + {module, [Module]} -> + case is_atom_node(Module) of + {true, ModuleName} -> + [poi(erl_syntax:get_pos(Module), module, ModuleName)]; + _ -> + [] + end; + {compile, [Arg]} -> + find_compile_options_pois(Arg); + {AttrName, [Arg]} when AttrName =:= export; + AttrName =:= export_type -> + find_export_pois(Tree, AttrName, Arg); + {import, [ModTree, ImportList]} -> + case is_atom_node(ModTree) of + {true, M} -> + Imports = erl_syntax:list_elements(ImportList), + find_import_entry_pois(M, Imports); _ -> [] end; - {record, {Record, Fields}} -> - [poi(Pos, record, Record, Fields) | record_def_fields(Tree, Record)]; - {type, {type, {Type, _, Args}}} -> - {Line, Col} = Pos, - [poi({Line, Col + length("type ")}, type_definition, - {Type, length(Args)}, type_args(Args))]; - {opaque, {opaque, {Type, _, Args}}} -> - {Line, Col} = Pos, - [poi({Line, Col + length("opaque ")}, type_definition, - {Type, length(Args)}, type_args(Args))]; + {define, [Define|Value]} -> + DefinePos = case erl_syntax:type(Define) of + application -> + Operator = erl_syntax:application_operator(Define), + erl_syntax:get_pos(Operator); + _ -> + erl_syntax:get_pos(Define) + end, + ValueRange = #{ from => get_start_location(hd(Value)) + , to => get_end_location(lists:last(Value)) + }, + Data = #{value_range => ValueRange}, + [poi(DefinePos, define, define_name(Define), Data)]; + {include, [String]} -> + [poi(Pos, include, erl_syntax:string_value(String))]; + {include_lib, [String]} -> + [poi(Pos, include_lib, erl_syntax:string_value(String))]; + {record, [Record, Fields]} -> + case is_atom_node(Record) of + {true, RecordName} -> + %% FIXME clean FieldList up -> analyze_record_fields + FieldList = + lists:flatten( + [case erl_syntax:type(F) of + record_field -> + FieldName = erl_syntax:record_field_name(F), + {erl_syntax:atom_value(FieldName), + erl_syntax:record_field_value(F)}; + typed_record_field -> + FF = erl_syntax:typed_record_field_body(F), + FieldName = erl_syntax:record_field_name(FF), + {erl_syntax:atom_value(FieldName), + erl_syntax:record_field_value(FF)} + end + || F <- erl_syntax:tuple_elements(Fields)]), + [poi(erl_syntax:get_pos(Record), record, RecordName, FieldList) + | record_def_fields(Tree, RecordName)]; + _ -> + [] + end; + {AttrName, [ArgTuple]} when AttrName =:= type; + AttrName =:= opaque -> + [Type, _, ArgsListTree] = erl_syntax:tuple_elements(ArgTuple), + TypeArgs = erl_syntax:list_elements(ArgsListTree), + case is_atom_node(Type) of + {true, TypeName} -> + [poi(erl_syntax:get_pos(Type), type_definition, + {TypeName, length(TypeArgs)}, type_args(TypeArgs))]; + _ -> + [] + end; + {callback, [ArgTuple]} -> + [FATree | _] = erl_syntax:tuple_elements(ArgTuple), + case spec_function_name(FATree) of + {F, A} -> + [FTree, _] = erl_syntax:tuple_elements(FATree), + Anno = erl_syntax:get_pos(FTree), + %% FIXME this is weird: + %% starts at '-', ends at the end of callback function name + Start = get_start_location(Tree), + CallbackAnno = erl_anno:set_location(Start, Anno), + [poi(CallbackAnno, callback, {F, A})]; + undefined -> + [] + end; + {spec, [ArgTuple]} -> + [FATree | _] = erl_syntax:tuple_elements(ArgTuple), + case spec_function_name(FATree) of + {F, A} -> + [poi(Pos, spec, {F, A})]; + undefined -> + [poi(Pos, spec, undefined)] + end; _ -> [] catch throw:syntax_error -> [] end. --spec type_args([any()]) -> [{integer(), string()}]. +-spec find_compile_options_pois(tree()) -> [poi()]. +find_compile_options_pois(Arg) -> + case erl_syntax:type(Arg) of + list -> + L = erl_syntax:list_elements(Arg), + lists:flatmap(fun find_compile_options_pois/1, L); + tuple -> + case erl_syntax:tuple_elements(Arg) of + [K, V] -> + case {is_atom_node(K), is_atom_node(V)} of + {{true, parse_transform}, {true, PT}} -> + [poi(erl_syntax:get_pos(V), parse_transform, PT)]; + _ -> + [] + end; + _ -> + [] + end; + atom -> + %% currently there is no atom compile option that we are interested in + []; + _ -> + [] + end. + +-spec find_export_pois(tree(), export | export_type, tree()) -> [poi()]. +find_export_pois(Tree, AttrName, Arg) -> + Exports = erl_syntax:list_elements(Arg), + EntryPoiKind = case AttrName of + export -> export_entry; + export_type -> export_type_entry + end, + ExportEntries = find_export_entry_pois(EntryPoiKind, Exports), + [ poi(erl_syntax:get_pos(Tree), AttrName, get_start_location(Tree)) + | ExportEntries ]. + +-spec find_export_entry_pois(export_entry | export_type_entry, [tree()]) + -> [poi()]. +find_export_entry_pois(EntryPoiKind, Exports) -> + lists:flatten( + [ try erl_syntax_lib:analyze_function_name(FATree) of + {F, A} -> + poi(erl_syntax:get_pos(FATree), EntryPoiKind, {F, A}) + catch throw:syntax_error -> + [] + end + || FATree <- Exports + ]). + +-spec find_import_entry_pois(atom(), [tree()]) -> [poi()]. +find_import_entry_pois(M, Imports) -> + lists:flatten( + [ try erl_syntax_lib:analyze_function_name(FATree) of + {F, A} -> + poi(erl_syntax:get_pos(FATree), import_entry, {M, F, A}) + catch throw:syntax_error -> + [] + end + || FATree <- Imports + ]). + +-spec spec_function_name(tree()) -> {atom(), arity()} | undefined. +spec_function_name(FATree) -> + %% concrete will throw an error if `FATree' contains any macro + try erl_syntax:concrete(FATree) of + {F, A} -> {F, A}; + _ -> undefined + catch _:_ -> + undefined + end. + +-spec type_args([tree()]) -> [{integer(), string()}]. type_args(Args) -> [ case erl_syntax:type(T) of variable -> {N, erl_syntax:variable_literal(T)}; @@ -338,45 +392,69 @@ type_args(Args) -> || {N, T} <- lists:zip(lists:seq(1, length(Args)), Args) ]. --spec function(tree(), erl_anno:location()) -> [poi()]. -function(Tree, {EndLine, _} = _EndLocation) -> - {F, A} = erl_syntax_lib:analyze_function(Tree), +-spec function(tree()) -> [poi()]. +function(Tree) -> + FunName = erl_syntax:function_name(Tree), Clauses = erl_syntax:function_clauses(Tree), + {F, A, Args} = analyze_function(FunName, Clauses), + IndexedClauses = lists:zip(lists:seq(1, length(Clauses)), Clauses), - ClausesPOIs = [ poi( erl_syntax:get_pos(Clause) + %% FIXME function_clause range should be the range of the name atom however + %% that is not present in the clause Tree (it is in the erlfmt_parse node) + ClausesPOIs = [ poi( get_start_location(Clause) , function_clause , {F, A, I} , pretty_print_clause(Clause) ) - || {I, Clause} <- IndexedClauses], - Args = function_args(hd(Clauses), A), - {StartLine, _} = StartLocation = erl_syntax:get_pos(Tree), + || {I, Clause} <- IndexedClauses, + erl_syntax:type(Clause) =:= clause], + {StartLine, _} = StartLocation = get_start_location(Tree), + {EndLine, _} = get_end_location(Tree), %% It only makes sense to fold a function if the function contains %% at least one line apart from its signature. - FoldingRanges = case EndLine - StartLine > 1 of + FoldingRanges = case EndLine > StartLine of true -> Range = #{ from => {StartLine, ?END_OF_LINE} - , to => {EndLine - 1, ?END_OF_LINE} + , to => {EndLine, ?END_OF_LINE} }, [ els_poi:new(Range, folding_range, StartLocation) ]; false -> [] end, - lists:append([ [ poi(StartLocation, function, {F, A}, Args) ] + lists:append([ [ poi(erl_syntax:get_pos(FunName), function, {F, A}, Args) ] , FoldingRanges , ClausesPOIs ]). --spec function_args(tree(), arity()) -> [{integer(), string()}]. -function_args(Clause, Arity) -> +-spec analyze_function(tree(), [tree()]) -> + {atom(), arity(), [{integer(), string()}]}. +analyze_function(FunName, Clauses) -> + F = case is_atom_node(FunName) of + {true, FAtom} -> FAtom; + false -> throw(syntax_error) + end, + + case lists:dropwhile(fun(T) -> erl_syntax:type(T) =/= clause end, Clauses) of + [Clause | _] -> + {Arity, Args} = function_args(Clause), + {F, Arity, Args}; + [] -> + throw(syntax_error) + end. + +-spec function_args(tree()) -> {arity(), [{integer(), string()}]}. +function_args(Clause) -> Patterns = erl_syntax:clause_patterns(Clause), - [ case erl_syntax:type(P) of - %% TODO: Handle literals - variable -> {N, erl_syntax:variable_literal(P)}; - _ -> {N, "Arg" ++ integer_to_list(N)} - end - || {N, P} <- lists:zip(lists:seq(1, Arity), Patterns) - ]. + Arity = length(Patterns), + Args = + [ case erl_syntax:type(P) of + %% TODO: Handle literals + variable -> {N, erl_syntax:variable_literal(P)}; + _ -> {N, "Arg" ++ integer_to_list(N)} + end + || {N, P} <- lists:zip(lists:seq(1, Arity), Patterns) + ], + {Arity, Args}. -spec implicit_fun(tree()) -> [poi()]. implicit_fun(Tree) -> @@ -393,11 +471,8 @@ implicit_fun(Tree) -> -spec macro(tree()) -> [poi()]. macro(Tree) -> - Pos = erl_syntax:get_pos(Tree), - case Pos of - 0 -> []; - _ -> [poi(Pos, macro, node_name(Tree))] - end. + Anno = macro_location(Tree), + [poi(Anno, macro, node_name(Tree))]. -spec record_access(tree()) -> [poi()]. record_access(Tree) -> @@ -414,7 +489,8 @@ record_access(Tree) -> _ -> [] end, - [ poi(erl_syntax:get_pos(Tree), record_expr, Record) + Anno = record_access_location(Tree), + [ poi(Anno, record_expr, Record) | FieldPoi ]; _ -> [] @@ -429,7 +505,8 @@ record_expr(Tree) -> FieldPois = lists:append( [record_field_name(F, Record, record_field) || F <- erl_syntax:record_expr_fields(Tree)]), - [ poi(erl_syntax:get_pos(Tree), record_expr, Record) + Anno = record_expr_location(Tree, RecordNode), + [ poi(Anno, record_expr, Record) | FieldPois ]; _ -> [] @@ -489,7 +566,8 @@ record_type(Tree) -> FieldPois = lists:append( [record_field_name(F, Record, record_field) || F <- erl_syntax:record_type_fields(Tree)]), - [ poi(erl_syntax:get_pos(Tree), record_expr, Record) + Anno = record_expr_location(Tree, RecordNode), + [ poi(Anno, record_expr, Record) | FieldPois ]; _ -> [] @@ -497,16 +575,17 @@ record_type(Tree) -> -spec type_application(tree()) -> [poi()]. type_application(Tree) -> - Pos = erl_syntax:get_pos(Tree), Type = erl_syntax:type(Tree), case erl_syntax_lib:analyze_type_application(Tree) of {Module, {Name, Arity}} -> %% remote type Id = {Module, Name, Arity}, + Pos = erl_syntax:get_pos(erl_syntax:type_application_name(Tree)), [poi(Pos, type_application, Id)]; {Name, Arity} when Type =:= user_type_application -> %% user-defined local type Id = {Name, Arity}, + Pos = erl_syntax:get_pos(erl_syntax:user_type_application_name(Tree)), [poi(Pos, type_application, Id)]; {_Name, _Arity} when Type =:= type_application -> %% No POIs for built-in types @@ -556,11 +635,20 @@ node_name(Tree) -> '_' end. --spec poi(pos() | {pos(), pos()}, poi_kind(), any()) -> poi(). +-spec is_atom_node(tree()) -> {true, atom()} | false. +is_atom_node(Tree) -> + case erl_syntax:type(Tree) of + atom -> + {true, erl_syntax:atom_value(Tree)}; + _ -> + false + end. + +-spec poi(pos() | {pos(), pos()} | erl_anno:anno(), poi_kind(), any()) -> poi(). poi(Pos, Kind, Id) -> poi(Pos, Kind, Id, undefined). --spec poi(pos() | {pos(), pos()}, poi_kind(), any(), any()) -> +-spec poi(pos() | {pos(), pos()} | erl_anno:anno(), poi_kind(), any(), any()) -> poi(). poi(Pos, Kind, Id, Data) -> Range = els_range:range(Pos, Kind, Id, Data), @@ -673,7 +761,11 @@ attribute_subtrees(record, [_RecordName, FieldsTuple]) -> [[FieldsTuple]]; attribute_subtrees(import, [Mod, Imports]) -> [ skip_record_field_atom(Mod) - , [Imports]]; + , skip_function_entries(Imports) ]; +attribute_subtrees(AttrName, [Exports]) + when AttrName =:= export; + AttrName =:= export_type -> + [ skip_function_entries(Exports) ]; attribute_subtrees(define, [_Name | Definition]) -> %% The definition can contain commas, in which case it will look like as if %% the attribute would have more than two arguments. Eg.: `-define(M, a, b).' @@ -682,6 +774,30 @@ attribute_subtrees(AttrName, _) when AttrName =:= include; AttrName =:= include_lib -> []; +attribute_subtrees(AttrName, [ArgTuple]) + when AttrName =:= callback; + AttrName =:= spec -> + case erl_syntax:type(ArgTuple) of + tuple -> + [FATree | Rest] = erl_syntax:tuple_elements(ArgTuple), + [ case spec_function_name(FATree) of + {_, _} -> []; + undefined -> [FATree] + end + , Rest ]; + _ -> + [[ArgTuple]] + end; +attribute_subtrees(AttrName, [ArgTuple]) + when AttrName =:= type; + AttrName =:= opaque -> + case erl_syntax:type(ArgTuple) of + tuple -> + [Type | Rest] = erl_syntax:tuple_elements(ArgTuple), + [skip_record_field_atom(Type), Rest]; + _ -> + [ArgTuple] + end; attribute_subtrees(AttrName, Args) when is_atom(AttrName) -> [Args]; @@ -689,8 +805,25 @@ attribute_subtrees(AttrName, Args) -> %% Attribute name not an atom, probably a macro [[AttrName], Args]. -%% Skip visiting atoms of record field names as they are already represented as -%% `record_field' pois +%% Skip visiting atoms of import/export entries +-spec skip_function_entries(tree()) -> [tree()]. +skip_function_entries(FunList) -> + case erl_syntax:type(FunList) of + list -> + lists:filter( + fun(FATree) -> + try erl_syntax_lib:analyze_function_name(FATree) of + {_, _} -> false + catch + throw:syntax_error -> true + end + end, erl_syntax:list_elements(FunList)); + _ -> + [FunList] + end. + +%% Skip visiting atoms of record and record field names as they are already +%% represented as `record_expr' or `record_field' pois -spec skip_record_field_atom(tree()) -> [tree()]. skip_record_field_atom(NameNode) -> case erl_syntax:type(NameNode) of @@ -703,8 +836,8 @@ skip_record_field_atom(NameNode) -> -spec skip_type_name_atom(tree()) -> [tree()]. skip_type_name_atom(NameNode) -> case erl_syntax:type(NameNode) of - atom -> - []; + atom -> + []; module_qualifier -> skip_record_field_atom(erl_syntax:module_qualifier_body(NameNode)) ++ @@ -729,3 +862,59 @@ pretty_print_clause(Tree) -> , PrettyGuard ]), els_utils:to_binary(PrettyClause). + +-spec record_access_location(tree()) -> erl_anno:anno(). +record_access_location(Tree) -> + %% erlfmt_parser sets start at the start of the argument expression + %% we don't have an exact location of '#' + %% best approximation is the end of the argument + Start = get_end_location(erl_syntax:record_access_argument(Tree)), + Anno = erl_syntax:get_pos(erl_syntax:record_access_type(Tree)), + erl_anno:set_location(Start, Anno). + +-spec record_expr_location(tree(), tree()) -> erl_anno:anno(). +record_expr_location(Tree, RecordName) -> + %% set start location at '#' + %% and end location at the end of record name + Start = record_expr_start_location(Tree), + Anno = erl_syntax:get_pos(RecordName), + erl_anno:set_location(Start, Anno). + +-spec record_expr_start_location(tree()) -> erl_anno:location(). +record_expr_start_location(Tree) -> + %% If this is a new record creation or record type + %% the tree start location is at '#'. + %% However if this is a record update, then + %% we don't have an exact location of '#', + %% best approximation is the end of the argument. + case erl_syntax:type(Tree) of + record_expr -> + case erl_syntax:record_expr_argument(Tree) of + none -> + get_start_location(Tree); + RecordArg -> + get_end_location(RecordArg) + end; + record_type -> + get_start_location(Tree) + end. + +-spec macro_location(tree()) -> erl_anno:anno(). +macro_location(Tree) -> + %% set start location at '?' + %% and end location at the end of macro name + %% (exclude arguments) + Start = get_start_location(Tree), + MacroName = erl_syntax:macro_name(Tree), + Anno = erl_syntax:get_pos(MacroName), + erl_anno:set_location(Start, Anno). + +-spec get_start_location(tree()) -> erl_anno:location(). +get_start_location(Tree) -> + erl_anno:location(erl_syntax:get_pos(Tree)). + +-spec get_end_location(tree()) -> erl_anno:location(). +get_end_location(Tree) -> + %% erl_anno:end_location(erl_syntax:get_pos(Tree)). + Anno = erl_syntax:get_pos(Tree), + proplists:get_value(end_location, erl_anno:to_term(Anno)). diff --git a/apps/els_lsp/src/els_range.erl b/apps/els_lsp/src/els_range.erl index a7c7ccf89..985bdc31c 100644 --- a/apps/els_lsp/src/els_range.erl +++ b/apps/els_lsp/src/els_range.erl @@ -22,135 +22,24 @@ compare(_, _) -> in(#{from := FromA, to := ToA}, #{from := FromB, to := ToB}) -> FromA >= FromB andalso ToA =< ToB. --spec range(pos() | {pos(), pos()}, poi_kind(), any(), any()) -> poi_range(). -range({{Line, Column}, {ToLine, ToColumn}}, Name, _, _Data) - when Name =:= folding_range; - Name =:= spec -> - %% -1 as we include the "-" before spec. - From = {Line, Column}, - %% +1 as we include the . after the spec - To = {ToLine, ToColumn + 1}, - #{ from => From, to => To }; +-spec range(pos() | {pos(), pos()} | erl_anno:anno(), poi_kind(), any(), any()) + -> poi_range(). range({{_Line, _Column} = From, {_ToLine, _ToColumn} = To}, Name, _, _Data) when Name =:= export; - Name =:= export_type -> - #{ from => From, to => To }; -range(Pos, export_entry, {F, A}, _Data) -> - get_entry_range(Pos, F, A); -range(Pos, import_entry, {_M, F, A}, _Data) -> - get_entry_range(Pos, F, A); -range({Line, Column}, export_type_entry, {F, A}, _Data) -> - get_entry_range({Line, Column}, F, A); -range({_Line, _Column} = From, atom, Name, _Data) -> - To = plus(From, atom_to_string(Name)), - #{ from => From, to => To }; -range({Line, Column}, application, {_, F, A}, #{imported := true} = Data) -> - range({Line, Column}, application, {F, A}, Data); -range({Line, Column} = From, application, {M, F, _A}, _Data) -> - %% module:function - CTo = Column + string:length(atom_to_string(M)) + - string:length(atom_to_string(F)) + 1, - To = {Line, CTo}, - #{ from => From, to => To }; -range({Line, Column}, application, {F, _A}, _Data) -> - From = {Line, Column}, - To = plus(From, atom_to_string(F)), - #{ from => From, to => To }; -range({Line, Column}, implicit_fun, {M, F, A}, _Data) -> - From = {Line, Column}, - %% Assumes "fun M:F/A" - To = plus(From, "fun " ++ atom_to_string(M) ++ ":" ++ - atom_to_string(F) ++ "/" ++ integer_to_list(A)), - #{ from => From, to => To }; -range({Line, Column}, implicit_fun, {F, A}, _Data) -> - From = {Line, Column}, - %% Assumes "fun F/A" - To = plus(From, "fun " ++ atom_to_string(F) ++ "/" ++ integer_to_list(A)), - #{ from => From, to => To }; -range({Line, Column}, behaviour, Behaviour, _Data) -> - From = {Line, Column - 1}, - To = plus(From, "-behaviour(" ++ atom_to_string(Behaviour) ++ ")."), - #{ from => From, to => To }; -range({Line, Column}, callback, {F, _A}, _Data) -> - From = {Line, Column - 1}, - To = plus(From, "-callback " ++ atom_to_string(F)), - #{ from => From, to => To }; -range({Line, Column}, function, {F, _A}, _Data) -> - From = {Line, Column}, - To = plus(From, atom_to_string(F)), + Name =:= export_type; + Name =:= spec -> + %% range from unparsable tokens #{ from => From, to => To }; range({Line, Column}, function_clause, {F, _A, _Index}, _Data) -> From = {Line, Column}, To = plus(From, atom_to_string(F)), #{ from => From, to => To }; -range({Line, Column}, define, Define, _Data) -> - From = plus({Line, Column}, "define("), - To = plus(From, atom_to_list(Define)), - #{ from => From, to => To }; -range({Line, Column}, include, Include, _Data) -> - From = {Line, Column - 1}, - To = plus(From, "-include(\"" ++ Include ++ "\")."), - #{ from => From, to => To }; -range({Line, Column}, include_lib, Include, _Data) -> - From = {Line, Column - 1}, - To = plus(From, "-include_lib(\"" ++ Include ++ "\")."), - #{ from => From, to => To }; -range({Line, Column}, macro, Macro, _Data) when is_atom(Macro) -> - From = {Line, Column}, - To = plus(From, "?" ++ atom_to_list(Macro)), - #{ from => From, to => To }; -range({Line, Column}, module, Module, _Data) -> - %% The Column we get is of the 'm' in the -module pragma - From = plus({Line, Column}, "module("), - To = plus(From, atom_to_string(Module)), - #{ from => From, to => To }; -range({Line, Column}, parse_transform, PT, _Data) -> - From = {Line, Column}, - To = plus(From, atom_to_string(PT)), - #{ from => From, to => To }; -range({Line, Column}, record_expr, Record, _Data) -> - %% the range includes the leading '#' - From = {Line, Column}, - #{ from => From, to => plus(From, "#" ++ atom_to_string(Record)) }; -range(Pos, record_field, {_Record, Field}, _Data) -> - From = Pos, - #{ from => From, to => plus(From, atom_to_string(Field)) }; -range(Pos, record_def_field, {_Record, Field}, _Data) -> - From = Pos, - #{ from => From, to => plus(From, atom_to_string(Field)) }; -range({Line, Column}, record, Record, _Data) -> - From = plus({Line, Column}, "record("), - To = plus(From, atom_to_string(Record)), - #{ from => From, to => To }; -range({Line, Column}, type_application, {F, _A}, _Data) -> - From = {Line, Column}, - To = plus(From, atom_to_string(F)), - #{ from => From, to => To }; -range({Line, Column}, type_application, {M, F, _A}, _Data) -> - From = {Line, Column}, - To = plus(From, atom_to_string(M) ++ ":" ++ atom_to_string(F)), - #{ from => From, to => To }; -range({Line, Column}, type_definition, {Name, _}, _Data) -> - From = {Line, Column}, - To = plus(From, atom_to_string(Name)), - #{ from => From, to => To }; -range({Line, Column}, variable, Name, _Data) -> - From = {Line, Column}, - To = plus(From, atom_to_list(Name)), +range(Anno, _Type, _Id, _Data) -> + From = erl_anno:location(Anno), + %% To = erl_anno:end_location(Anno), + To = proplists:get_value(end_location, erl_anno:to_term(Anno)), #{ from => From, to => To }. --spec get_entry_range(pos(), atom(), non_neg_integer()) -> poi_range(). -get_entry_range({Line, Column}, F, A) -> - From = {Line, Column}, - %% length("function/arity") - Length = string:length(atom_to_string(F) ++ "/" ++ integer_to_list(A)), - To = {Line, Column + Length}, - #{ from => From, to => To }. - -%% -spec minus(pos(), string()) -> pos(). -%% minus({Line, Column}, String) -> -%% {Line, Column - string:length(String)}. - -spec plus(pos(), string()) -> pos(). plus({Line, Column}, String) -> {Line, Column + string:length(String)}. diff --git a/apps/els_lsp/test/els_diagnostics_SUITE.erl b/apps/els_lsp/test/els_diagnostics_SUITE.erl index c8924d07e..139f2dff9 100644 --- a/apps/els_lsp/test/els_diagnostics_SUITE.erl +++ b/apps/els_lsp/test/els_diagnostics_SUITE.erl @@ -215,13 +215,14 @@ compiler(Config) -> ?assertEqual(3, length(Errors)), WarningRanges = [ Range || #{range := Range} <- Warnings], ExpectedWarningRanges = - fixcolumns( - [ #{'end' => #{character => 4, line => 6} - , start => #{character => 0, line => 6}}], Config) ++ - [#{'end' => #{character => 35, line => 3}, - start => #{character => 0, line => 3}} - ], - ?assertEqual(ExpectedWarningRanges, WarningRanges), + [#{'end' => #{character => 35, line => 3} + , start => #{character => 0, line => 3}} + ] ++ + fixcolumns( + [ #{'end' => #{character => 4, line => 6} + , start => #{character => 0, line => 6}}], Config), + + ?assertEqual(ExpectedWarningRanges, sort_ranges(WarningRanges)), ErrorRanges = [ Range || #{range := Range} <- Errors], ExpectedErrorRanges = [ #{'end' => #{character => 35, line => 3} @@ -231,7 +232,7 @@ compiler(Config) -> fixcolumns( [ #{'end' => #{character => 44, line => 5} , start => #{character => 30, line => 5}}], Config), - ?assertEqual(ExpectedErrorRanges, ErrorRanges), + ?assertEqual(ExpectedErrorRanges, sort_ranges(ErrorRanges)), ok. -spec compiler_with_behaviour(config()) -> ok. @@ -356,13 +357,13 @@ compiler_with_parse_transform_included(Config) -> ?assertEqual(2, length(Warnings)), WarningRanges = [ Range || #{range := Range} <- Warnings], ExpectedWarningsRanges = - fixcolumns( - [ #{ 'end' => #{character => 9, line => 6} - , start => #{character => 5, line => 6}}], Config) ++ [#{ 'end' => #{character => 32, line => 4} , start => #{character => 0, line => 4}} - ], - ?assertEqual(ExpectedWarningsRanges, WarningRanges), + ] ++ + fixcolumns( + [ #{ 'end' => #{character => 9, line => 6} + , start => #{character => 5, line => 6}}], Config), + ?assertEqual(ExpectedWarningsRanges, sort_ranges(WarningRanges)), ok. -spec compiler_with_parse_transform_broken(config()) -> ok. @@ -377,11 +378,12 @@ compiler_with_parse_transform_broken(Config) -> Errors = [D || #{severity := ?DIAGNOSTIC_ERROR} = D <- Diagnostics], ?assertEqual(2, length(Errors)), ErrorsRanges = [ Range || #{range := Range} <- Errors], - ExpectedErrorsRanges = [#{'end' => #{character => 61, line => 4}, - start => #{character => 27, line => 4}}, - #{'end' => #{character => 0, line => 1}, - start => #{character => 0, line => 0}}], - ?assertEqual(ExpectedErrorsRanges, ErrorsRanges), + ExpectedErrorsRanges = [#{'end' => #{character => 0, line => 1}, + start => #{character => 0, line => 0}}, + #{'end' => #{character => 61, line => 4}, + start => #{character => 27, line => 4}} + ], + ?assertEqual(ExpectedErrorsRanges, sort_ranges(ErrorsRanges)), ok. -spec compiler_with_parse_transform_deps(config()) -> ok. @@ -442,7 +444,7 @@ epp_with_nonexistent_macro(Config) -> start => #{character => 0, line => 4}}, #{'end' => #{character => 0, line => 7}, start => #{character => 0, line => 6}}], - ?assertEqual(ExpectedErrorsRanges, ErrorsRanges), + ?assertEqual(ExpectedErrorsRanges, sort_ranges(ErrorsRanges)), ok. -spec elvis(config()) -> ok. @@ -716,6 +718,14 @@ fixcolumns(Ranges, Config) -> end, Ranges) end. +-spec sort_ranges([range()]) -> [range()]. +sort_ranges(Ranges) -> + lists:sort(fun(#{start := #{line := L1, character := C1}}, + #{start := #{line := L2, character := C2}}) -> + {L1, C1} =< {L2, C2} + end, + Ranges). + mock_rpc() -> meck:new(rpc, [passthrough, no_link, unstick]), {ok, HostName} = inet:gethostname(), diff --git a/apps/els_lsp/test/els_document_highlight_SUITE.erl b/apps/els_lsp/test/els_document_highlight_SUITE.erl index a7ddf8565..e6c835ac0 100644 --- a/apps/els_lsp/test/els_document_highlight_SUITE.erl +++ b/apps/els_lsp/test/els_document_highlight_SUITE.erl @@ -216,7 +216,7 @@ record_field(Config) -> export(Config) -> Uri = ?config(code_navigation_uri, Config), #{result := Locations} = els_client:document_highlight(Uri, 5, 5), - ExpectedLocations = [ #{range => #{from => {5, 1}, to => {5, 92}}} + ExpectedLocations = [ #{range => #{from => {5, 1}, to => {5, 93}}} ], assert_locations(ExpectedLocations, Locations), ok. diff --git a/apps/els_lsp/test/els_hover_SUITE.erl b/apps/els_lsp/test/els_hover_SUITE.erl index 2da5b5d1e..e69168aa6 100644 --- a/apps/els_lsp/test/els_hover_SUITE.erl +++ b/apps/els_lsp/test/els_hover_SUITE.erl @@ -19,6 +19,7 @@ , no_poi/1 , included_macro/1 , local_macro/1 + , weird_macro/1 ]). %%============================================================================== @@ -133,6 +134,16 @@ included_macro(Config) -> ?assertEqual(Expected, Result), ok. +weird_macro(Config) -> + Uri = ?config(hover_macro_uri, Config), + #{result := Result} = els_client:hover(Uri, 12, 20), + Value = <<"```erlang\n?WEIRD_MACRO = A when A > 1\n```">>, + Expected = #{contents => #{ kind => <<"markdown">> + , value => Value + }}, + ?assertEqual(Expected, Result), + ok. + no_poi(Config) -> Uri = ?config(hover_docs_caller_uri, Config), #{result := Result} = els_client:hover(Uri, 10, 1), diff --git a/apps/els_lsp/test/els_io_string_SUITE.erl b/apps/els_lsp/test/els_io_string_SUITE.erl index f0af02c63..d8e3195fa 100644 --- a/apps/els_lsp/test/els_io_string_SUITE.erl +++ b/apps/els_lsp/test/els_io_string_SUITE.erl @@ -10,7 +10,6 @@ %% Test cases -export([ scan_forms/1 - , parse_text/1 ]). %%============================================================================== @@ -64,22 +63,6 @@ scan_forms(Config) -> ok. --spec parse_text(config()) -> ok. -parse_text(Config) -> - Path = path(Config), - {ok, IoFile} = file:open(Path, [read]), - {ok, Expected} = els_parser:parse_file(IoFile), - ok = file:close(IoFile), - - {ok, Text} = file:read_file(Path), - IoString = els_io_string:new(Text), - {ok, Result} = els_parser:parse_file(IoString), - ok = file:close(IoString), - - ?assertEqual(Expected, Result), - - ok. - %%============================================================================== %% Helper functions %%============================================================================== diff --git a/apps/els_lsp/test/els_parser_SUITE.erl b/apps/els_lsp/test/els_parser_SUITE.erl index d7b0d58d1..79db847cb 100644 --- a/apps/els_lsp/test/els_parser_SUITE.erl +++ b/apps/els_lsp/test/els_parser_SUITE.erl @@ -28,6 +28,7 @@ , type_name_macro/1 , spec_name_macro/1 , unicode_clause_pattern/1 + , latin1_source_code/1 ]). %%============================================================================== @@ -73,13 +74,13 @@ parse_invalid_code(_Config) -> -spec underscore_macro(config()) -> ok. underscore_macro(_Config) -> - ?assertMatch({ok, [#{id := '_'} | _]}, + ?assertMatch({ok, [#{id := '_', kind := define} | _]}, els_parser:parse("-define(_(Text), gettexter:gettext(Text)).")), - ?assertMatch({ok, [#{id := '_'} | _]}, + ?assertMatch({ok, [#{id := '_', kind := define} | _]}, els_parser:parse("-define(_, smth).")), - ?assertMatch({ok, []}, + ?assertMatch({ok, [#{id := '_', kind := macro}]}, els_parser:parse("?_.")), - ?assertMatch({ok, []}, + ?assertMatch({ok, [#{id := '_', kind := macro} | _]}, els_parser:parse("?_(ok).")), ok. @@ -225,16 +226,16 @@ type_name_macro(_Config) -> ok. spec_name_macro(_Config) -> - %% Currently els_dodger cannot parse this - %% We can only find a spec-context + %% Verify the parser does not crash on macros in spec function names and it + %% still returns an unnamed spec-context and POIs from the definition body Text1 = "-spec ?M() -> integer() | t().", - ?assertMatch({ok, [#{kind := spec, id := undefined}]}, - els_parser:parse(Text1)), + ?assertMatch([#{id := undefined}], parse_find_pois(Text1, spec)), + ?assertMatch([_], parse_find_pois(Text1, type_application, {t, 0})), - %% Verify the parser does not crash on macros in spec function names - %% and it still returns POIs from the definition body Text2 = "-spec ?MODULE:b() -> integer() | t().", - ?assertMatch([_], parse_find_pois(Text2, type_application, {t, 0})), + ?assertMatch([#{id := undefined}], parse_find_pois(Text2, spec)), + %% TODO: Update erlfmt, a later version can parse this + %%?assertMatch([_], parse_find_pois(Text2, type_application, {t, 0})), ok. -spec unicode_clause_pattern(config()) -> ok. @@ -245,6 +246,14 @@ unicode_clause_pattern(_Config) -> parse_find_pois(Text, function_clause, {match_literal, 1, 1})), ok. +%% Issue #306, PR #592 +-spec latin1_source_code(config()) -> ok. +latin1_source_code(_Config) -> + Text = lists:flatten(["f(\"", 200, "\") -> 200. %% ", 200]), + ?assertMatch([#{data := <<"(\"È\") "/utf8>>}], + parse_find_pois(Text, function_clause, {f, 1, 1})), + ok. + %%============================================================================== %% Helper functions %%============================================================================== diff --git a/elvis.config b/elvis.config index c14f3448d..e74131a4d 100644 --- a/elvis.config +++ b/elvis.config @@ -66,7 +66,7 @@ , {elvis_style, atom_naming_convention, disable} , {elvis_style, state_record_and_type, disable} ] - , ignore => [els_dodger, els_typer] + , ignore => [els_dodger, els_typer, els_erlfmt_ast] } , #{ dirs => ["."] , filter => "Makefile" diff --git a/rebar.config b/rebar.config index fbdc73bb1..c556cb83b 100644 --- a/rebar.config +++ b/rebar.config @@ -13,6 +13,7 @@ , {docsh, "0.7.2"} , {elvis_core, "1.1.1"} , {rebar3_format, "0.8.2"} + , {erlfmt, {git, "https://github.com/WhatsApp/erlfmt.git", {ref, "2e93fc4a"}}} %% syntaxtools-compat branch is based on this one , {ephemeral, "2.0.4"} , {tdiff, "0.1.2"} , {uuid, "2.0.1", {pkg, uuid_erl}} diff --git a/rebar.lock b/rebar.lock index d678e87be..9303ad97d 100644 --- a/rebar.lock +++ b/rebar.lock @@ -3,6 +3,10 @@ {<<"docsh">>,{pkg,<<"docsh">>,<<"0.7.2">>},0}, {<<"elvis_core">>,{pkg,<<"elvis_core">>,<<"1.1.1">>},0}, {<<"ephemeral">>,{pkg,<<"ephemeral">>,<<"2.0.4">>},0}, + {<<"erlfmt">>, + {git,"https://github.com/WhatsApp/erlfmt.git", + {ref,"2e93fc4a646111357642b0179a2a63151868d890"}}, + 0}, {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},2}, {<<"jsx">>,{pkg,<<"jsx">>,<<"3.0.0">>},0}, {<<"katana_code">>,{pkg,<<"katana_code">>,<<"0.2.1">>},1},