Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions lib/elixir_sense/providers/definition/locator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -361,19 +361,52 @@ defmodule ElixirSense.Providers.Definition.Locator do
# Does the recorded definition sit on a `use Foo` / `Kernel.use(Foo)` line?
defp use_site?(location, metadata, line) do
with source when is_binary(source) <- location_source(location, metadata),
text when is_binary(text) <- Enum.at(Source.split_lines(source), line - 1),
{:ok, ast} <- Code.string_to_quoted(text) do
case ast do
{:use, _, [_ | _]} -> true
{{:., _, [{:__aliases__, _, [:Kernel]}, :use]}, _, [_ | _]} -> true
{{:., _, [Kernel, :use]}, _, [_ | _]} -> true
_ -> false
text when is_binary(text) <- Enum.at(Source.split_lines(source), line - 1) do
case Code.string_to_quoted(text, emit_warnings: false) do
{:ok, ast} ->
use_ast?(ast)

{:error, _} ->
source
|> Source.text_before(line, String.length(text) + 1)
|> use_fragment?(line)
end
else
_ -> false
end
end

defp use_fragment?(prefix, line) do
case Code.Fragment.container_cursor_to_quoted(prefix, columns: true) do
{:ok, ast} -> use_ast_on_line?(ast, line)
_ -> false
end
end

defp use_ast_on_line?(ast, line) do
{_ast, found?} =
Macro.prewalk(ast, false, fn
node, true -> {node, true}
node, false -> {node, use_ast?(node, line)}
end)

found?
end

defp use_ast?({:use, meta, [_ | _]}, line), do: meta[:line] == line

defp use_ast?({{:., _, [{:__aliases__, _, [:Kernel]}, :use]}, meta, [_ | _]}, line),
do: meta[:line] == line

defp use_ast?({{:., _, [Kernel, :use]}, meta, [_ | _]}, line), do: meta[:line] == line

defp use_ast?(_ast, _line), do: false

defp use_ast?({:use, _, [_ | _]}), do: true
defp use_ast?({{:., _, [{:__aliases__, _, [:Kernel]}, :use]}, _, [_ | _]}), do: true
defp use_ast?({{:., _, [Kernel, :use]}, _, [_ | _]}), do: true
defp use_ast?(_ast), do: false

# Modules `mod` uses, resolved at expansion time. For the current buffer they
# are in `metadata.uses`; for an external module we parse its source (only
# reached once we already know the position is a `use` site, so this is not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,85 @@ defmodule ElixirSense.Providers.Definition.LocatorUsingMacroTest do
assert location.column == 19
end

test "finds definition injected through a transitive use chain (current source)" do
code = """
defmodule MyModule do
use ElixirSenseExample.UsingMacroOuter

def test do
nested_using_function()
end
end
"""

location = Locator.definition(code, 5, 5)

assert location != nil
assert location.type == :function
assert location.file =~ "using_macro_example.ex"
# `def nested_using_function` lives in UsingMacroInner.__using__, two hops
# away (MyModule -> UsingMacroOuter -> UsingMacroInner)
assert location.line == 72
assert location.column == 11
end

test "finds definition through a transitive use chain via external module" do
code = """
defmodule MyModule do
def test do
ElixirSenseExample.ModuleUsingNested.nested_using_function()
end
end
"""

location = Locator.definition(code, 3, 48)

assert location != nil
assert location.type == :function
assert location.file =~ "using_macro_example.ex"
assert location.line == 72
assert location.column == 11
end

test "finds definition when the use spans multiple lines (current source)" do
code = """
defmodule MyModule do
use ElixirSenseExample.UsingMacroWithOpts,
some: :opt

def test do
opted_using_function()
end
end
"""

location = Locator.definition(code, 6, 5)

assert location != nil
assert location.type == :function
assert location.file =~ "using_macro_example.ex"
assert location.line == 95
assert location.column == 11
end

test "finds definition when the use spans multiple lines (external module)" do
code = """
defmodule MyModule do
def test do
ElixirSenseExample.ModuleUsingWithOpts.opted_using_function()
end
end
"""

location = Locator.definition(code, 3, 45)

assert location != nil
assert location.type == :function
assert location.file =~ "using_macro_example.ex"
assert location.line == 95
assert location.column == 11
end

test "finds definition injected via defguard" do
# A remote guard must be called from a guard context (and the module
# required) to resolve as a macro — unrelated to the using-macro search.
Expand Down
40 changes: 40 additions & 0 deletions test/support/using_macro_example.ex
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,43 @@ end
defmodule ElixirSenseExample.ModuleUsingOtherForms do
use ElixirSenseExample.UsingMacroOtherForms
end

# Transitive `use` chain: the injected function is defined several `use` hops
# away. `UsingMacroOuter.__using__` itself does `use UsingMacroInner`, which is
# where `nested_using_function` actually lives (mirrors MyApp.Repo ->
# AshPostgres.Repo -> Ecto.Repo). Go-to-definition must follow the chain.
defmodule ElixirSenseExample.UsingMacroInner do
defmacro __using__(_opts) do
quote do
def nested_using_function(), do: :nested
end
end
end

defmodule ElixirSenseExample.UsingMacroOuter do
defmacro __using__(_opts) do
quote do
use ElixirSenseExample.UsingMacroInner
end
end
end

defmodule ElixirSenseExample.ModuleUsingNested do
use ElixirSenseExample.UsingMacroOuter
end

# Multi-line `use` with options. The recorded definition line is the `use`
# keyword line (`use ...,`), which is not valid Elixir on its own, so the
# use-site detection must not rely on parsing that line in isolation.
defmodule ElixirSenseExample.UsingMacroWithOpts do
defmacro __using__(_opts) do
quote do
def opted_using_function(), do: :ok
end
end
end

defmodule ElixirSenseExample.ModuleUsingWithOpts do
use ElixirSenseExample.UsingMacroWithOpts,
some: :opt
end
Loading