diff --git a/lib/next_ls/ast_helpers.ex b/lib/next_ls/ast_helpers.ex new file mode 100644 index 00000000..e2cef28d --- /dev/null +++ b/lib/next_ls/ast_helpers.ex @@ -0,0 +1,75 @@ +defmodule NextLS.ASTHelpers do + @moduledoc false + + @spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() + def get_attribute_reference_name(file, line, column) do + ast = ast_from_file(file) + + {_ast, name} = + Macro.prewalk(ast, nil, fn + {:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, name} + other, acc -> {other, acc} + end) + + "@#{name}" + end + + @spec get_module_attributes(String.t(), module()) :: [{atom(), String.t(), integer(), integer()}] + def get_module_attributes(file, module) do + reserved_attributes = Module.reserved_attributes() + + symbols = parse_symbols(file, module) + + Enum.filter(symbols, fn + {:attribute, "@" <> name, _, _} -> + not Map.has_key?(reserved_attributes, String.to_atom(name)) + + _other -> + false + end) + end + + defp parse_symbols(file, module) do + ast = ast_from_file(file) + + {_ast, %{symbols: symbols}} = + Macro.traverse(ast, %{modules: [], symbols: []}, &prewalk/2, &postwalk(&1, &2, module)) + + symbols + end + + # add module name to modules stack on enter + defp prewalk({:defmodule, _, [{:__aliases__, _, module_name_atoms} | _]} = ast, acc) do + modules = [module_name_atoms | acc.modules] + {ast, %{acc | modules: modules}} + end + + defp prewalk(ast, acc), do: {ast, acc} + + defp postwalk({:@, meta, [{name, _, args}]} = ast, acc, module) when is_list(args) do + ast_module = + acc.modules + |> Enum.reverse() + |> List.flatten() + |> Module.concat() + + if module == ast_module do + symbols = [{:attribute, "@#{name}", meta[:line], meta[:column]} | acc.symbols] + {ast, %{acc | symbols: symbols}} + else + {ast, acc} + end + end + + # remove module name from modules stack on exit + defp postwalk({:defmodule, _, [{:__aliases__, _, _modules} | _]} = ast, acc, _module) do + [_exit_mudule | modules] = acc.modules + {ast, %{acc | modules: modules}} + end + + defp postwalk(ast, acc, _module), do: {ast, acc} + + defp ast_from_file(file) do + file |> File.read!() |> Code.string_to_quoted!(columns: true) + end +end diff --git a/lib/next_ls/runtime/sidecar.ex b/lib/next_ls/runtime/sidecar.ex index 9c98c349..00606305 100644 --- a/lib/next_ls/runtime/sidecar.ex +++ b/lib/next_ls/runtime/sidecar.ex @@ -2,6 +2,7 @@ defmodule NextLS.Runtime.Sidecar do @moduledoc false use GenServer + alias NextLS.ASTHelpers alias NextLS.DB def start_link(args) do @@ -15,10 +16,7 @@ defmodule NextLS.Runtime.Sidecar do end def handle_info({:tracer, payload}, state) do - module_name = payload.module |> to_string() |> String.replace("Elixir.", "") - all_symbols = parse_symbols(payload.file, module_name) - attributes = filter_attributes(all_symbols) - + attributes = ASTHelpers.get_module_attributes(payload.file, payload.module) payload = Map.put_new(payload, :symbols, attributes) DB.insert_symbol(state.db, payload) @@ -26,19 +24,8 @@ defmodule NextLS.Runtime.Sidecar do end def handle_info({{:tracer, :reference, :attribute}, payload}, state) do - ast = payload.file |> File.read!() |> Code.string_to_quoted!(columns: true) - location = [line: payload.meta[:line], column: payload.meta[:column]] - - {_ast, name} = - Macro.prewalk(ast, nil, fn - {:@, ^location, [{name, _meta, nil}]} = ast, _acc -> {ast, name} - other, acc -> {other, acc} - end) - - if name do - payload = %{payload | identifier: "@#{name}"} - DB.insert_reference(state.db, payload) - end + name = ASTHelpers.get_attribute_reference_name(payload.file, payload.meta[:line], payload.meta[:column]) + if name, do: DB.insert_reference(state.db, %{payload | identifier: name}) {:noreply, state} end @@ -54,60 +41,4 @@ defmodule NextLS.Runtime.Sidecar do {:noreply, state} end - - defp filter_attributes(symbols) do - symbols - |> Enum.filter(&match?({:attribute, _, _, _}, &1)) - |> Enum.reject(fn {_, "@" <> name, _, _} -> - Map.has_key?(Module.reserved_attributes(), String.to_atom(name)) - end) - end - - defp parse_symbols(file, module) do - ast = file |> File.read!() |> Code.string_to_quoted!(columns: true) - - {_ast, %{symbols: symbols}} = - Macro.traverse(ast, %{modules: [], symbols: []}, &prewalk/2, &postwalk(&1, &2, module)) - - symbols - end - - # add module name to modules stack on enter - defp prewalk({:defmodule, _, [{:__aliases__, _, modules} | _]} = ast, acc) do - modules_string = - modules - |> Enum.map(&Atom.to_string/1) - |> Enum.intersperse(".") - |> List.to_string() - - modules = [modules_string | acc.modules] - - {ast, %{acc | modules: modules}} - end - - defp prewalk(ast, acc), do: {ast, acc} - - defp postwalk({:@, meta, [{name, _, args}]} = ast, acc, module) when is_list(args) do - # get current module for this node - ast_module = - acc.modules - |> Enum.reverse() - |> Enum.intersperse(".") - |> List.to_string() - - if module == ast_module do - symbols = [{:attribute, "@#{name}", meta[:line], meta[:column]} | acc.symbols] - {ast, %{acc | symbols: symbols}} - else - {ast, acc} - end - end - - # remove module name from modules stack on exit - defp postwalk({:defmodule, _, [{:__aliases__, _, _modules} | _]} = ast, acc, _module) do - [_exit_mudule | modules] = acc.modules - {ast, %{acc | modules: modules}} - end - - defp postwalk(ast, acc, _module), do: {ast, acc} end