Skip to content

Commit b14a09d

Browse files
authored
feat(definition,references): module attributes (#215)
1 parent 920aaa1 commit b14a09d

File tree

8 files changed

+351
-1
lines changed

8 files changed

+351
-1
lines changed

lib/next_ls.ex

+22-1
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,20 @@ defmodule NextLS do
215215
[module, "alias"]
216216
)
217217

218+
{:attribute, module, attribute} ->
219+
DB.query(
220+
database,
221+
~Q"""
222+
SELECT file, start_line, end_line, start_column, end_column
223+
FROM "references" as refs
224+
WHERE refs.identifier = ?
225+
AND refs.type = ?
226+
AND refs.module = ?
227+
AND refs.source = 'user'
228+
""",
229+
[attribute, "attribute", module]
230+
)
231+
218232
:unknown ->
219233
[]
220234
end
@@ -266,7 +280,7 @@ defmodule NextLS do
266280
filtered_symbols =
267281
for {pid, _} <- entries, symbol <- symbols.(pid), score = fuzzy_match(symbol.name, query, case_sensitive?) do
268282
name =
269-
if symbol.type != "defstruct" do
283+
if symbol.type not in ["defstruct", "attribute"] do
270284
"#{symbol.type} #{symbol.name}"
271285
else
272286
"#{symbol.name}"
@@ -679,6 +693,7 @@ defmodule NextLS do
679693

680694
defp elixir_kind_to_lsp_kind("defmodule"), do: GenLSP.Enumerations.SymbolKind.module()
681695
defp elixir_kind_to_lsp_kind("defstruct"), do: GenLSP.Enumerations.SymbolKind.struct()
696+
defp elixir_kind_to_lsp_kind("attribute"), do: GenLSP.Enumerations.SymbolKind.property()
682697

683698
defp elixir_kind_to_lsp_kind(kind) when kind in ["def", "defp", "defmacro", "defmacrop"],
684699
do: GenLSP.Enumerations.SymbolKind.function()
@@ -737,11 +752,17 @@ defmodule NextLS do
737752
[[module, "defmacro", function]] ->
738753
{:function, module, function}
739754

755+
[[module, "attribute", attribute]] ->
756+
{:attribute, module, attribute}
757+
740758
_unknown_definition ->
741759
case DB.query(database, reference_query, [file, line, col]) do
742760
[[function, "function", module]] ->
743761
{:function, module, function}
744762

763+
[[attribute, "attribute", module]] ->
764+
{:attribute, module, attribute}
765+
745766
[[_alias, "alias", module]] ->
746767
{:module, module}
747768

lib/next_ls/ast_helpers.ex

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
defmodule NextLS.ASTHelpers do
2+
@moduledoc false
3+
4+
@spec get_attribute_reference_name(String.t(), integer(), integer()) :: String.t() | nil
5+
def get_attribute_reference_name(file, line, column) do
6+
ast = ast_from_file(file)
7+
8+
{_ast, name} =
9+
Macro.prewalk(ast, nil, fn
10+
{:@, [line: ^line, column: ^column], [{name, _meta, nil}]} = ast, _acc -> {ast, "@#{name}"}
11+
other, acc -> {other, acc}
12+
end)
13+
14+
name
15+
end
16+
17+
@spec get_module_attributes(String.t(), module()) :: [{atom(), String.t(), integer(), integer()}]
18+
def get_module_attributes(file, module) do
19+
reserved_attributes = Module.reserved_attributes()
20+
21+
symbols = parse_symbols(file, module)
22+
23+
Enum.filter(symbols, fn
24+
{:attribute, "@" <> name, _, _} ->
25+
not Map.has_key?(reserved_attributes, String.to_atom(name))
26+
27+
_other ->
28+
false
29+
end)
30+
end
31+
32+
defp parse_symbols(file, module) do
33+
ast = ast_from_file(file)
34+
35+
{_ast, %{symbols: symbols}} =
36+
Macro.traverse(ast, %{modules: [], symbols: []}, &prewalk/2, &postwalk(&1, &2, module))
37+
38+
symbols
39+
end
40+
41+
# add module name to modules stack on enter
42+
defp prewalk({:defmodule, _, [{:__aliases__, _, module_name_atoms} | _]} = ast, acc) do
43+
modules = [module_name_atoms | acc.modules]
44+
{ast, %{acc | modules: modules}}
45+
end
46+
47+
defp prewalk(ast, acc), do: {ast, acc}
48+
49+
defp postwalk({:@, meta, [{name, _, args}]} = ast, acc, module) when is_list(args) do
50+
ast_module =
51+
acc.modules
52+
|> Enum.reverse()
53+
|> List.flatten()
54+
|> Module.concat()
55+
56+
if module == ast_module do
57+
symbols = [{:attribute, "@#{name}", meta[:line], meta[:column]} | acc.symbols]
58+
{ast, %{acc | symbols: symbols}}
59+
else
60+
{ast, acc}
61+
end
62+
end
63+
64+
# remove module name from modules stack on exit
65+
defp postwalk({:defmodule, _, [{:__aliases__, _, _modules} | _]} = ast, acc, _module) do
66+
[_exit_mudule | modules] = acc.modules
67+
{ast, %{acc | modules: modules}}
68+
end
69+
70+
defp postwalk(ast, acc, _module), do: {ast, acc}
71+
72+
defp ast_from_file(file) do
73+
file |> File.read!() |> Code.string_to_quoted!(columns: true)
74+
end
75+
end

