From c3d91eda481202349eb1d7a8d5ab099636eb7957 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 4 Jul 2026 08:37:51 +0200 Subject: [PATCH] Fix multi-line use site detection --- .../providers/definition/locator.ex | 47 +++++++++-- .../definition/locator_using_macro_test.exs | 79 +++++++++++++++++++ test/support/using_macro_example.ex | 40 ++++++++++ 3 files changed, 159 insertions(+), 7 deletions(-) diff --git a/lib/elixir_sense/providers/definition/locator.ex b/lib/elixir_sense/providers/definition/locator.ex index cc172527..fc0b569c 100644 --- a/lib/elixir_sense/providers/definition/locator.ex +++ b/lib/elixir_sense/providers/definition/locator.ex @@ -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 diff --git a/test/elixir_sense/providers/definition/locator_using_macro_test.exs b/test/elixir_sense/providers/definition/locator_using_macro_test.exs index 452b0e60..4e541529 100644 --- a/test/elixir_sense/providers/definition/locator_using_macro_test.exs +++ b/test/elixir_sense/providers/definition/locator_using_macro_test.exs @@ -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. diff --git a/test/support/using_macro_example.ex b/test/support/using_macro_example.ex index 8e9a7a60..97641e4b 100644 --- a/test/support/using_macro_example.ex +++ b/test/support/using_macro_example.ex @@ -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