Skip to content

Commit e10a846

Browse files
committed
Use Compiler to track macro using
1 parent 3548e2b commit e10a846

7 files changed

Lines changed: 228 additions & 72 deletions

File tree

lib/elixir_sense/core/compiler.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,27 @@ defmodule ElixirSense.Core.Compiler do
948948

949949
# Macro handling
950950

951+
defp expand_macro(
952+
meta,
953+
Kernel,
954+
:use,
955+
[target_module | _opts] = args,
956+
callback,
957+
state,
958+
env
959+
) do
960+
{expanded_module, state, env} = expand(target_module, state, env)
961+
962+
state =
963+
if is_atom(expanded_module) do
964+
State.add_use(expanded_module, state, env)
965+
else
966+
state
967+
end
968+
969+
expand_macro_callback(meta, Kernel, :use, args, callback, state, env)
970+
end
971+
951972
defp expand_macro(
952973
meta,
953974
Kernel,
@@ -2153,8 +2174,10 @@ defmodule ElixirSense.Core.Compiler do
21532174
# If expanding the macro fails, we just give up.
21542175
_kind, _payload ->
21552176
# Logger.warning(Exception.format(kind, payload, __STACKTRACE__))
2177+
uses = state.uses
21562178
# look for cursor in args
21572179
{_ast, state, _env} = expand(args, state, env)
2180+
state = %{state | uses: uses}
21582181

21592182
{{{:., meta, [module, fun]}, meta, args}, state, env}
21602183
else

lib/elixir_sense/core/compiler/state.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ defmodule ElixirSense.Core.Compiler.State do
4848
attributes: list(list(ElixirSense.Core.State.AttributeInfo.t())),
4949
scope_attributes: list(list(atom)),
5050
behaviours: %{optional(module) => [module]},
51+
uses: %{optional(module) => [module]},
5152
specs: specs_t,
5253
types: types_t,
5354
mods_funs_to_positions: mods_funs_to_positions_t,
@@ -88,6 +89,7 @@ defmodule ElixirSense.Core.Compiler.State do
8889
defstruct attributes: [[]],
8990
scope_attributes: [[]],
9091
behaviours: %{},
92+
uses: %{},
9193
specs: %{},
9294
types: %{},
9395
mods_funs_to_positions: %{},
@@ -1050,6 +1052,12 @@ defmodule ElixirSense.Core.Compiler.State do
10501052

10511053
def add_behaviour(_module, %__MODULE__{} = state, env), do: {nil, state, env}
10521054

1055+
def add_use(module, %__MODULE__{} = state, env) when is_atom(module) and not is_nil(module) do
1056+
update_in(state.uses[env.module], &Enum.uniq([module | &1 || []]))
1057+
end
1058+
1059+
def add_use(_module, %__MODULE__{} = state, _env), do: state
1060+
10531061
def register_doc(%__MODULE__{} = state, env, :moduledoc, doc_arg) do
10541062
current_module = env.module
10551063
doc_arg_formatted = format_doc_arg(doc_arg)

lib/elixir_sense/core/metadata.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule ElixirSense.Core.Metadata do
2525
specs: ElixirSense.Core.Compiler.State.specs_t(),
2626
structs: ElixirSense.Core.Compiler.State.structs_t(),
2727
records: ElixirSense.Core.Compiler.State.records_t(),
28+
uses: %{optional(module) => [module]},
2829
error: nil | term,
2930
first_alias_positions: map(),
3031
moduledoc_positions: map()
@@ -41,6 +42,7 @@ defmodule ElixirSense.Core.Metadata do
4142
specs: %{},
4243
structs: %{},
4344
records: %{},
45+
uses: %{},
4446
error: nil,
4547
first_alias_positions: %{},
4648
moduledoc_positions: %{}
@@ -62,6 +64,7 @@ defmodule ElixirSense.Core.Metadata do
6264
specs: acc.specs,
6365
structs: acc.structs,
6466
records: acc.records,
67+
uses: acc.uses,
6568
mods_funs_to_positions: acc.mods_funs_to_positions,
6669
lines_to_env: acc.lines_to_env,
6770
vars_info_per_scope_id: acc.vars_info_per_scope_id,

lib/elixir_sense/core/parser.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ defmodule ElixirSense.Core.Parser do
190190
specs: acc.specs,
191191
structs: acc.structs,
192192
records: acc.records,
193+
uses: acc.uses,
193194
mods_funs_to_positions: acc.mods_funs_to_positions,
194195
cursor_env: acc.cursor_env,
195196
closest_env: acc.closest_env,

lib/elixir_sense/providers/definition/locator.ex

Lines changed: 13 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -359,13 +359,22 @@ defmodule ElixirSense.Providers.Definition.Locator do
359359

360360
defp find_function_in_module_using_macro(mod, fun, metadata) do
361361
if Map.has_key?(metadata.mods_funs_to_positions, {mod, nil, nil}) do
362-
# Module is in the current source - use metadata.source
363-
find_used_modules_and_search_in_source(metadata.source, mod, fun)
362+
# Module is in the current source - use metadata.uses
363+
used_modules = Map.get(metadata.uses, mod, [])
364+
365+
Enum.find_value(used_modules, fn used_module ->
366+
search_in_using_macro(used_module, fun)
367+
end)
364368
else
365-
# Module is external - try to get source file
369+
# Module is external - read the file contents and parse it
366370
with file when not is_nil(file) <- get_module_source_file(mod),
367371
{:ok, content} <- File.read(file) do
368-
find_used_modules_and_search_in_source(content, mod, fun)
372+
external_metadata = Parser.parse_string(content, false, false, nil)
373+
used_modules = Map.get(external_metadata.uses, mod, [])
374+
375+
Enum.find_value(used_modules, fn used_module ->
376+
search_in_using_macro(used_module, fun)
377+
end)
369378
else
370379
_ -> nil
371380
end
@@ -380,74 +389,6 @@ defmodule ElixirSense.Providers.Definition.Locator do
380389
_, _ -> nil
381390
end
382391

383-
defp find_used_modules_and_search_in_source(source, mod, fun) do
384-
with {:ok, ast} <- Code.string_to_quoted(source) do
385-
collect_use_statements(ast, mod)
386-
|> Enum.find_value(fn used_module ->
387-
search_in_using_macro(used_module, fun)
388-
end)
389-
else
390-
_ -> nil
391-
end
392-
end
393-
394-
defp collect_use_statements(ast, mod) do
395-
target_parts = Module.split(mod)
396-
397-
{_, {_, modules}} =
398-
Macro.traverse(
399-
ast,
400-
{[], []},
401-
fn
402-
{:defmodule, _meta, [module_ast | _]} = node, {stack, modules} ->
403-
module_parts = module_ast_parts(module_ast)
404-
405-
full_parts =
406-
case stack do
407-
[parent_parts | _] when length(module_parts) == 1 ->
408-
parent_parts ++ module_parts
409-
410-
_ ->
411-
module_parts
412-
end
413-
414-
{node, {[full_parts | stack], modules}}
415-
416-
{:use, _meta, [module_ast | _]} = node, {[current_parts | _] = stack, modules} ->
417-
modules =
418-
if current_parts == target_parts do
419-
module = resolve_use_module(module_ast, %State.Env{aliases: []})
420-
if module, do: [module | modules], else: modules
421-
else
422-
modules
423-
end
424-
425-
{node, {stack, modules}}
426-
427-
node, acc ->
428-
{node, acc}
429-
end,
430-
fn
431-
{:defmodule, _meta, [_ | _]} = node, {[_ | rest], modules} ->
432-
{node, {rest, modules}}
433-
434-
node, acc ->
435-
{node, acc}
436-
end
437-
)
438-
439-
Enum.reverse(modules)
440-
end
441-
442-
defp module_ast_parts({:__aliases__, _meta, parts}) do
443-
parts
444-
|> Module.concat()
445-
|> Module.split()
446-
end
447-
448-
defp module_ast_parts(atom) when is_atom(atom), do: Module.split(atom)
449-
defp module_ast_parts(_), do: []
450-
451392
defp search_in_using_macro(module, fun) do
452393
case Location.find_mod_fun_source(module, :__using__, :any) do
453394
%Location{file: file, line: using_line, column: using_col} when not is_nil(file) ->

test/elixir_sense/providers/definition/locator_test.exs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,158 @@ defmodule ElixirSense.Providers.Definition.LocatorTest do
9595
assert location.line == 4
9696
assert location.column == 11
9797
end
98+
99+
test "finds definition when using Kernel.use qualified call" do
100+
code = """
101+
defmodule MyModule do
102+
Kernel.use(ElixirSense.Providers.Definition.LocatorTest.MyBehaviour)
103+
104+
def test do
105+
my_function()
106+
end
107+
end
108+
"""
109+
110+
{line, column} = {5, 5}
111+
112+
location = Locator.definition(code, line, column)
113+
114+
assert location != nil
115+
assert location.type == :function
116+
assert location.line == 4
117+
assert location.column == 11
118+
end
119+
120+
test "finds definition when using module alias" do
121+
code = """
122+
defmodule MyModule do
123+
alias ElixirSense.Providers.Definition.LocatorTest.MyBehaviour
124+
125+
use MyBehaviour
126+
127+
def test do
128+
my_function()
129+
end
130+
end
131+
"""
132+
133+
{line, column} = {7, 5}
134+
135+
location = Locator.definition(code, line, column)
136+
137+
assert location != nil
138+
assert location.type == :function
139+
assert location.line == 4
140+
assert location.column == 11
141+
end
142+
143+
test "finds definition when using module with custom alias" do
144+
code = """
145+
defmodule MyModule do
146+
alias ElixirSense.Providers.Definition.LocatorTest.MyBehaviour, as: MyB
147+
148+
use MyB
149+
150+
def test do
151+
my_function()
152+
end
153+
end
154+
"""
155+
156+
{line, column} = {7, 5}
157+
158+
location = Locator.definition(code, line, column)
159+
160+
assert location != nil
161+
assert location.type == :function
162+
assert location.line == 4
163+
assert location.column == 11
164+
end
165+
166+
test "no false positive when local function named use exists" do
167+
code = """
168+
defmodule MyModule do
169+
defp use(_module), do: nil
170+
171+
use ElixirSense.Providers.Definition.LocatorTest.MyBehaviour
172+
173+
def test do
174+
use(SomeModule)
175+
my_function()
176+
end
177+
end
178+
"""
179+
180+
{line, column} = {8, 5}
181+
182+
location = Locator.definition(code, line, column)
183+
184+
assert location != nil
185+
assert location.type == :function
186+
# should find the function from __using__ macro, not be confused by local use/1
187+
assert location.line == 4
188+
assert location.column == 11
189+
end
190+
191+
describe "modules in external file" do
192+
test "finds definition via external module using Kernel.use qualified call" do
193+
code = """
194+
defmodule MyModule do
195+
def test do
196+
ElixirSenseExample.ModuleUsingKernelUse.using_macro_function()
197+
end
198+
end
199+
"""
200+
201+
{line, column} = {3, 45}
202+
203+
location = Locator.definition(code, line, column)
204+
205+
assert location != nil
206+
assert location.type == :function
207+
assert location.file =~ "using_macro_example.ex"
208+
assert location.line == 4
209+
assert location.column == 11
210+
end
211+
212+
test "finds definition via external module using aliased module" do
213+
code = """
214+
defmodule MyModule do
215+
def test do
216+
ElixirSenseExample.ModuleUsingAlias.using_macro_function()
217+
end
218+
end
219+
"""
220+
221+
{line, column} = {3, 42}
222+
223+
location = Locator.definition(code, line, column)
224+
225+
assert location != nil
226+
assert location.type == :function
227+
assert location.file =~ "using_macro_example.ex"
228+
assert location.line == 4
229+
assert location.column == 11
230+
end
231+
232+
test "finds definition via external module that has other local functions" do
233+
code = """
234+
defmodule MyModule do
235+
def test do
236+
ElixirSenseExample.ModuleWithLocalUse.using_macro_function()
237+
end
238+
end
239+
"""
240+
241+
{line, column} = {3, 44}
242+
243+
location = Locator.definition(code, line, column)
244+
245+
assert location != nil
246+
assert location.type == :function
247+
assert location.file =~ "using_macro_example.ex"
248+
assert location.line == 4
249+
assert location.column == 11
250+
end
251+
end
98252
end

test/support/using_macro_example.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,29 @@ end
99
defmodule ElixirSenseExample.ModuleUsingMacroExample do
1010
use ElixirSenseExample.UsingMacroExample
1111
end
12+
13+
# Module using Kernel.use qualified call (tests reviewer concern #1)
14+
defmodule ElixirSenseExample.ModuleUsingKernelUse do
15+
Kernel.use(ElixirSenseExample.UsingMacroExample)
16+
end
17+
18+
# Module using aliased module (tests reviewer concern #2)
19+
defmodule ElixirSenseExample.ModuleUsingAlias do
20+
alias ElixirSenseExample.UsingMacroExample, as: MyMacro
21+
22+
use MyMacro
23+
end
24+
25+
# Module with local function named use (tests reviewer concern #3)
26+
# This tests that a local function named `use` doesn't confuse use tracking
27+
defmodule ElixirSenseExample.ModuleWithLocalUse do
28+
# This local function exists but won't shadow the Kernel.use macro
29+
# The test verifies that proper AST expansion correctly identifies Kernel.use
30+
defp my_use(_module), do: nil
31+
32+
use ElixirSenseExample.UsingMacroExample
33+
34+
def call_local_use do
35+
my_use(SomeModule)
36+
end
37+
end

0 commit comments

Comments
 (0)