lib/next_ls/db.ex

+12
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ defmodule NextLS.DB do
6161
struct: struct,
6262
file: file,
6363
defs: defs,
64+
symbols: symbols,
6465
source: source
6566
} = symbol
6667

@@ -106,6 +107,17 @@ defmodule NextLS.DB do
106107
)
107108
end
108109

110+
for {type, name, line, column} <- symbols do
111+
__query__(
112+
{conn, s.logger},
113+
~Q"""
114+
INSERT INTO symbols (module, file, type, name, line, 'column', source)
115+
VALUES (?, ?, ?, ?, ?, ?, ?);
116+
""",
117+
[mod, file, type, name, line, column, source]
118+
)
119+
end
120+
109121
{:noreply, s}
110122
end
111123

lib/next_ls/definition.ex

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ defmodule NextLS.Definition do
4141
"function" ->
4242
[module, identifier]
4343

44+
"attribute" ->
45+
[module, identifier]
46+
4447
_ ->
4548
nil
4649
end

lib/next_ls/runtime/sidecar.ex

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule NextLS.Runtime.Sidecar do
22
@moduledoc false
33
use GenServer
44

5+
alias NextLS.ASTHelpers
56
alias NextLS.DB
67

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

1718
def handle_info({:tracer, payload}, state) do
19+
attributes = ASTHelpers.get_module_attributes(payload.file, payload.module)
20+
payload = Map.put_new(payload, :symbols, attributes)
1821
DB.insert_symbol(state.db, payload)
1922

2023
{:noreply, state}
2124
end
2225

26+
def handle_info({{:tracer, :reference, :attribute}, payload}, state) do
27+
name = ASTHelpers.get_attribute_reference_name(payload.file, payload.meta[:line], payload.meta[:column])
28+
if name, do: DB.insert_reference(state.db, %{payload | identifier: name})
29+
30+
{:noreply, state}
31+
end
32+
2333
def handle_info({{:tracer, :reference}, payload}, state) do
2434
DB.insert_reference(state.db, payload)
2535

priv/monkey/_next_ls_private_compiler.ex

+21
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,27 @@ defmodule NextLSPrivate.Tracer do
8484
:ok
8585
end
8686

87+
def trace({:imported_macro, meta, _module, :@, arity}, env) do
88+
parent = parent_pid()
89+
90+
Process.send(
91+
parent,
92+
{{:tracer, :reference, :attribute},
93+
%{
94+
meta: meta,
95+
identifier: :@,
96+
arity: arity,
97+
file: env.file,
98+
type: :attribute,
99+
module: env.module,
100+
source: @source
101+
}},
102+
[]
103+
)
104+
105+
:ok
106+
end
107+
87108
def trace({type, meta, module, func, arity}, env) when type in [:remote_function, :remote_macro, :imported_macro] do
88109
parent = parent_pid()
89110

