defmodule Recode.Task.AliasExpansion do
@moduledoc """
Multi aliases makes module uses harder to search for in large code bases.
# preferred
alias Module.Foo
alias Module.Bar
# not preferred
alias Module.{Foo, Bar}
This task rewrites the code when `mix recode` runs with `autocorrect: true`.
"""
use Recode.Task, correct: true, check: true
alias Recode.Issue
alias Recode.Task.AliasExpansion
alias Rewrite.Source
alias Sourceror.Zipper
@impl Recode.Task
def run(source, opts) do
{zipper, issues} =
source
|> Source.ast()
|> Zipper.zip()
|> Zipper.traverse([], fn zipper, issues ->
expand_alias(zipper, issues, opts[:autocorrect])
end)
case opts[:autocorrect] do
true ->
Source.update(source, AliasExpansion, ast: Zipper.root(zipper))
false ->
Source.add_issues(source, issues)
end
end
defp expand_alias({{:alias, _meta, _args} = ast, _zipper_meta} = zipper, issues, true) do
zipper =
case extract(ast) do
{:ok, {base, segments, alias_meta, call_meta}} ->
segments
|> segments_to_alias(base)
|> put_leading_comments(alias_meta)
|> put_trailing_comments(call_meta)
|> insert(zipper)
:error ->
zipper
end
{zipper, issues}
end
defp expand_alias({{:alias, meta, _args} = ast, _zipper_meta} = zipper, issues, false) do
issues =
case extract(ast) do
{:ok, _data} ->
message = "Avoid multi aliases."
issue = Issue.new(AliasExpansion, message, meta)
[issue | issues]
:error ->
issues
end
{zipper, issues}
end
defp expand_alias(zipper, issues, _autocorrect), do: {zipper, issues}
defp extract(tree) do
case tree do
{:alias, alias_meta, [{{:., _meta, [base, :{}]}, call_meta, segments}]} ->
{:ok, {base, segments, alias_meta, call_meta}}
_tree ->
:error
end
end
defp insert(aliases, zipper) do
aliases
|> Enum.reduce(zipper, fn alias, zip -> Zipper.insert_left(zip, alias) end)
|> Zipper.remove()
end
defp segments_to_alias(segments, {_name, _meta, base_segments}) when is_list(segments) do
Enum.map(segments, fn {_name, meta, segments} ->
{:alias, meta, [{:__aliases__, [], base_segments ++ segments}]}
end)
end
defp put_leading_comments([first | rest], meta) do
comments = meta[:leading_comments] || []
[Sourceror.prepend_comments(first, comments) | rest]
end
defp put_trailing_comments(list, meta) do
comments = meta[:trailing_comments] || []
case List.pop_at(list, -1) do
{nil, list} ->
list
{last, list} ->
last =
{:__block__,
[
trailing_comments: comments,
end_of_expression: [newlines: 2]
], [last]}
list ++ [last]
end
end
end