Packages
styler
1.9.1
1.11.0
1.10.1
1.10.0
1.9.1
1.9.0
1.8.0
1.7.0
1.6.0
1.5.1
1.5.0
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
1.0.0-alpha.0
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.5
0.10.4
retired
0.10.3
0.10.2
0.10.1
0.10.0
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
retired
0.9.1
retired
0.9.0
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.0
0.1.1
0.1.0
A code-style enforcer that will just FIFY instead of complaining
Current section
Files
Jump to
Current section
Files
lib/alias_env.ex
# Copyright 2024 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.AliasEnv do
@moduledoc """
A datastructure for maintaining something like compiler alias state when traversing AST.
Not anywhere as correct as what the compiler gives us, but close enough for open source work.
An alias env is a map from an alias's `as` to its resolution in a context.
Given the ast for
alias Foo.Bar
we'd create the env:
%{:Bar => [:Foo, :Bar]}
"""
def define(env \\ %{}, ast)
def define(env, asts) when is_list(asts), do: Enum.reduce(asts, env, &define(&2, &1))
def define(env, {:alias, _, [{:__aliases__, _, aliases}]}), do: define(env, aliases, List.last(aliases))
def define(env, {:alias, _, [{:__aliases__, _, aliases}, [{_, {:__aliases__, _, [as]}}]]}), do: define(env, aliases, as)
# `alias __MODULE__` or other oddities i'm not bothering to get right
def define(env, {:alias, _, _}), do: env
defp define(env, modules, as), do: Map.put(env, as, expand(env, modules))
@doc """
Lengthens an alias to its full name, if its first name is defined in the environment"
Useful for transforming the ast for code like:
alias Bar.Baz.Foo #<- given the env with this alias
Foo.Woo.Cool # <- ast
to the ast for code like:
alias Bar.Baz.Foo
Bar.Baz.Foo.Woo.Cool
"""
# no need to traverse ast if there are no aliases
def expand_ast(env, ast) when map_size(env) == 0, do: ast
def expand_ast(env, ast) do
Macro.prewalk(ast, fn
{:__aliases__, meta, modules} -> {:__aliases__, meta, expand(env, modules)}
ast -> ast
end)
end
@doc """
Expands modules from env (wow that was helpful).
Using the examples from `expand_ast`, this works roughly like so:
> expand(%{Foo: [Bar, Baz, Foo]}, [Foo, Woo, Cool])
=> [Bar, Baz, Foo, Woo, Cool]
> expand(%{}, [No, Alias, For, Me])
=> [No, Alias, For, Me]
"""
def expand(env, [first | rest] = modules) do
if dealias = env[first], do: dealias ++ rest, else: modules
end
@doc """
An inverted AliasEnv is useful for translating a module to its alias, if one existed in the env
In the case that a module is aliased multiple times, the inverted env will only keep the final alias as lexically sorted
"""
def invert(env) do
# It's a bit of a bummer to do the extra group_by out of caution that this 1-off mistake happens,
# but ultimately we're usually working with a small list so performance costs are negligible
env
|> Enum.group_by(fn {_, v} -> v end, fn {k, _} -> k end)
|> Map.new(fn
{modules, [as]} ->
{modules, as}
# someone has something goofy going on, aliasing the same module with multiple names
# alias A.B.C
# alias A.B.C, as: Bar
# alias A.B.C, as: Foo
# we'll choose the one that comes last lexically, which will be the alpha-sorted last entry that isn't the default as
{modules, multiple_as} ->
default_as = List.last(modules)
# being clever - rather than rejecting the default up front and doing an extra list-traversal,
# just sort things and if the default comes first, grab the second element
case Enum.sort(multiple_as, :desc) do
[^default_as, last_as | _] -> {modules, last_as}
[last_as | _] -> {modules, last_as}
end
end)
end
end