Current section

Files

Jump to
styler lib style aliases.ex
Raw

lib/style/aliases.ex

# Copyright 2023 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
defmodule Styler.Style.Aliases do
@moduledoc """
Styles up aliases!
This Style will expand multi-aliases and sort aliases within their groups.
It also adds a newline after all alias groups.
Rewrites for the following Credo rules:
* `Credo.Check.Readability.AliasOrder`
* `Credo.Check.Readability.MultiAlias`
* `Credo.Check.Readability.UnnecessaryAliasExpansion`
This module is more particular than credo for sorting. Notably, it sorts `alias __MODULE__`, whereas Credo allowed
that alias intermixed anywhere in a group.
"""
@behaviour Styler.Style
alias Styler.Zipper
def run({{:alias, _, _}, _} = zipper) do
{zipper, aliases} =
zipper
|> Zipper.insert_left(:placeholder)
|> consume_alias_group([])
[first | rest] =
aliases
# Credo does case-agnostic sorting, so we have to match that here
|> Enum.map(&{&1, &1 |> Macro.to_string() |> String.downcase()})
# a splash of deduping for happiness
|> Enum.uniq_by(&elem(&1, 1))
|> Enum.sort_by(&elem(&1, 1))
|> Enum.map(&(&1 |> elem(0) |> set_newlines(1)))
zipper =
zipper
|> Zipper.find(:prev, &(&1 == :placeholder))
|> Zipper.replace(first)
rest
|> Enum.reduce(zipper, &(&2 |> Zipper.insert_right(&1) |> Zipper.right()))
|> Zipper.update(&set_newlines(&1, 2))
end
def run(zipper), do: zipper
defp set_newlines({node, meta, children}, newline) do
meta = Keyword.update(meta, :end_of_expression, [newlines: newline], &Keyword.put(&1, :newlines, newline))
{node, meta, children}
end
defp consume_alias_group({{:alias, meta, _} = alias, _} = zipper, aliases) do
aliases = expand_alias(alias, aliases)
zipper = Zipper.remove(zipper)
# multiple newlines means this isn't a group. missing newline means EOF.
# thus only when there's one newline do we continue accumulating for our alias group
if meta[:end_of_expression][:newlines] == 1 do
consume_alias_group(zipper, aliases)
else
{zipper, aliases}
end
end
defp consume_alias_group(zipper, aliases) do
# aliases in groups are always siblings, so we're using `Zipper.right` to save time from going down subtrees
case Zipper.right(zipper) do
nil -> {zipper, aliases}
zipper -> consume_alias_group(zipper, aliases)
end
end
# This is where multi alias expansion happens
#
# alias Foo.{Bar, Baz}
# =>
# alias Foo.Bar
# alias Foo.Baz
defp expand_alias({:alias, _, [{{:., _, [{_, _, module}, :{}]}, _, right}]}, aliases) do
right
|> Enum.map(fn {_, meta, segments} -> {:alias, meta, [{:__aliases__, [], module ++ segments}]} end)
|> Enum.concat(aliases)
end
defp expand_alias({:alias, _, _} = alias, aliases), do: [alias | aliases]
end