From 127328d22232124dd3a3177d37e33c5663ea1900 Mon Sep 17 00:00:00 2001 From: Dmitry Biletskyy Date: Tue, 12 Sep 2023 21:27:37 +0300 Subject: [PATCH 1/5] feat: go to module attribute definition --- lib/next_ls/db.ex | 12 ++ lib/next_ls/definition.ex | 3 + lib/next_ls/runtime/sidecar.ex | 79 ++++++++++++ priv/monkey/_next_ls_private_compiler.ex | 21 +++ test/next_ls/definition_test.exs | 155 +++++++++++++++++++++++ 5 files changed, 270 insertions(+) diff --git a/lib/next_ls/db.ex b/lib/next_ls/db.ex index 99724167..a416a7d1 100644 --- a/lib/next_ls/db.ex +++ b/lib/next_ls/db.ex @@ -61,6 +61,7 @@ defmodule NextLS.DB do struct: struct, file: file, defs: defs, + symbols: symbols, source: source } = symbol @@ -106,6 +107,17 @@ defmodule NextLS.DB do ) end + for {type, name, line, column} <- symbols do + __query__( + {conn, s.logger}, + ~Q""" + INSERT INTO symbols (module, file, type, name, line, 'column', source) + VALUES (?, ?, ?, ?, ?, ?, ?); + """, + [mod, file, type, name, line, column, source] + ) + end + {:noreply, s} end diff --git a/lib/next_ls/definition.ex b/lib/next_ls/definition.ex index 4dc304f0..d8db4dab 100644 --- a/lib/next_ls/definition.ex +++ b/lib/next_ls/definition.ex @@ -41,6 +41,9 @@ defmodule NextLS.Definition do "function" -> [module, identifier] + "attribute" -> + [module, identifier] + _ -> nil end diff --git a/lib/next_ls/runtime/sidecar.ex b/lib/next_ls/runtime/sidecar.ex index 64ec9626..17968679 100644 --- a/lib/next_ls/runtime/sidecar.ex +++ b/lib/next_ls/runtime/sidecar.ex @@ -15,11 +15,34 @@ defmodule NextLS.Runtime.Sidecar do end def handle_info({:tracer, payload}, state) do + "Elixir." <> module_name = to_string(payload.module) + all_symbols = parse_symbols(payload.file, module_name) + attributes = filter_attributes(all_symbols) + + payload = Map.put_new(payload, :symbols, attributes) DB.insert_symbol(state.db, payload) {:noreply, state} 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 + + {:noreply, state} + end + def handle_info({{:tracer, :reference}, payload}, state) do DB.insert_reference(state.db, payload) @@ -31,4 +54,60 @@ 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 diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index e382a96e..755a1371 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -84,6 +84,27 @@ defmodule NextLSPrivate.Tracer do :ok end + def trace({:imported_macro, meta, _module, :@, arity}, env) do + parent = parent_pid() + + Process.send( + parent, + {{:tracer, :reference, :attribute}, + %{ + meta: meta, + identifier: :@, + arity: arity, + file: env.file, + type: :attribute, + module: env.module, + source: @source + }}, + [] + ) + + :ok + end + def trace({type, meta, module, func, arity}, env) when type in [:remote_function, :remote_macro, :imported_macro] do parent = parent_pid() diff --git a/test/next_ls/definition_test.exs b/test/next_ls/definition_test.exs index 53d860e7..faba3942 100644 --- a/test/next_ls/definition_test.exs +++ b/test/next_ls/definition_test.exs @@ -403,4 +403,159 @@ defmodule NextLS.DefinitionTest do 500 end end + + describe "attribute" do + @describetag root_paths: ["my_proj"] + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + [cwd: tmp_dir] + end + + setup %{cwd: cwd} do + bar = Path.join(cwd, "my_proj/lib/bar.ex") + + File.write!(bar, """ + defmodule Bar do + @my_attr 1 + @second_attr 2 + + @spec run() :: :ok | :error + def run() do + if @my_attr == 1 do + :ok + else + {:error, @second_attr} + end + end + + defmodule Inner do + @inner_attr 123 + + def foo(a) do + if a, do: @inner_attr + end + end + + def foo() do + :nothing + end + end + + defmodule TopSecond.Some.Long.Name do + @top_second_attr "something" + + def run_second do + {:error, @top_second_attr} + end + end + """) + + [bar: bar] + end + + setup :with_lsp + + test "go to attribute definition", %{client: client, bar: bar} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + uri = uri(bar) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 6, character: 9}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, + %{ + "range" => %{ + "start" => %{ + "line" => 1, + "character" => 2 + }, + "end" => %{ + "line" => 1, + "character" => 2 + } + }, + "uri" => ^uri + }, + 500 + end + + test "go to attribute definition in second module", %{client: client, bar: bar} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + uri = uri(bar) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 30, character: 17}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, + %{ + "range" => %{ + "start" => %{ + "line" => 27, + "character" => 2 + }, + "end" => %{ + "line" => 27, + "character" => 2 + } + }, + "uri" => ^uri + }, + 500 + end + + test "go to attribute definition in inner module", %{client: client, bar: bar} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + uri = uri(bar) + + request(client, %{ + method: "textDocument/definition", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 17, character: 20}, + textDocument: %{uri: uri} + } + }) + + assert_result 4, + %{ + "range" => %{ + "start" => %{ + "line" => 14, + "character" => 4 + }, + "end" => %{ + "line" => 14, + "character" => 4 + } + }, + "uri" => ^uri + }, + 500 + end + end end From 2d9e63e487797566e6290d2f1b734562ecb98b4c Mon Sep 17 00:00:00 2001 From: Dmitry Biletskyy Date: Wed, 13 Sep 2023 13:12:32 +0300 Subject: [PATCH 2/5] fix references --- lib/next_ls.ex | 23 +++++++++++++- lib/next_ls/runtime/sidecar.ex | 2 +- test/next_ls/references_test.exs | 53 ++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 499a41bd..5df083b7 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -215,6 +215,20 @@ defmodule NextLS do [module, "alias"] ) + {:attribute, module, attribute} -> + DB.query( + database, + ~Q""" + SELECT file, start_line, end_line, start_column, end_column + FROM "references" as refs + WHERE refs.identifier = ? + AND refs.type = ? + AND refs.module = ? + AND refs.source = 'user' + """, + [attribute, "attribute", module] + ) + :unknown -> [] end @@ -266,7 +280,7 @@ defmodule NextLS do filtered_symbols = for {pid, _} <- entries, symbol <- symbols.(pid), score = fuzzy_match(symbol.name, query, case_sensitive?) do name = - if symbol.type != "defstruct" do + if symbol.type not in ["defstruct", "attribute"] do "#{symbol.type} #{symbol.name}" else "#{symbol.name}" @@ -679,6 +693,7 @@ defmodule NextLS do defp elixir_kind_to_lsp_kind("defmodule"), do: GenLSP.Enumerations.SymbolKind.module() defp elixir_kind_to_lsp_kind("defstruct"), do: GenLSP.Enumerations.SymbolKind.struct() + defp elixir_kind_to_lsp_kind("attribute"), do: GenLSP.Enumerations.SymbolKind.property() defp elixir_kind_to_lsp_kind(kind) when kind in ["def", "defp", "defmacro", "defmacrop"], do: GenLSP.Enumerations.SymbolKind.function() @@ -737,11 +752,17 @@ defmodule NextLS do [[module, "defmacro", function]] -> {:function, module, function} + [[module, "attribute", attribute]] -> + {:attribute, module, attribute} + _unknown_definition -> case DB.query(database, reference_query, [file, line, col]) do [[function, "function", module]] -> {:function, module, function} + [[attribute, "attribute", module]] -> + {:attribute, module, attribute} + [[_alias, "alias", module]] -> {:module, module} diff --git a/lib/next_ls/runtime/sidecar.ex b/lib/next_ls/runtime/sidecar.ex index 17968679..9c98c349 100644 --- a/lib/next_ls/runtime/sidecar.ex +++ b/lib/next_ls/runtime/sidecar.ex @@ -15,7 +15,7 @@ defmodule NextLS.Runtime.Sidecar do end def handle_info({:tracer, payload}, state) do - "Elixir." <> module_name = to_string(payload.module) + module_name = payload.module |> to_string() |> String.replace("Elixir.", "") all_symbols = parse_symbols(payload.file, module_name) attributes = filter_attributes(all_symbols) diff --git a/test/next_ls/references_test.exs b/test/next_ls/references_test.exs index be8ef570..e840cb02 100644 --- a/test/next_ls/references_test.exs +++ b/test/next_ls/references_test.exs @@ -33,6 +33,18 @@ defmodule NextLS.ReferencesTest do Peace.and_love() end end + + defmodule Foo do + @foo_attr 123 + + def foo_foo(a) do + {:ok, a + @foo_attr} + end + + def foo2 do + {:error, @foo_attr} + end + end """) [bar: bar, peace: peace] @@ -105,4 +117,45 @@ defmodule NextLS.ReferencesTest do } ] end + + test "list attribute references", %{client: client, bar: bar} = context do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_request(client, "client/registerCapability", fn _params -> nil end) + assert_is_ready(context, "my_proj") + assert_compiled(context, "my_proj") + assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}} + + request(client, %{ + method: "textDocument/references", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 8, character: 4}, + textDocument: %{uri: uri(bar)}, + context: %{includeDeclaration: true} + } + }) + + uri = uri(bar) + + assert_result2( + 4, + [ + %{ + "uri" => uri, + "range" => %{ + "start" => %{"line" => 11, "character" => 14}, + "end" => %{"line" => 11, "character" => 23} + } + }, + %{ + "uri" => uri, + "range" => %{ + "start" => %{"line" => 15, "character" => 13}, + "end" => %{"line" => 15, "character" => 22} + } + } + ] + ) + end end From 29f3c065c3c56aefe08da66a45485852945922c2 Mon Sep 17 00:00:00 2001 From: Dmitry Biletskyy Date: Fri, 15 Sep 2023 18:08:08 +0300 Subject: [PATCH 3/5] extract to ASTHelpers module and some small fixes --- lib/next_ls/ast_helpers.ex | 75 +++++++++++++++++++++++++++++++++ lib/next_ls/runtime/sidecar.ex | 77 ++-------------------------------- 2 files changed, 79 insertions(+), 73 deletions(-) create mode 100644 lib/next_ls/ast_helpers.ex 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 From d171420d25a8e8f3a45889bb9db5a41a24ad7eec Mon Sep 17 00:00:00 2001 From: Dmitry Biletskyy Date: Fri, 15 Sep 2023 18:19:44 +0300 Subject: [PATCH 4/5] fix spec --- lib/next_ls/ast_helpers.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/next_ls/ast_helpers.ex b/lib/next_ls/ast_helpers.ex index e2cef28d..c1d97b53 100644 --- a/lib/next_ls/ast_helpers.ex +++ b/lib/next_ls/ast_helpers.ex @@ -1,7 +1,7 @@ defmodule NextLS.ASTHelpers do @moduledoc false - @spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() + @spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() | nil def get_attribute_reference_name(file, line, column) do ast = ast_from_file(file) From d9042f3284f191de7bcd7aac0eeb2a9a374d239c Mon Sep 17 00:00:00 2001 From: Dmitry Biletskyy Date: Fri, 15 Sep 2023 18:31:21 +0300 Subject: [PATCH 5/5] fix error get_attribute_reference_name never nil --- lib/next_ls/ast_helpers.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/next_ls/ast_helpers.ex b/lib/next_ls/ast_helpers.ex index c1d97b53..7c0d1079 100644 --- a/lib/next_ls/ast_helpers.ex +++ b/lib/next_ls/ast_helpers.ex @@ -7,11 +7,11 @@ defmodule NextLS.ASTHelpers do {_ast, name} = Macro.prewalk(ast, nil, fn - {:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, name} + {:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, "@#{name}"} other, acc -> {other, acc} end) - "@#{name}" + name end @spec get_module_attributes(String.t(), module()) :: [{atom(), String.t(), integer(), integer()}]