Skip to content

Commit

Permalink
yeehaw
Browse files Browse the repository at this point in the history
  • Loading branch information
novaugust committed Apr 11, 2024
1 parent dac2332 commit 8a83f27
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 77 deletions.
36 changes: 33 additions & 3 deletions lib/dealias.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
defmodule Styler.Dealias 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.
"""
def new(aliases), do: Enum.reduce(aliases, %{}, &put(&2, &1))

def put(dealiases, ast)
def put(d, list) when is_list(list), do: Enum.reduce(list, d, &put(&2, &1))
def put(d, {:alias, _, [{:__aliases__, _, aliases}]}), do: do_put(d, aliases, List.last(aliases))
def put(d, {:alias, _, [{:__aliases__, _, aliases}, [{_as, {:__aliases__, _, [as]}}]]}), do: do_put(d, aliases, as)
# `alias __MODULE__` or other oddities i'm not bothering to get right
def put(dealiases, {:alias, _, _}), do: dealiases

defp do_put(dealiases, [first | rest] = modules, as) do
modules = if dealias = dealiases[first], do: dealias ++ rest, else: modules
Map.put(dealiases, as, modules)
defp do_put(dealiases, modules, as) do
Map.put(dealiases, as, do_dealias(dealiases, modules))
end

# no need to traverse ast if there are no aliases
def apply(dealiases, ast) when map_size(dealiases) == 0, do: ast

def apply(dealiases, {:alias, m, [{:__aliases__, m_, modules} | rest]}),
do: {:alias, m, [{:__aliases__, m_, do_dealias(dealiases, modules)} | rest]}

def apply(dealiases, ast) do
Macro.prewalk(ast, fn
{:__aliases__, meta, modules} -> {:__aliases__, meta, do_dealias(dealiases, modules)}
ast -> ast
end)
end
# if the list of modules is itself already aliased, dealias it with the compound alias
# given:
# alias Foo.Bar
# Bar.Baz.Bop.baz()
#
# lifting Bar.Baz.Bop should result in:
# alias Foo.Bar
# alias Foo.Bar.Baz.Bop
# Bop.baz()
defp do_dealias(dealiases, [first | rest] = modules) do
if dealias = dealiases[first], do: dealias ++ rest, else: modules
end
end
133 changes: 59 additions & 74 deletions lib/style/module_directives.ex
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ defmodule Styler.Style.ModuleDirectives do

@directives ~w(alias import require use)a
@callback_attrs ~w(before_compile after_compile after_verify)a
@attr_directives ~w(moduledoc shortdoc behaviour)a ++ @callback_attrs
@attr_directives ~w(moduledoc shortdoc behaviour)a
@defstruct ~w(schema embedded_schema defstruct)a

@moduledoc_false {:@, [line: nil], [{:moduledoc, [line: nil], [{:__block__, [line: nil], [false]}]}]}
Expand Down Expand Up @@ -190,7 +190,7 @@ defmodule Styler.Style.ModuleDirectives do
# a dynamic module name, like `defmodule my_variable do ... end`
defp moduledoc(_), do: nil

@state %{
@acc %{
"@shortdoc": [],
"@moduledoc": [],
"@behaviour": [],
Expand All @@ -203,7 +203,7 @@ defmodule Styler.Style.ModuleDirectives do
}

defp organize_directives(parent, moduledoc \\ nil) do
directives =
acc =
parent
|> Zipper.children()
# @TODO if i switch this to a reduce, i can keep information about whether or not i saw
Expand All @@ -213,35 +213,52 @@ defmodule Styler.Style.ModuleDirectives do
# holy crap actually, i can keep track of aliases seen so far, and whenever i see a pre-alias node,
# immediately call de-alias on it!
# i believe i could simultaneously find liftable aliases?
|> Enum.reduce(@state, fn
# the order of callbacks relative to use can matter if the use is also doing callbacks
# looking back, this is probably a hack to support one person's weird hackery 🤣
# TODO drop for a 1.0 release?
{:@, _, [{attr, _, _}]} = ast, state when attr in @callback_attrs -> %{state | use: [ast | state.use]}
{:@, _, [{attr, _, _}]} = ast, state when attr in @attr_directives -> %{state | "@#{attr}": [ast | state[:"@#{attr}"]]}
{:alias, _, _} = alias, state ->
%{state | alias: [alias | state.alias], dealiases: Dealias.put(state.dealiases, alias)}
{directive, _, _} = ast, state when directive in @directives -> %{state | directive => [ast | state[directive]]}
ast, state -> %{state | nondirectives: [ast | state.nondirectives]}
|> Enum.reduce(@acc, fn
{:@, _, [{attr, _, _}]} = ast, acc ->
key =
cond do
# TODO drop for a 1.0 release?
# the order of callbacks relative to use can matter if the use is also doing callbacks
# looking back, this is probably a hack to support one person's weird hackery 🤣
attr in @callback_attrs -> :use
attr in @attr_directives -> :"@#{attr}"
true -> :nondirectives
end
# both callback and attr_directives are moved above aliases, so we need to dealias them
ast = if key == :nondirectives, do: ast, else: Dealias.apply(acc.dealiases, ast)
%{acc | key => [ast | acc[key]]}

{directive, _, _} = ast, acc when directive in @directives ->
ast = expand(ast)
# import and used get hoisted above aliases, so need to dealias
ast = if directive in ~w(import use)a, do: Dealias.apply(acc.dealiases, ast), else: ast
dealiases = if directive == :alias, do: Dealias.put(acc.dealiases, ast), else: acc.dealiases
# the reverse accounts for `expand` putting things in reading order, whereas we're accumulating in reverse
%{acc | directive => Enum.reverse(ast, acc[directive]), :dealiases => dealiases}

ast, acc -> %{acc | nondirectives: [ast | acc.nondirectives]}
end)
|> Map.new(fn {k, v} -> {k, if(is_list(v), do: Enum.reverse(v), else: v)} end)

nondirectives = directives.nondirectives
# Reversing once we're done accumulating since `reduce`ing into list accs means you're reversed!
|> Map.new(fn
{:"@moduledoc", []} -> {:"@moduledoc", List.wrap(moduledoc)}
{:use, uses} -> {:use, uses |> Enum.reverse() |> Style.reset_newlines()}
{directive, to_sort} when directive in ~w(@behaviour import alias require)a -> {directive, sort(to_sort)}
{:dealiases, d} -> {:dealiases, d}
{k, v} -> {k, Enum.reverse(v)}
end)
|> lift_aliases()

aliases = directives[:alias] |> expand() |> sort()
requires = directives[:require] |> expand() |> sort()
{aliases, requires, nondirectives} = lift_aliases(aliases, requires, nondirectives)
min_alias_line = aliases |> Stream.map(fn {_, meta, _} -> meta[:line] end) |> Enum.min(fn -> nil end)
nondirectives = acc.nondirectives

directives =
[
directives[:"@shortdoc"] |> dealias(aliases, min_alias_line),
directives[:"@moduledoc"] |> List.first(moduledoc) |> List.wrap() |> dealias(aliases, min_alias_line),
directives[:"@behaviour"] |> dealias(aliases, min_alias_line) |> sort(),
directives[:use] |> expand() |> dealias(aliases, min_alias_line) |> Style.reset_newlines(),
directives[:import] |> expand() |> dealias(aliases, min_alias_line) |> sort(),
aliases,
requires
acc."@shortdoc",
acc."@moduledoc",
acc."@behaviour",
acc.use,
acc.import,
acc.alias,
acc.require
]
|> Stream.concat()
|> Style.fix_line_numbers(List.first(nondirectives))
Expand All @@ -259,41 +276,11 @@ defmodule Styler.Style.ModuleDirectives do
end
end

defp dealias(directives, [], _), do: directives

defp dealias(directives, aliases, min_alias_line) do
Enum.map(directives, fn {_, meta, _} = ast ->
line = meta[:line]

if line < min_alias_line do
ast
else
dealiases = aliases |> Enum.filter(fn {_, meta, _} -> meta[:line] < line end) |> Styler.Dealias.new()

Macro.prewalk(ast, fn
{:__aliases__, meta, modules} -> {:__aliases__, meta, do_dealias(modules, dealiases)}
ast -> ast
end)
end
end)
end

# if the list of modules is itself already aliased, dealias it with the compound alias
# given:
# alias Foo.Bar
# Bar.Baz.Bop.baz()
#
# lifting Bar.Baz.Bop should result in:
# alias Foo.Bar
# alias Foo.Bar.Baz.Bop
# Bop.baz()
defp do_dealias([first | rest] = modules, dealiases) do
if dealias = dealiases[first], do: dealias ++ rest, else: modules
end

defp lift_aliases(aliases, requires, nondirectives) do
dealiasing_map = Styler.Dealias.new(aliases)
excluded = dealiasing_map |> MapSet.new(fn {a, _} -> a end) |> MapSet.union(Styler.Config.get(:lifting_excludes))
defp lift_aliases(%{alias: aliases, require: requires, nondirectives: nondirectives} = acc) do
# we can't use the dealias map built into state as that's what things look like before sorting
# now that we've sorted, it could be different!
dealiases = Dealias.new(aliases)
excluded = dealiases |> Map.keys() |> Enum.into(Styler.Config.get(:lifting_excludes))
liftable = find_liftable_aliases(requires ++ nondirectives, excluded)

if Enum.any?(liftable) do
Expand All @@ -304,16 +291,16 @@ defmodule Styler.Style.ModuleDirectives do

aliases =
liftable
|> Enum.map(&{:alias, m, [{:__aliases__, [{:last, m} | m], do_dealias(&1, dealiasing_map)}]})
|> Enum.map(&Dealias.apply(dealiases, {:alias, m, [{:__aliases__, [{:last, m} | m], &1}]}))
|> Enum.concat(aliases)
|> sort()

# lifting could've given us a new order
requires = requires |> do_lift_aliases(liftable) |> sort()
nondirectives = do_lift_aliases(nondirectives, liftable)
{aliases, requires, nondirectives}
%{acc | alias: aliases, require: requires, nondirectives: nondirectives}
else
{aliases, requires, nondirectives}
acc
end
end

Expand All @@ -328,7 +315,7 @@ defmodule Styler.Style.ModuleDirectives do
lifts =
case args do
[{:__aliases__, _, aliases} | _] when defx == :defmodule ->
Map.put(lifts, List.last(aliases), {:collision_with_submodule, false})
Map.put(lifts, List.last(aliases), :collision_with_submodule)

_ ->
lifts
Expand All @@ -351,7 +338,7 @@ defmodule Styler.Style.ModuleDirectives do
{^aliases, _} -> {aliases, true}
# if we have `Foo.Bar.Baz` and `Foo.Bar.Bop.Baz` both not aliased, we'll create a collision by lifting both
# grouping by last alias lets us detect these collisions
_ -> {:collision_with_last, false}
_ -> :collision_with_last
end)
end

Expand All @@ -365,7 +352,7 @@ defmodule Styler.Style.ModuleDirectives do
# C.foo()
#
# lifting A.B.C would create a collision with C.
{:skip, zipper, Map.put(lifts, first, {:collision_with_first, false})}
{:skip, zipper, Map.put(lifts, first, :collision_with_first)}

zipper, lifts ->
{:cont, zipper, lifts}
Expand Down Expand Up @@ -399,29 +386,27 @@ defmodule Styler.Style.ModuleDirectives do
|> Zipper.node()
end

defp expand(directives), do: Enum.flat_map(directives, &expand_directive/1)

# Deletes root level aliases ala (`alias Foo` -> ``)
defp expand_directive({:alias, _, [{:__aliases__, _, [_]}]}), do: []
defp expand({:alias, _, [{:__aliases__, _, [_]}]}), do: []

# import Foo.{Bar, Baz}
# =>
# import Foo.Bar
# import Foo.Baz
defp expand_directive({directive, _, [{{:., _, [{:__aliases__, _, module}, :{}]}, _, right}]}) do
defp expand({directive, _, [{{:., _, [{:__aliases__, _, module}, :{}]}, _, right}]}) do
Enum.map(right, fn {_, meta, segments} ->
{directive, meta, [{:__aliases__, [line: meta[:line]], module ++ segments}]}
end)
end

# alias __MODULE__.{Bar, Baz}
defp expand_directive({directive, _, [{{:., _, [{:__MODULE__, _, _} = module, :{}]}, _, right}]}) do
defp expand({directive, _, [{{:., _, [{:__MODULE__, _, _} = module, :{}]}, _, right}]}) do
Enum.map(right, fn {_, meta, segments} ->
{directive, meta, [{:__aliases__, [line: meta[:line]], [module | segments]}]}
end)
end

defp expand_directive(other), do: [other]
defp expand(other), do: [other]

defp sort(directives) do
# sorting is done with `downcase` to match Credo
Expand Down

0 comments on commit 8a83f27

Please sign in to comment.