Skip to content

Commit

Permalink
feat(definition,references): module attributes (#215)
Browse files Browse the repository at this point in the history
  • Loading branch information
biletskyy committed Sep 15, 2023
1 parent 920aaa1 commit b14a09d
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 1 deletion.
23 changes: 22 additions & 1 deletion lib/next_ls.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}

Expand Down
75 changes: 75 additions & 0 deletions lib/next_ls/ast_helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule NextLS.ASTHelpers do
@moduledoc false

@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)

{_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
12 changes: 12 additions & 0 deletions lib/next_ls/db.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ defmodule NextLS.DB do
struct: struct,
file: file,
defs: defs,
symbols: symbols,
source: source
} = symbol

Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions lib/next_ls/definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ defmodule NextLS.Definition do
"function" ->
[module, identifier]

"attribute" ->
[module, identifier]

_ ->
nil
end
Expand Down
10 changes: 10 additions & 0 deletions lib/next_ls/runtime/sidecar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule NextLS.Runtime.Sidecar do
@moduledoc false
use GenServer

alias NextLS.ASTHelpers
alias NextLS.DB

def start_link(args) do
Expand All @@ -15,11 +16,20 @@ defmodule NextLS.Runtime.Sidecar do
end

def handle_info({:tracer, payload}, state) do
attributes = ASTHelpers.get_module_attributes(payload.file, payload.module)
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
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

def handle_info({{:tracer, :reference}, payload}, state) do
DB.insert_reference(state.db, payload)

Expand Down
21 changes: 21 additions & 0 deletions priv/monkey/_next_ls_private_compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
155 changes: 155 additions & 0 deletions test/next_ls/definition_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit b14a09d

Please sign in to comment.