test/next_ls/definition_test.exs

+155
Original file line numberDiff line numberDiff line change
@@ -403,4 +403,159 @@ defmodule NextLS.DefinitionTest do
403403
500
404404
end
405405
end
406+
407+
describe "attribute" do
408+
@describetag root_paths: ["my_proj"]
409+
setup %{tmp_dir: tmp_dir} do
410+
File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib"))
411+
File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs())
412+
[cwd: tmp_dir]
413+
end
414+
415+
setup %{cwd: cwd} do
416+
bar = Path.join(cwd, "my_proj/lib/bar.ex")
417+
418+
File.write!(bar, """
419+
defmodule Bar do
420+
@my_attr 1
421+
@second_attr 2
422+
423+
@spec run() :: :ok | :error
424+
def run() do
425+
if @my_attr == 1 do
426+
:ok
427+
else
428+
{:error, @second_attr}
429+
end
430+
end
431+
432+
defmodule Inner do
433+
@inner_attr 123
434+
435+
def foo(a) do
436+
if a, do: @inner_attr
437+
end
438+
end
439+
440+
def foo() do
441+
:nothing
442+
end
443+
end
444+
445+
defmodule TopSecond.Some.Long.Name do
446+
@top_second_attr "something"
447+
448+
def run_second do
449+
{:error, @top_second_attr}
450+
end
451+
end
452+
""")
453+
454+
[bar: bar]
455+
end
456+
457+
setup :with_lsp
458+
459+
test "go to attribute definition", %{client: client, bar: bar} do
460+
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})
461+
assert_request(client, "client/registerCapability", fn _params -> nil end)
462+
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}
463+
464+
uri = uri(bar)
465+
466+
request(client, %{
467+
method: "textDocument/definition",
468+
id: 4,
469+
jsonrpc: "2.0",
470+
params: %{
471+
position: %{line: 6, character: 9},
472+
textDocument: %{uri: uri}
473+
}
474+
})
475+
476+
assert_result 4,
477+
%{
478+
"range" => %{
479+
"start" => %{
480+
"line" => 1,
481+
"character" => 2
482+
},
483+
"end" => %{
484+
"line" => 1,
485+
"character" => 2
486+
}
487+
},
488+
"uri" => ^uri
489+
},
490+
500
491+
end
492+
493+
test "go to attribute definition in second module", %{client: client, bar: bar} do
494+
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})
495+
assert_request(client, "client/registerCapability", fn _params -> nil end)
496+
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}
497+
498+
uri = uri(bar)
499+
500+
request(client, %{
501+
method: "textDocument/definition",
502+
id: 4,
503+
jsonrpc: "2.0",
504+
params: %{
505+
position: %{line: 30, character: 17},
506+
textDocument: %{uri: uri}
507+
}
508+
})
509+
510+
assert_result 4,
511+
%{
512+
"range" => %{
513+
"start" => %{
514+
"line" => 27,
515+
"character" => 2
516+
},
517+
"end" => %{
518+
"line" => 27,
519+
"character" => 2
520+
}
521+
},
522+
"uri" => ^uri
523+
},
524+
500
525+
end
526+
527+
test "go to attribute definition in inner module", %{client: client, bar: bar} do
528+
assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}})
529+
assert_request(client, "client/registerCapability", fn _params -> nil end)
530+
assert_notification "$/progress", %{"value" => %{"kind" => "end", "message" => "Finished indexing!"}}
531+
532+
uri = uri(bar)
533+
534+
request(client, %{
535+
method: "textDocument/definition",
536+
id: 4,
537+
jsonrpc: "2.0",
538+
params: %{
539+
position: %{line: 17, character: 20},
540+
textDocument: %{uri: uri}
541+
}
542+
})
543+
544+
assert_result 4,
545+
%{
546+
"range" => %{
547+
"start" => %{
548+
"line" => 14,
549+
"character" => 4
550+
},
551+
"end" => %{
552+
"line" => 14,
553+
"character" => 4
554+
}
555+
},
556+
"uri" => ^uri
557+
},
558+
500
559+
end
560+
end
406561
end

0 commit comments

Comments
 (0)