Skip to content

Commit

Permalink
Made aliases better handle the __aliases__ special form (#393)
Browse files Browse the repository at this point in the history
* Made aliases better handle the __aliases__ special form

The __aliases__ special form defines three types of aliases, and we
were not handling one of them properly. This was made apparent to us
in issue #378, where the module defined via `defmodule
__MODULE__.Child` was not handled properly. This change fixes that
issue, and should handle other aliases as well.

Fixes #378
  • Loading branch information
scohen authored Oct 2, 2023
1 parent 8a4b369 commit acd6f1d
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 30 deletions.
51 changes: 42 additions & 9 deletions apps/common/lib/lexical/ast.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion apps/common/lib/lexical/ast/aliases.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ defmodule Lexical.Ast.Aliases do
end

defmodule Reducer do
alias Lexical.Ast
defstruct scopes: []

def new do
Expand Down Expand Up @@ -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 =
Expand Down
24 changes: 21 additions & 3 deletions apps/common/lib/lexical/ast/module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions apps/common/test/lexical/ast/aliases_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions apps/common/test/lexical/ast_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,28 +152,12 @@ 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(
Position.new(document, line, column),
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
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit acd6f1d

Please sign in to comment.