diff --git a/apps/common/lib/lexical/ast.ex b/apps/common/lib/lexical/ast.ex index 85027d308..2a47e09c5 100644 --- a/apps/common/lib/lexical/ast.ex +++ b/apps/common/lib/lexical/ast.ex @@ -463,17 +463,18 @@ defmodule Lexical.Ast do |> expand_aliases(document, quoted_document, position) end - def expand_aliases( - [first | rest] = segments, - %Document{} = document, - quoted_document, - %Position{} = position - ) do + def expand_aliases(segments, %Document{} = document, quoted_document, %Position{} = position) + when is_list(segments) do with {:ok, aliases_mapping} <- Aliases.at(document, quoted_document, position), - {:ok, resolved} <- Map.fetch(aliases_mapping, first) do - {:ok, Module.concat([resolved | rest])} + {:ok, resolved} <- resolve_alias(segments, aliases_mapping) do + {:ok, Module.concat(resolved)} else - _ -> {:ok, Module.concat(segments)} + _ -> + if Enum.all?(segments, &is_atom/1) do + {:ok, Module.concat(segments)} + else + :error + end end end @@ -482,7 +483,39 @@ defmodule Lexical.Ast do :error end + # Expands aliases given the rules in the special form + # https://hexdocs.pm/elixir/1.13.4/Kernel.SpecialForms.html#__aliases__/1 + def reify_alias(current_module, [:"Elixir" | _] = reified) do + [current_module | reified] + end + + def reify_alias(current_module, [:__MODULE__ | rest]) do + [current_module | rest] + end + + def reify_alias(current_module, [atom | _rest] = reified) when is_atom(atom) do + [current_module | reified] + end + + def reify_alias(current_module, [unreified | rest]) do + env = %Macro.Env{module: current_module} + reified = Macro.expand(unreified, env) + + [reified | rest] + end + # private + defp resolve_alias([first | _] = segments, aliases_mapping) when is_tuple(first) do + with {:ok, current_module} <- Map.fetch(aliases_mapping, :__MODULE__) do + {:ok, reify_alias(current_module, segments)} + end + end + + defp resolve_alias([first | rest], aliases_mapping) do + with {:ok, resolved} <- Map.fetch(aliases_mapping, first) do + {:ok, [resolved | rest]} + end + end defp do_string_to_quoted(string) when is_binary(string) do Code.string_to_quoted(string, diff --git a/apps/common/lib/lexical/ast/aliases.ex b/apps/common/lib/lexical/ast/aliases.ex index 5b1765e69..e05184bf4 100644 --- a/apps/common/lib/lexical/ast/aliases.ex +++ b/apps/common/lib/lexical/ast/aliases.ex @@ -71,6 +71,7 @@ defmodule Lexical.Ast.Aliases do end defmodule Reducer do + alias Lexical.Ast defstruct scopes: [] def new do @@ -106,7 +107,7 @@ defmodule Lexical.Ast.Aliases do module_name current_module -> - Module.split(current_module) ++ module_name + Ast.reify_alias(current_module, module_name) end current_module_alias = diff --git a/apps/common/lib/lexical/ast/module.ex b/apps/common/lib/lexical/ast/module.ex index fe37c1d2e..20948809a 100644 --- a/apps/common/lib/lexical/ast/module.ex +++ b/apps/common/lib/lexical/ast/module.ex @@ -6,8 +6,26 @@ defmodule Lexical.Ast.Module do @doc """ Formats a module name as a string. """ - @spec name(module()) :: String.t() - def name(module) when is_atom(module) do - module |> to_string() |> String.replace_prefix("Elixir.", "") + @spec name(module() | Macro.t() | String.t()) :: String.t() + def name([{:__MODULE__, _, _} | rest]) do + [__MODULE__ | rest] + |> Module.concat() + |> name() + end + + def name(module_name) when is_list(module_name) do + module_name + |> Module.concat() + |> name() + end + + def name(module_name) when is_binary(module_name) do + module_name + end + + def name(module_name) when is_atom(module_name) do + module_name + |> inspect() + |> name() end end diff --git a/apps/common/test/lexical/ast/aliases_test.exs b/apps/common/test/lexical/ast/aliases_test.exs index f5d49b648..4dff32b3e 100644 --- a/apps/common/test/lexical/ast/aliases_test.exs +++ b/apps/common/test/lexical/ast/aliases_test.exs @@ -205,6 +205,20 @@ defmodule Lexical.Ast.AliasesTest do assert aliases[:Child] == Grandparent.Parent.Child assert aliases[:__MODULE__] == Grandparent.Parent.Child end + + test "with a child that has an explicit parent" do + {:ok, aliases} = + ~q[ + defmodule Parent do + defmodule __MODULE__.Child do + | + end + end + ] + |> aliases_at_cursor() + + assert aliases[:__MODULE__] == Parent.Child + end end describe "alias scopes" do diff --git a/apps/common/test/lexical/ast_test.exs b/apps/common/test/lexical/ast_test.exs index a8030b1be..23f12ec05 100644 --- a/apps/common/test/lexical/ast_test.exs +++ b/apps/common/test/lexical/ast_test.exs @@ -255,6 +255,23 @@ defmodule Lexical.AstTest do end end + describe "expand_aliases/4" do + test "works with __MODULE__ aliases" do + {position, document} = + ~q[ + defmodule Parent do + defmodule __MODULE__.Child do + | + end + end + ] + |> pop_cursor(as: :document) + + assert {:ok, Parent.Child} = + Ast.expand_aliases([quote(do: __MODULE__), nil], document, position) + end + end + defp ast(s) do case Ast.from(s) do {:ok, {:__block__, _, [node]}} -> node diff --git a/apps/remote_control/lib/lexical/remote_control/search/indexer/extractors/module.ex b/apps/remote_control/lib/lexical/remote_control/search/indexer/extractors/module.ex index a72e4ef00..504edf3b7 100644 --- a/apps/remote_control/lib/lexical/remote_control/search/indexer/extractors/module.ex +++ b/apps/remote_control/lib/lexical/remote_control/search/indexer/extractors/module.ex @@ -152,7 +152,7 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.Module do defp to_range(%Document{} = document, module_name, {line, column}) do module_length = module_name - |> module_name() + |> Ast.Module.name() |> String.length() Range.new( @@ -160,20 +160,4 @@ defmodule Lexical.RemoteControl.Search.Indexer.Extractors.Module do Position.new(document, line, column + module_length) ) end - - defp module_name(module_name) when is_list(module_name) do - module_name - |> Module.concat() - |> module_name() - end - - defp module_name(module_name) when is_binary(module_name) do - module_name - end - - defp module_name(module_name) when is_atom(module_name) do - module_name - |> inspect() - |> module_name() - end end diff --git a/apps/remote_control/test/lexical/remote_control/search/indexer/source_test.exs b/apps/remote_control/test/lexical/remote_control/search/indexer/source_test.exs index ea7601ac6..59ccbad85 100644 --- a/apps/remote_control/test/lexical/remote_control/search/indexer/source_test.exs +++ b/apps/remote_control/test/lexical/remote_control/search/indexer/source_test.exs @@ -387,5 +387,24 @@ defmodule Lexical.RemoteControl.Search.Indexer.SourceTest do assert child_alias.subtype == :reference assert child_alias.subject == Something.Else.Other end + + test "works with __MODULE__" do + {:ok, [parent, child], _} = + ~q[ + defmodule Parent do + defmodule __MODULE__.Child do + end + end + ] + |> index() + + assert parent.parent == :root + assert parent.type == :module + assert parent.subtype == :definition + + assert child.parent == parent.ref + assert child.type == :module + assert child.subtype == :definition + end end end