diff --git a/.travis.yml b/.travis.yml index e0e3e7011..c1611774a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ otp_release: - 22.0 - 21.3 script: - - rebar3 as test do compile, proper --cover, xref, cover, coveralls send + - rebar3 as test do compile, ct, proper --cover, dialyzer, xref, cover, coveralls send cache: directories: - "$HOME/.cache/rebar3" diff --git a/priv/code_navigation/include/code_navigation.hrl b/priv/code_navigation/include/code_navigation.hrl new file mode 100644 index 000000000..cd234d92b --- /dev/null +++ b/priv/code_navigation/include/code_navigation.hrl @@ -0,0 +1,3 @@ +-record(included_record_a, {field_a, field_b}). + +-define(INCLUDED_MACRO_A, included_macro_a). diff --git a/priv/code_navigation/src/behaviour_a.erl b/priv/code_navigation/src/behaviour_a.erl new file mode 100644 index 000000000..83db351d9 --- /dev/null +++ b/priv/code_navigation/src/behaviour_a.erl @@ -0,0 +1,3 @@ +-module(behaviour_a). + +-callback callback_a() -> ok. diff --git a/priv/code_navigation/src/code_navigation.erl b/priv/code_navigation/src/code_navigation.erl new file mode 100644 index 000000000..474ad168e --- /dev/null +++ b/priv/code_navigation/src/code_navigation.erl @@ -0,0 +1,29 @@ +-module(code_navigation). + +-behaviour(behaviour_a). + +-export([ function_a/0 ]). + +%% behaviour_a callbacks +-export([ callback_a/0 ]). + +-include("code_navigation.hrl"). +-include_lib("code_navigation/include/code_navigation.hrl"). + +-record(record_a, {field_a, field_b}). + +-define(MACRO_A, macro_a). + +function_a() -> + function_b(), + #record_a{}. + +function_b() -> + ?MACRO_A. + +callback_a() -> + ok. + +function_c() -> + code_navigation_extra:do(test), + length([1, 2, 3]). diff --git a/priv/code_navigation/src/code_navigation_extra.erl b/priv/code_navigation/src/code_navigation_extra.erl new file mode 100644 index 000000000..724e62e14 --- /dev/null +++ b/priv/code_navigation/src/code_navigation_extra.erl @@ -0,0 +1,6 @@ +-module(code_navigation_extra). + +-export([ do/1 ]). + +do(_Config) -> + ok. diff --git a/src/erlang_ls_buffer.erl b/src/erlang_ls_buffer.erl index 9fc6b08c7..fe668e974 100644 --- a/src/erlang_ls_buffer.erl +++ b/src/erlang_ls_buffer.erl @@ -140,12 +140,12 @@ do_get_mfa(Text, Line, _Character) -> {M, F, A}. -spec do_get_element_at_pos(binary(), non_neg_integer(), non_neg_integer()) -> - [erlang_ls_parser:poi()]. + [erlang_ls_poi:poi()]. do_get_element_at_pos(Text, Line, Column) -> %% TODO: Cache tree {ok, Tree} = erlang_ls_parser:parse(Text), - AnnotatedTree = erlang_ls_parser:annotate(Tree), - erlang_ls_parser:find_poi_by_pos(AnnotatedTree, {Line, Column}). + AnnotatedTree = erlang_ls_tree:annotate(Tree), + erlang_ls_poi:match_pos(AnnotatedTree, {Line, Column}). -spec get_line_text(binary(), integer()) -> binary(). get_line_text(Text, Line) -> diff --git a/src/erlang_ls_code_navigation.erl b/src/erlang_ls_code_navigation.erl new file mode 100644 index 000000000..ca8c7c0b5 --- /dev/null +++ b/src/erlang_ls_code_navigation.erl @@ -0,0 +1,173 @@ +%%============================================================================== +%% Code Navigation +%%============================================================================== +-module(erlang_ls_code_navigation). + +%%============================================================================== +%% Exports +%%============================================================================== + +%% API +-export([ goto_definition/2 + , goto_definition/3 + ]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include("erlang_ls.hrl"). + +%%============================================================================== +%% Macros +%%============================================================================== +-define(OTP_INCLUDE_PATH, "/usr/local/Cellar/erlang/21.2.4/lib/erlang/lib"). +%% TODO: Implement support for workspaces +-define(ERLANG_LS_PATH, "/Users/robert.aloi/git/github/erlang-ls/erlang_ls"). +-define(TEST_APP_PATH, "/Users/robert.aloi/git/github/erlang-ls/test"). +-define(DEPS_PATH, "/Users/robert.aloi/git/github/erlang-ls/erlang_ls/_build/debug/lib"). + +%%============================================================================== +%% API +%%============================================================================== + +-spec goto_definition(binary(), erlang_ls_poi:poi()) -> + {ok, binary(), erlang_ls_poi:range()} | {error, any()}. +goto_definition(Filename, POI) -> + goto_definition(Filename, POI, full_path()). + +-spec goto_definition(binary(), erlang_ls_poi:poi(), [string()]) -> + {ok, binary(), erlang_ls_poi:range()} | {error, any()}. +goto_definition( _Filename + , #{ info := {application, {M, _F, _A}} = Info } + , Path) -> + case erlang_ls_tree:annotate_file(filename(M), Path) of + {ok, FullName, AnnotatedTree} -> + case erlang_ls_poi:match(AnnotatedTree, definition(Info)) of + [#{ range := Range }] -> + {ok, FullName, Range}; + [] -> + {error, not_found} + end; + {error, Error} -> + {error, Error} + end; +goto_definition( Filename + , #{ info := {application, {_F, _A}} = Info } + , Path) -> + case erlang_ls_tree:annotate_file(filename:basename(Filename), Path) of + {ok, FullName, AnnotatedTree} -> + case erlang_ls_poi:match(AnnotatedTree, definition(Info)) of + [#{ range := Range }] -> + {ok, FullName, Range}; + [] -> + {error, not_found} + end; + {error, Error} -> + {error, Error} + end; +goto_definition(_Filename, #{ info := {behaviour, Behaviour} = Info }, Path) -> + search(filename(Behaviour), Path, definition(Info)); +%% TODO: Eventually search everywhere and suggest a code lens to include a file +goto_definition(Filename, #{ info := {macro, _Define} = Info }, Path) -> + search(filename:basename(Filename), Path, definition(Info)); +goto_definition(Filename, #{ info := {record_expr, _Record} = Info }, Path) -> + search(filename:basename(Filename), Path, definition(Info)); +goto_definition(_Filename, #{ info := {include, Include0} }, Path) -> + Include = list_to_binary(string:trim(Include0, both, [$"])), + case erlang_ls_tree:annotate_file(Include, Path) of + {ok, FullName, _AnnotatedTree} -> + {ok, FullName, #{ from => {0, 0}, to => {0, 0} }}; + {error, Error} -> + {error, Error} + end; +goto_definition(_Filename, #{ info := {include_lib, Include0} }, Path) -> + Include = list_to_binary(lists:last(filename:split(string:trim(Include0, both, [$"])))), + case erlang_ls_tree:annotate_file(Include, Path) of + {ok, FullName, _AnnotatedTree} -> + {ok, FullName, #{ from => {0, 0}, to => {0, 0} }}; + {error, Error} -> + {error, Error} + end; +goto_definition(_Filename, _, _Path) -> + {error, not_found}. + +-spec definition({atom(), any()}) -> {atom(), any()}. +definition({application, {_M, F, A}}) -> + {function, {F, A}}; +definition({application, {F, A}}) -> + {function, {F, A}}; +definition({behaviour, Behaviour}) -> + {module, Behaviour}; +definition({macro, Define}) -> + {define, Define}; +definition({record_expr, Record}) -> + {record, Record}. + +-spec otp_path() -> [string()]. +otp_path() -> + filelib:wildcard(filename:join([?OTP_INCLUDE_PATH, "*/src"])). + +-spec app_path() -> [string()]. +app_path() -> + [ filename:join([?TEST_APP_PATH, "src"]) + , filename:join([?TEST_APP_PATH, "include"]) + , filename:join([?ERLANG_LS_PATH, "src"]) + , filename:join([?TEST_APP_PATH, "include"]) + ]. + +-spec deps_path() -> [string()]. +deps_path() -> + filelib:wildcard(filename:join([?DEPS_PATH, "*/src"])). + +full_path() -> + lists:append( [ app_path() , deps_path() , otp_path() ]). + +%% Look for a definition recursively in a file and its includes. +-spec search(binary(), [string()], any()) -> + {ok, binary(), erlang_ls_poi:range()} | {error, any()}. +search(Filename, Path, Thing) -> + case erlang_ls_tree:annotate_file(Filename, Path) of + {ok, FullName, AnnotatedTree} -> + case find(AnnotatedTree, Thing) of + {error, not_found} -> + Includes = erlang_ls_poi:match_key(AnnotatedTree, include), + IncludeLibs = erlang_ls_poi:match_key(AnnotatedTree, include_lib), + search_in_includes(Includes ++ IncludeLibs, Path, Thing); + {ok, Range} -> + {ok, FullName, Range} + end; + {error, Error} -> + {error, Error} + end. + +%% Look for a definition in a given tree +-spec find(erlang_ls_tree:tree(), any()) -> + {ok, erlang_ls_poi:range()} | {error, any()}. +find(AnnotatedTree, Thing) -> + case erlang_ls_poi:match(AnnotatedTree, Thing) of + [#{ range := Range }|_] -> + {ok, Range}; + [] -> + {error, not_found} + end. + +-spec search_in_includes([erlang_ls_poi:poi()], [string()], any()) -> + {ok, binary(), erlang_ls_poi:range()} | {error, any()}. +search_in_includes([], _Path, _Thing) -> + {error, not_found}; +search_in_includes([#{info := Info}|T], Path, Thing) -> + Include = normalize_include(Info), + case search(list_to_binary(Include), Path, Thing) of + {error, _Error} -> search_in_includes(T, Path, Thing); + {ok, FullName, Range} -> {ok, FullName, Range} + end. + +-spec normalize_include({atom(), string()}) -> string(). +normalize_include({include, Include}) -> + string:trim(Include, both, [$"]); +normalize_include({include_lib, Include}) -> + lists:last(filename:split(string:trim(Include, both, [$"]))). + +-spec filename(atom()) -> binary(). +filename(Module) -> + list_to_binary(atom_to_list(Module) ++ ".erl"). diff --git a/src/erlang_ls_completion.erl b/src/erlang_ls_completion.erl index 090187098..d4466413e 100644 --- a/src/erlang_ls_completion.erl +++ b/src/erlang_ls_completion.erl @@ -3,7 +3,6 @@ -export([ record_definitions/0 ]). -%% TODO: Store these in an ETS table -spec record_definitions() -> [atom()]. record_definitions() -> lists:usort(lists:flatten([record_definitions(B) || B <- all_beams()])). diff --git a/src/erlang_ls_parser.erl b/src/erlang_ls_parser.erl index 88eb0a299..3699b3c6a 100644 --- a/src/erlang_ls_parser.erl +++ b/src/erlang_ls_parser.erl @@ -1,257 +1,34 @@ -%% TODO: Rename into erlang_ls_syntax -module(erlang_ls_parser). --export([ annotate/1 - , annotate_node/1 - , find_poi_by_info/2 - , find_poi_by_info_key/2 - , find_poi_by_pos/2 - , list_poi/1 - , parse/1 +-export([ parse/1 , parse_file/1 - , postorder_update/2 ]). --type syntax_tree() :: erl_syntax:syntaxTree(). --type line() :: non_neg_integer(). --type column() :: non_neg_integer(). --type pos() :: {line(), column()}. --type range() :: #{ from := pos(), to := pos() }. -%% Point of Interest --type poi() :: #{ type := atom(), info => any(), range := range()}. - --export_type([ poi/0 - , range/0 - ]). - -%%============================================================================== -%% Dialyzer Exceptions -%%============================================================================== -%% The specs for the epp_dodger API are slightly incorrect. -%% A bug has been reported (see https://bugs.erlang.org/browse/ERL-1005) -%% Meanwhile, let's skip checking this module. --dialyzer(no_contracts). --dialyzer(no_return). --dialyzer(no_unused). --dialyzer(no_fail_call). - %% TODO: Generate random filename %% TODO: Ideally avoid writing to file at all (require epp changes) --define(TMP_PATH, "/tmp/erlang_ls_tmp"). +-define(TMP_PATH, <<"/tmp/erlang_ls_tmp">>). --spec parse(binary()) -> {ok, syntax_tree()}. +-spec parse(binary()) -> {ok, erlang_ls_tree:tree()}. parse(Text) -> %% epp_dodger only works with source files, %% so let's use a temporary file. ok = file:write_file(?TMP_PATH, Text), parse_file(?TMP_PATH). --spec parse_file(string()) -> {ok, syntax_tree()} | {error, any()}. +-spec parse_file(binary()) -> + {ok, erlang_ls_tree:tree()} | {error, any()}. parse_file(Path) -> case file:open(Path, [read]) of {ok, IoDevice} -> %% Providing `{1, 1}` as the initial location ensures %% that the returned forms include column numbers, as well. - {ok, Forms} = epp_dodger:parse(IoDevice, {1, 1}), + %% The specs for the epp_dodger API are slightly incorrect. + %% A bug has been reported (see https://bugs.erlang.org/browse/ERL-1005) + %% Meanwhile, let's trick Dialyzer with an apply. + {ok, Forms} = erlang:apply(epp_dodger, parse, [IoDevice, {1, 1}]), Tree = erl_syntax:form_list(Forms), ok = file:close(IoDevice), {ok, Tree}; {error, Error} -> {error, Error} end. - --spec annotate(syntax_tree()) -> syntax_tree(). -annotate(Tree) -> - postorder_update(fun annotate_node/1, Tree). - -%% Create annotations for the points of interest (aka `poi`) in the -%% tree. --spec annotate_node(syntax_tree()) -> syntax_tree(). -annotate_node(Tree) -> - lists:foldl(fun erl_syntax:add_ann/2, Tree, analyze(Tree)). - -%% Extracted from the `erl_syntax` documentation. --spec postorder_update(fun(), syntax_tree()) -> syntax_tree(). -postorder_update(F, Tree) -> - F(case erl_syntax:subtrees(Tree) of - [] -> Tree; - List -> erl_syntax:update_tree(Tree, - [[postorder_update(F, Subtree) - || Subtree <- Group] - || Group <- List]) - end). - --spec get_range(syntax_tree(), pos(), {atom(), any()}) -> range(). -get_range(_Tree, {Line, Column}, {application, {M, F, _A}}) -> - CFrom = Column - length(atom_to_list(M)), - From = {Line, CFrom}, - CTo = Column + length(atom_to_list(F)), - To = {Line, CTo}, - #{ from => From, to => To }; -get_range(_Tree, {Line, Column}, {application, {F, _A}}) -> - From = {Line, Column}, - To = {Line, Column + length(atom_to_list(F))}, - #{ from => From, to => To }; -get_range(_Tree, {Line, Column}, {behaviour, Behaviour}) -> - From = {Line, Column - 1}, - To = {Line, Column + length("behaviour") + length(atom_to_list(Behaviour))}, - #{ from => From, to => To }; -get_range(_Tree, {_Line, _Column}, {exports_entry, {_F, _A}}) -> - %% TODO: The location information for the arity qualifiers are lost during - %% parsing in `epp_dodger`. This requires fixing. - #{ from => {0, 0}, to => {0, 0} }; -get_range(_Tree, {Line, Column}, {function, {F, _A}}) -> - From = {Line - 1, Column - 1}, - To = {Line - 1, Column + length(atom_to_list(F)) - 1}, - #{ from => From, to => To }; -get_range(_Tree, {Line, _Column}, {define, _Define}) -> - From = {Line - 1, 0}, - To = From, - #{ from => From, to => To }; -get_range(_Tree, {Line, Column}, {include, Include}) -> - From = {Line, Column - 1}, - To = {Line, Column + length("include") + length(Include)}, - #{ from => From, to => To }; -get_range(_Tree, {Line, Column}, {include_lib, Include}) -> - From = {Line, Column - 1}, - To = {Line, Column + length("include_lib") + length(Include)}, - #{ from => From, to => To }; -get_range(_Tree, {Line, Column}, {macro, Macro}) -> - From = {Line, Column}, - To = {Line, Column + length(atom_to_list(Macro))}, - #{ from => From, to => To }; -get_range(_Tree, {Line, Column}, {record_expr, Record}) -> - From = {Line, Column - 1}, - To = {Line, Column + length(Record) - 1}, - #{ from => From, to => To }; -%% TODO: Distinguish between usage poi and definition poi -get_range(_Tree, {Line, _Column}, {record, _Record}) -> - From = {Line - 1, 0}, - To = From, - #{ from => From, to => To }; -get_range(_Tree, {_Line, _Column}, {spec, _Spec}) -> - %% TODO: The location information for the arity qualifiers are lost during - %% parsing in `epp_dodger`. This requires fixing. - #{ from => {0, 0}, to => {0, 0} }. - --spec find_poi_by_info(syntax_tree(), any()) -> poi(). -find_poi_by_info(Tree, Info0) -> - [POI || #{info := Info} = POI <- list_poi(Tree), Info0 =:= Info]. - -%% TODO: Rename --spec find_poi_by_info_key(syntax_tree(), atom()) -> poi(). -find_poi_by_info_key(Tree, Key0) -> - [POI || #{info := {Key, _}} = POI <- list_poi(Tree), Key0 =:= Key]. - --spec find_poi_by_pos(syntax_tree(), pos()) -> [poi()]. -find_poi_by_pos(Tree, Pos) -> - [POI || #{range := Range} = POI <- list_poi(Tree), matches_pos(Pos, Range)]. - --spec list_poi(syntax_tree()) -> [poi()]. -list_poi(Tree) -> - F = fun(T, Acc) -> - Annotations = erl_syntax:get_ann(T), - case [POI || #{ type := poi } = POI <- Annotations] of - [] -> Acc; - L -> L ++ Acc - end - end, - erl_syntax_lib:fold(F, [], Tree). - --spec matches_pos(pos(), range()) -> boolean(). -matches_pos(Pos, #{from := From, to := To}) -> - (From =< Pos) andalso (Pos =< To). - --spec analyze(syntax_tree()) -> [poi()]. -analyze(Tree) -> - Type = erl_syntax:type(Tree), - try analyze(Tree, Type) - catch - Class:Reason -> - lager:warning("Could not analyze tree: ~p:~p", [Class, Reason]), - [] - end. - --spec analyze(syntax_tree(), any()) -> [poi()]. -analyze(Tree, application) -> - case erl_syntax_lib:analyze_application(Tree) of - {M, {F, A}} -> - %% Remote call - [poi(Tree, {application, {M, F, A}})]; - {F, A} -> - case lists:member({F, A}, erlang:module_info(exports)) of - true -> - %% Call to a function from the `erlang` module - [poi(Tree, {application, {erlang, F, A}})]; - false -> - %% Local call - [poi(Tree, {application, {F, A}})] - end; - 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 - %% arity A is returned. - %% In the special case where the macro `?MODULE` is used as the - %% module qualifier, we can consider it as a local call. - Operator = erl_syntax:application_operator(Tree), - try { erl_syntax:variable_name( - erl_syntax:macro_name( - erl_syntax:module_qualifier_argument(Operator))) - , erl_syntax:atom_value( - erl_syntax:module_qualifier_body(Operator)) - } of - {'MODULE', F} -> - [poi(Tree, {application, {'MODULE', F, A}})] - catch _:_ -> - [] - end - end; -analyze(Tree, attribute) -> - case erl_syntax_lib:analyze_attribute(Tree) of - %% Yes, Erlang allows both British and American spellings for - %% keywords. - {behavior, {behavior, Behaviour}} -> - [poi(Tree, {behaviour, Behaviour})]; - {behaviour, {behaviour, Behaviour}} -> - [poi(Tree, {behaviour, Behaviour})]; - {export, Exports} -> - [poi(Tree, {exports_entry, {F, A}}) || {F, A} <- Exports]; - preprocessor -> - Name = erl_syntax:atom_value(erl_syntax:attribute_name(Tree)), - case {Name, erl_syntax:attribute_arguments(Tree)} of - {define, [Define|_]} -> - [poi(Tree, {define, erl_syntax:variable_name(Define)})]; - {include, [String]} -> - [poi(Tree, {include, erl_syntax:string_literal(String)})]; - {include_lib, [String]} -> - [poi(Tree, {include_lib, erl_syntax:string_literal(String)})]; - _ -> - [] - end; - {record, {Record, _Fields}} -> - [poi(Tree, {record, atom_to_list(Record)})]; - {spec, Spec} -> - [poi(Tree, {spec, Spec})]; - _ -> - [] - end; -analyze(Tree, function) -> - {F, A} = erl_syntax_lib:analyze_function(Tree), - [poi(Tree, {function, {F, A}})]; -analyze(Tree, macro) -> - Macro = erl_syntax:variable_name(erl_syntax:macro_name(Tree)), - [poi(Tree, {macro, Macro})]; -analyze(Tree, record_expr) -> - Record = erl_syntax:atom_name(erl_syntax:record_expr_type(Tree)), - [poi(Tree, {record_expr, Record})]; -analyze(_Tree, _) -> - []. - --spec poi(syntax_tree(), any()) -> poi(). -poi(Tree, Info) -> - Pos = erl_syntax:get_pos(Tree), - Range = get_range(Tree, Pos, Info), - #{ type => poi - , info => Info - , range => Range - }. diff --git a/src/erlang_ls_poi.erl b/src/erlang_ls_poi.erl new file mode 100644 index 000000000..362e983f1 --- /dev/null +++ b/src/erlang_ls_poi.erl @@ -0,0 +1,128 @@ +%%============================================================================== +%% The Point Of Interest (a.k.a. _poi_) Data Structure +%%============================================================================== +-module(erlang_ls_poi). + +-type line() :: non_neg_integer(). +-type column() :: non_neg_integer(). +-type pos() :: {line(), column()}. +-type range() :: #{ from := pos(), to := pos() }. +-type poi() :: #{ type := atom() + , info => any() + , range := range() + }. + +-export_type([ poi/0 + , range/0 + ]). + +-export([ poi/2 ]). + +-export([ list/1 + , match/2 + , match_key/2 + , match_pos/2 + ]). + +%%============================================================================== +%% API +%%============================================================================== + +%% @edoc Constructor for a Point of Interest. +-spec poi(erlang_ls_tree:tree(), any()) -> poi(). +poi(Tree, Info) -> + Pos = erl_syntax:get_pos(Tree), + Range = get_range(Pos, Info), + #{ type => poi + , info => Info + , range => Range + }. + +%% @edoc List the Points of Interest for a given tree. +-spec list(erlang_ls_tree:tree()) -> [poi()]. +list(Tree) -> + F = fun(T, Acc) -> + Annotations = erl_syntax:get_ann(T), + case [POI || #{ type := poi } = POI <- Annotations] of + [] -> Acc; + L -> L ++ Acc + end + end, + erl_syntax_lib:fold(F, [], Tree). + +-spec match(erlang_ls_tree:tree(), any()) -> [poi()]. +match(Tree, Info0) -> + [POI || #{info := Info} = POI <- list(Tree), Info0 =:= Info]. + +-spec match_key(erlang_ls_tree:tree(), atom()) -> [poi()]. +match_key(Tree, Key0) -> + [POI || #{info := {Key, _}} = POI <- list(Tree), Key0 =:= Key]. + +-spec match_pos(erlang_ls_tree:tree(), pos()) -> [poi()]. +match_pos(Tree, Pos) -> + [POI || #{range := Range} = POI <- list(Tree), matches_pos(Pos, Range)]. + +%%============================================================================== +%% Internal Functions +%%============================================================================== + +-spec get_range(pos(), {atom(), any()}) -> range(). +get_range({Line, Column}, {application, {M, F, _A}}) -> + CFrom = Column - length(atom_to_list(M)), + From = {Line, CFrom}, + CTo = Column + length(atom_to_list(F)), + To = {Line, CTo}, + #{ from => From, to => To }; +get_range({Line, Column}, {application, {F, _A}}) -> + From = {Line, Column}, + To = {Line, Column + length(atom_to_list(F))}, + #{ from => From, to => To }; +get_range({Line, Column}, {behaviour, Behaviour}) -> + From = {Line, Column - 1}, + To = {Line, Column + length("behaviour") + length(atom_to_list(Behaviour))}, + #{ from => From, to => To }; +get_range({_Line, _Column}, {exports_entry, {_F, _A}}) -> + %% TODO: The location information for the arity qualifiers are lost during + %% parsing in `epp_dodger`. This requires fixing. + #{ from => {0, 0}, to => {0, 0} }; +get_range({Line, Column}, {function, {F, _A}}) -> + From = {Line - 1, Column - 1}, + To = {Line - 1, Column + length(atom_to_list(F)) - 1}, + #{ from => From, to => To }; +get_range({Line, _Column}, {define, _Define}) -> + From = {Line - 1, 0}, + To = From, + #{ from => From, to => To }; +get_range({Line, Column}, {include, Include}) -> + From = {Line, Column - 1}, + To = {Line, Column + length("include") + length(Include)}, + #{ from => From, to => To }; +get_range({Line, Column}, {include_lib, Include}) -> + From = {Line, Column - 1}, + To = {Line, Column + length("include_lib") + length(Include)}, + #{ from => From, to => To }; +get_range({Line, Column}, {macro, Macro}) -> + From = {Line, Column}, + To = {Line, Column + length(atom_to_list(Macro))}, + #{ from => From, to => To }; +get_range({Line, Column}, {module, _}) -> + From = {Line - 1, Column - 1}, + To = From, + #{ from => From, to => To }; +get_range({Line, Column}, {record_expr, Record}) -> + From = {Line, Column - 1}, + To = {Line, Column + length(Record) - 1}, + #{ from => From, to => To }; +%% TODO: Distinguish between usage poi and definition poi +get_range({Line, _Column}, {record, _Record}) -> + From = {Line - 1, 0}, + To = From, + #{ from => From, to => To }; +get_range({_Line, _Column}, {spec, _Spec}) -> + %% TODO: The location information for the arity qualifiers are lost during + %% parsing in `epp_dodger`. This requires fixing. + #{ from => {0, 0}, to => {0, 0} }. + +-spec matches_pos(pos(), range()) -> boolean(). +matches_pos(Pos, #{from := From, to := To}) -> + (From =< Pos) andalso (Pos =< To). diff --git a/src/erlang_ls_protocol.erl b/src/erlang_ls_protocol.erl index ca4a64f46..5c7d337ff 100644 --- a/src/erlang_ls_protocol.erl +++ b/src/erlang_ls_protocol.erl @@ -52,7 +52,7 @@ response(RequestId, Result) -> %%============================================================================== %% Data Structures %%============================================================================== --spec range(erlang_ls_parser:range()) -> range(). +-spec range(erlang_ls_poi:range()) -> range(). range(#{ from := {FromL, FromC}, to := {ToL, ToC} }) -> #{ start => #{line => FromL, character => FromC} , 'end' => #{line => ToL, character => ToC} diff --git a/src/erlang_ls_server.erl b/src/erlang_ls_server.erl index 6efe9ff38..40d58cddf 100644 --- a/src/erlang_ls_server.erl +++ b/src/erlang_ls_server.erl @@ -36,12 +36,6 @@ %%============================================================================== -record(state, {socket, buffer}). --define(OTP_INCLUDE_PATH, "/usr/local/Cellar/erlang/21.2.4/lib/erlang/lib"). -%% TODO: Implement support for workspaces --define(ERLANG_LS_PATH, "/Users/robert.aloi/git/github/erlang-ls/erlang_ls"). --define(TEST_APP_PATH, "/Users/robert.aloi/git/github/erlang-ls/test"). --define(DEPS_PATH, "/Users/robert.aloi/git/github/erlang-ls/erlang_ls/_build/debug/lib"). - %%============================================================================== %% Type Definitions %%============================================================================== @@ -183,7 +177,15 @@ handle_method(<<"textDocument/definition">>, Params) -> {ok, Buffer} = erlang_ls_buffer_server:get_buffer(Uri), case erlang_ls_buffer:get_element_at_pos(Buffer, Line + 1, Character + 1) of [POI|_] -> - {response, definition(Uri, POI)}; + Filename = erlang_ls_uri:path(Uri), + case erlang_ls_code_navigation:goto_definition(Filename, POI) of + {error, _Error} -> + {response, null}; + {ok, FullName, Range} -> + {response, #{ uri => erlang_ls_uri:uri(FullName) + , range => erlang_ls_protocol:range(Range) + }} + end; [] -> {response, null} end; @@ -201,171 +203,3 @@ send_notification(Socket, Method, Params) -> Notification = erlang_ls_protocol:notification(Method, Params), lager:debug("[SERVER] Sending notification [notification=~p]", [Notification]), gen_tcp:send(Socket, Notification). - --spec definition(uri(), erlang_ls_parser:poi()) -> null | map(). -definition(_Uri, #{ info := {application, {M, _F, _A}} = Info }) -> - case annotated_tree(erlang_ls_uri:filename(M)) of - {ok, Uri, AnnotatedTree} -> - case erlang_ls_parser:find_poi_by_info(AnnotatedTree, definition(Info)) of - [#{ range := Range }] -> - %% TODO: Use API to create types - #{ uri => Uri - , range => erlang_ls_protocol:range(Range) - }; - [] -> - null - end; - {error, _Error} -> - null - end; -definition(Uri, #{ info := {application, {_F, _A}} = Info }) -> - case annotated_tree(erlang_ls_uri:basename(Uri)) of - {ok, Uri, AnnotatedTree} -> - case erlang_ls_parser:find_poi_by_info(AnnotatedTree, definition(Info)) of - [#{ range := Range }] -> - %% TODO: Use API to create types - #{ uri => Uri - , range => erlang_ls_protocol:range(Range) - }; - [] -> - null - end; - {error, _Error} -> - null - end; -definition(_Uri, #{ info := {behaviour, Behaviour} }) -> - case annotated_tree(erlang_ls_uri:filename(Behaviour)) of - {ok, Uri, _AnnotatedTree} -> - #{ uri => Uri - %% TODO: We could point to the module attribute, instead - , range => erlang_ls_protocol:range(#{ from => {0, 0} - , to => {0, 0} - }) - }; - {error, _Error} -> - null - end; -%% TODO: Eventually search everywhere and suggest a code lens to include a file -definition(Uri, #{ info := {macro, _Define} = Info }) -> - Filename = erlang_ls_uri:basename(Uri), - search(Filename, app_path(), definition(Info)); -definition(Uri, #{ info := {record_expr, _Record} = Info }) -> - Filename = erlang_ls_uri:basename(Uri), - search(Filename, app_path(), Info); -definition(_Uri, #{ info := {include, Include0} }) -> - Include = list_to_binary(string:trim(Include0, both, [$"])), - case annotated_tree(Include) of - {ok, Uri, _AnnotatedTree} -> - #{ uri => Uri - %% TODO: We could point to the module attribute, instead - , range => erlang_ls_protocol:range(#{ from => {0, 0} - , to => {0, 0} - }) - }; - {error, _Error} -> - null - end; -definition(_Uri, #{ info := {include_lib, Include0} }) -> - Include = list_to_binary(lists:last(filename:split(string:trim(Include0, both, [$"])))), - case annotated_tree(Include) of - {ok, Uri, _AnnotatedTree} -> - #{ uri => Uri - %% TODO: We could point to the module attribute, instead - , range => erlang_ls_protocol:range(#{ from => {0, 0} - , to => {0, 0} - }) - }; - {error, _Error} -> - null - end; -definition(_Uri, _) -> - null. - --spec definition({atom(), any()}) -> {atom(), any()}. -definition({application, {_M, F, A}}) -> - {function, {F, A}}; -definition({application, {F, A}}) -> - {function, {F, A}}; -definition({macro, Define}) -> - {define, Define}; -definition({record_expr, Record}) -> - {record, Record}. - --spec annotated_tree(binary()) -> - {ok, uri(), erlang_ls_parser:syntax_tree()} | {error, any()}. -annotated_tree(Filename) -> - Path = lists:append( [ app_path() , deps_path() , otp_path() ]), - annotated_tree(Filename, Path). - --spec annotated_tree(binary(), [string()]) -> - {ok, uri(), erlang_ls_parser:syntax_tree()} | {error, any()}. -annotated_tree(Filename, Path) -> - case file:path_open(Path, Filename, [read]) of - {ok, IoDevice, FullName} -> - %% TODO: Avoid opening file twice - file:close(IoDevice), - {ok, Tree} = erlang_ls_parser:parse_file(FullName), - Uri = erlang_ls_uri:uri(FullName), - {ok, Uri, erlang_ls_parser:annotate(Tree)}; - {error, Error} -> - {error, Error} - end. - --spec otp_path() -> [string()]. -otp_path() -> - filelib:wildcard(filename:join([?OTP_INCLUDE_PATH, "*/src"])). - --spec app_path() -> [string()]. -app_path() -> - [ filename:join([?TEST_APP_PATH, "src"]) - , filename:join([?TEST_APP_PATH, "include"]) - , filename:join([?ERLANG_LS_PATH, "src"]) - , filename:join([?TEST_APP_PATH, "include"]) - ]. - --spec deps_path() -> [string()]. -deps_path() -> - filelib:wildcard(filename:join([?DEPS_PATH, "*/src"])). - -%% Look for a definition recursively in a file and its includes. --spec search(binary(), [string()], any()) -> null | map(). -search(Filename, Path, Thing) -> - case annotated_tree(Filename, Path) of - {ok, Uri, AnnotatedTree} -> - case find(Uri, AnnotatedTree, Thing) of - null -> - Includes = erlang_ls_parser:find_poi_by_info_key(AnnotatedTree, include), - IncludeLibs = erlang_ls_parser:find_poi_by_info_key(AnnotatedTree, include_lib), - search_in_includes(Includes ++ IncludeLibs, Thing); - Def -> - Def - end; - {error, _Error} -> - null - end. - -%% Look for a definition in a given tree --spec find(uri(), erlang_ls_parser:syntax_tree(), any()) -> null | map(). -find(Uri, AnnotatedTree, Thing) -> - case erlang_ls_parser:find_poi_by_info(AnnotatedTree, Thing) of - [#{ range := Range }|_] -> - #{ uri => Uri, range => erlang_ls_protocol:range(Range) }; - [] -> - null - end. - --spec search_in_includes([erlang_ls_parser:poi()], string()) -> null | map(). -search_in_includes([], _Thing) -> - null; -search_in_includes([#{info := Info}|T], Thing) -> - Include = normalize_include(Info), - case search(list_to_binary(Include), app_path(), Thing) of - null -> search_in_includes(T, Thing); - Def -> Def - end. - --spec normalize_include({atom(), string()}) -> string(). -normalize_include({include, Include}) -> - string:trim(Include, both, [$"]); -normalize_include({include_lib, Include}) -> - lists:last(filename:split(string:trim(Include, both, [$"]))). diff --git a/src/erlang_ls_tree.erl b/src/erlang_ls_tree.erl new file mode 100644 index 000000000..9ce9a74c8 --- /dev/null +++ b/src/erlang_ls_tree.erl @@ -0,0 +1,166 @@ +%%============================================================================== +%% Library to handle syntax trees annotated with points of interest +%%============================================================================== +-module(erlang_ls_tree). + +%%============================================================================== +%% Exports +%%============================================================================== +-export([ annotate/1 + , annotate_file/2 + , annotate_node/1 + , postorder_update/2 + , points_of_interest/1 + ]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include("erlang_ls.hrl"). + +%%============================================================================== +%% Types +%%============================================================================== +-type tree() :: erl_syntax:syntaxTree(). +-type annotated_tree() :: tree(). + +-export_type([ annotated_tree/0 + , tree/0 + ]). + +%%============================================================================== +%% API +%%============================================================================== +%% @edoc Given a syntax tree, it returns a new one, annotated with all +%% the identified _points of interest_ (a.k.a. _poi_). +-spec annotate(tree()) -> tree(). +annotate(Tree) -> + postorder_update(fun annotate_node/1, Tree). + +-spec annotate_file(binary(), [string()]) -> + {ok, binary(), annotated_tree()} | {error, any()}. +annotate_file(Filename, Path) -> + case file:path_open(Path, Filename, [read]) of + {ok, IoDevice, FullName} -> + %% TODO: Avoid opening file twice + file:close(IoDevice), + {ok, Tree} = erlang_ls_parser:parse_file(FullName), + {ok, FullName, annotate(Tree)}; + {error, Error} -> + {error, Error} + end. + + +%% @edoc Add an annotation to the root of the given `Tree` for each +%% point of interest found. +-spec annotate_node(tree()) -> tree(). +annotate_node(Tree) -> + lists:foldl(fun erl_syntax:add_ann/2, Tree, points_of_interest(Tree)). + +%% @edoc Traverse the given `Tree`, applying the function `F` to all +%% nodes in the tree, in post-order. Extracted from the `erl_syntax` +%% documentation. +-spec postorder_update(fun(), tree()) -> tree(). +postorder_update(F, Tree) -> + F(case erl_syntax:subtrees(Tree) of + [] -> Tree; + List -> erl_syntax:update_tree(Tree, + [[postorder_update(F, Subtree) + || Subtree <- Group] + || Group <- List]) + end). + +%% @edoc Return the list of points of interest for a given `Tree`. +-spec points_of_interest(tree()) -> [erlang_ls_poi:poi()]. +points_of_interest(Tree) -> + Type = erl_syntax:type(Tree), + try points_of_interest(Tree, Type) + catch + Class:Reason -> + lager:warning("Could not analyze tree: ~p:~p", [Class, Reason]), + [] + end. + +%% @edoc Return the list of points of interest of a specific `Type` +%% for a given `Tree`. +-spec points_of_interest(tree(), any()) -> [erlang_ls_poi:poi()]. +points_of_interest(Tree, application) -> + case erl_syntax_lib:analyze_application(Tree) of + {M, {F, A}} -> + %% Remote call + [erlang_ls_poi:poi(Tree, {application, {M, F, A}})]; + {F, A} -> + case lists:member({F, A}, erlang:module_info(exports)) of + true -> + %% Call to a function from the `erlang` module + [erlang_ls_poi:poi(Tree, {application, {erlang, F, A}})]; + false -> + %% Local call + [erlang_ls_poi:poi(Tree, {application, {F, A}})] + end; + 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 + %% arity A is returned. + %% In the special case where the macro `?MODULE` is used as the + %% module qualifier, we can consider it as a local call. + Operator = erl_syntax:application_operator(Tree), + try { erl_syntax:variable_name( + erl_syntax:macro_name( + erl_syntax:module_qualifier_argument(Operator))) + , erl_syntax:atom_value( + erl_syntax:module_qualifier_body(Operator)) + } of + {'MODULE', F} -> + [erlang_ls_poi:poi(Tree, {application, {'MODULE', F, A}})] + catch _:_ -> + [] + end + end; +points_of_interest(Tree, attribute) -> + case erl_syntax_lib:analyze_attribute(Tree) of + %% Yes, Erlang allows both British and American spellings for + %% keywords. + {behavior, {behavior, Behaviour}} -> + [erlang_ls_poi:poi(Tree, {behaviour, Behaviour})]; + {behaviour, {behaviour, Behaviour}} -> + [erlang_ls_poi:poi(Tree, {behaviour, Behaviour})]; + {export, Exports} -> + [erlang_ls_poi:poi(Tree, {exports_entry, {F, A}}) || {F, A} <- Exports]; + {module, {Module, _Args}} -> + [erlang_ls_poi:poi(Tree, {module, Module})]; + {module, Module} -> + [erlang_ls_poi:poi(Tree, {module, Module})]; + preprocessor -> + Name = erl_syntax:atom_value(erl_syntax:attribute_name(Tree)), + case {Name, erl_syntax:attribute_arguments(Tree)} of + {define, [Define|_]} -> + [erlang_ls_poi:poi( Tree + , {define, erl_syntax:variable_name(Define)})]; + {include, [String]} -> + [erlang_ls_poi:poi( Tree + , {include, erl_syntax:string_literal(String)})]; + {include_lib, [String]} -> + [erlang_ls_poi:poi( Tree + , {include_lib, erl_syntax:string_literal(String)})]; + _ -> + [] + end; + {record, {Record, _Fields}} -> + [erlang_ls_poi:poi(Tree, {record, atom_to_list(Record)})]; + {spec, Spec} -> + [erlang_ls_poi:poi(Tree, {spec, Spec})]; + _ -> + [] + end; +points_of_interest(Tree, function) -> + {F, A} = erl_syntax_lib:analyze_function(Tree), + [erlang_ls_poi:poi(Tree, {function, {F, A}})]; +points_of_interest(Tree, macro) -> + Macro = erl_syntax:variable_name(erl_syntax:macro_name(Tree)), + [erlang_ls_poi:poi(Tree, {macro, Macro})]; +points_of_interest(Tree, record_expr) -> + Record = erl_syntax:atom_name(erl_syntax:record_expr_type(Tree)), + [erlang_ls_poi:poi(Tree, {record_expr, Record})]; +points_of_interest(_Tree, _) -> + []. diff --git a/src/erlang_ls_uri.erl b/src/erlang_ls_uri.erl index 9f68412d0..c444ab00f 100644 --- a/src/erlang_ls_uri.erl +++ b/src/erlang_ls_uri.erl @@ -8,9 +8,7 @@ %%============================================================================== %% Exports %%============================================================================== --export([ basename/1 - , filename/1 - , module/1 +-export([ module/1 , path/1 , uri/1 ]). @@ -20,17 +18,9 @@ %%============================================================================== -include("erlang_ls.hrl"). --spec basename(uri()) -> binary(). -basename(Uri) -> - filename:basename(path(Uri)). - --spec filename(atom()) -> binary(). -filename(Module) -> - list_to_binary(atom_to_list(Module) ++ ".erl"). - -spec module(uri()) -> atom(). module(Uri) -> - binary_to_atom(filename:basename(basename(Uri), <<".erl">>), utf8). + binary_to_atom(filename:basename(path(Uri), <<".erl">>), utf8). -spec path(uri()) -> uri_path(). path(<<"file://", Path/binary>>) -> diff --git a/test/erlang_ls_code_navigation_SUITE.erl b/test/erlang_ls_code_navigation_SUITE.erl new file mode 100644 index 000000000..8fd401fcf --- /dev/null +++ b/test/erlang_ls_code_navigation_SUITE.erl @@ -0,0 +1,184 @@ +%%============================================================================== +%% Unit Tests for Code Navigation +%%============================================================================== +-module(erlang_ls_code_navigation_SUITE). + +%% CT Callbacks +-export([ suite/0 + , init_per_suite/1 + , end_per_suite/1 + , init_per_testcase/2 + , end_per_testcase/2 + , all/0 + ]). + +%% Test cases +-export([ application_local/1 + , application_remote/1 + , behaviour/1 + , include/1 + , include_lib/1 + , macro/1 + , macro_included/1 + , record/1 + , record_included/1 + ]). + +%%============================================================================== +%% Includes +%%============================================================================== +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +%%============================================================================== +%% Defines +%%============================================================================== +-define(TEST_APP, <<"code_navigation">>). + +%%============================================================================== +%% Types +%%============================================================================== +-type config() :: [{atom(), any()}]. + +%%============================================================================== +%% CT Callbacks +%%============================================================================== +-spec suite() -> [tuple()]. +suite() -> + [{timetrap, {seconds, 30}}]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + RootDir = code:priv_dir(erlang_ls), + AppDir = filename:join([list_to_binary(RootDir), ?TEST_APP]), + [ {app_dir, AppDir} + , {include_path, [ filename:join([AppDir, "src"]) + , filename:join([AppDir, "include"]) + ]} + |Config]. + +-spec end_per_suite(config()) -> ok. +end_per_suite(_Config) -> + ok. + +-spec init_per_testcase(atom(), config()) -> config(). +init_per_testcase(_TestCase, Config) -> + Config. + +-spec end_per_testcase(atom(), config()) -> ok. +end_per_testcase(_TestCase, _Config) -> + ok. + +-spec all() -> [atom()]. +all() -> + [ application_local + , application_remote + , behaviour + , include + , include_lib + , macro + , macro_included + , record + , record_included + ]. + +%%============================================================================== +%% Testcases +%%============================================================================== +-spec application_local(config()) -> ok. +application_local(Config) -> + Thing = #{info => {application, {function_b, 0}}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(src, <<"code_navigation.erl">>, Config), FullName), + ?assertEqual(#{from => {20, 0}, to => {20, 10}}, Range), + ok. + +-spec application_remote(config()) -> ok. +application_remote(Config) -> + Thing = #{info => {application, {code_navigation_extra, do, 1}}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(src, <<"code_navigation_extra.erl">>, Config), FullName), + ?assertEqual(#{from => {4, 0}, to => {4, 2}}, Range), + ok. + +-spec behaviour(config()) -> ok. +behaviour(Config) -> + Thing = #{info => {behaviour, 'behaviour_a'}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(src, <<"behaviour_a.erl">>, Config), FullName), + ?assertEqual(#{from => {0, 1}, to => {0, 1}}, Range), + ok. + +-spec include(config()) -> ok. +include(Config) -> + Thing = #{info => {include, "code_navigation.hrl"}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(include, <<"code_navigation.hrl">>, Config), FullName), + ?assertEqual(#{from => {0, 0}, to => {0, 0}}, Range), + ok. + +-spec include_lib(config()) -> ok. +include_lib(Config) -> + Thing = #{info => {include_lib, "code_navigation/include/code_navigation.hrl"}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(include, <<"code_navigation.hrl">>, Config), FullName), + ?assertEqual(#{from => {0, 0}, to => {0, 0}}, Range), + ok. + +-spec macro(config()) -> ok. +macro(Config) -> + Thing = #{info => {macro, 'MACRO_A'}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(src, <<"code_navigation.erl">>, Config), FullName), + ?assertEqual(#{from => {14, 0}, to => {14, 0}}, Range), + ok. + +-spec macro_included(config()) -> ok. +macro_included(Config) -> + Thing = #{info => {macro, 'INCLUDED_MACRO_A'}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(include, <<"code_navigation.hrl">>, Config), FullName), + ?assertEqual(#{from => {2, 0}, to => {2, 0}}, Range), + ok. + +%% TODO: Additional constructors for POI +%% TODO: Navigation should return POI, not range +-spec record(config()) -> ok. +record(Config) -> + Thing = #{info => {record_expr, "record_a"}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(src, <<"code_navigation.erl">>, Config), FullName), + ?assertEqual(#{from => {12, 0}, to => {12, 0}}, Range), + ok. + +-spec record_included(config()) -> ok. +record_included(Config) -> + Thing = #{info => {record_expr, "included_record_a"}}, + Path = ?config(include_path, Config), + {ok, FullName, Range} = goto_def(<<"code_navigation.erl">>, Thing, Path), + ?assertEqual(full_path(include, <<"code_navigation.hrl">>, Config), FullName), + ?assertEqual(#{from => {0, 0}, to => {0, 0}}, Range), + ok. + +%%============================================================================== +%% Internal Functions +%%============================================================================== +-spec full_path(binary(), binary(), config()) -> binary(). +full_path(Dir, FileName, Config) -> + filename:join([ ?config(app_dir, Config) + , atom_to_binary(Dir, utf8) + , FileName + ]). + +-spec goto_def(binary(), erlang_ls_poi:poi(), [string()]) -> + {ok, binary(), erlang_ls_poi:range()} | {error, any()}. +goto_def(FileName, Thing, Path) -> + erlang_ls_code_navigation:goto_definition(FileName, Thing, Path).