Skip to content

Commit

Permalink
hmm hmm hmm
Browse files Browse the repository at this point in the history
  • Loading branch information
novaugust committed Mar 11, 2024
1 parent fb73330 commit 7159e04
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 4 deletions.
91 changes: 89 additions & 2 deletions lib/style/module_directives.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ defmodule Styler.Style.ModuleDirectives do
@attr_directives ~w(moduledoc shortdoc behaviour)a ++ @callback_attrs
@defstruct ~w(schema embedded_schema defstruct)a

# taken from Credo
@excluded_namespaces MapSet.new(~w(File IO Inspect Kernel Macro Supervisor Task Version)a)
@stdlib MapSet.new(~w(Access Agent Application Atom Base Behaviour
Bitwise Code Date DateTime Dict Ecto Enum Exception
File Float GenEvent GenServer HashDict HashSet
Integer IO Kernel Keyword List Macro Map MapSet
Module NaiveDateTime Node Oban OptionParser Path Port
Process Protocol Range Record Regex Registry Set
Stream String StringIO Supervisor System Task Time
Tuple URI Version)a)

@libraries MapSet.new(~w(Ecto Plug Phoenix Oban)a)
@stdlib MapSet.union(@stdlib, @libraries)

@moduledoc_false {:@, [line: nil], [{:moduledoc, [line: nil], [{:__block__, [line: nil], [false]}]}]}

def run({{:defmodule, _, children}, _} = zipper, ctx) do
Expand Down Expand Up @@ -183,9 +197,16 @@ defmodule Styler.Style.ModuleDirectives do

uses = (directives[:use] || []) |> Enum.flat_map(&expand_directive/1) |> reset_newlines()
imports = expand_and_sort(directives[:import] || [])
requires = expand_and_sort(directives[:require] || [])

all_aliases = directives[:alias] || []
aliases = expand_and_sort(all_aliases)
requires = expand_and_sort(directives[:require] || [])

to_alias = find_extractable_aliases(requires ++ nondirectives, aliases)
# @TODO bug here if the first line of the parent is a comment
new_aliases = Enum.map(to_alias, &{:alias, [line: 0], [{:__aliases__, [last: [line: 0], line: 0], &1}]})
aliases = expand_and_sort(aliases ++ new_aliases)
requires = use_aliases(requires, to_alias)

directives =
[
Expand Down Expand Up @@ -213,7 +234,73 @@ defmodule Styler.Style.ModuleDirectives do
|> Zipper.update(&Zipper.replace_children(&1, directives))
|> Zipper.down()
|> Zipper.rightmost()
|> Zipper.insert_siblings(nondirectives)
|> Zipper.insert_siblings(use_aliases(nondirectives, to_alias))
end
end

defp find_extractable_aliases(ast, aliases) do
aliased =
aliases
|> Enum.flat_map(fn
{:alias, _, [{:__aliases__, _, aliases}]} -> [aliases]
_ -> []
end)
|> MapSet.new(&List.last/1)

excluded_first = MapSet.union(aliased, @excluded_namespaces)
excluded_last = MapSet.union(aliased, @stdlib)

ast
|> Zipper.zip()
|> Zipper.traverse_while(%{}, fn
# Credo doesn't look at :@ nodes. I kinda like it? especially for typespecs. otoh, we should just Zipper.remove those 🤣
{{directive, _, _}, _} = zipper, acc when directive in ~w(use)a ->
{:skip, zipper, acc}

# A.B.C.f(...)
{{{:., _, [{:__aliases__, _, [first, _, _ | _] = aliases}, _]}, _, _}, _} = zipper, acc ->
last = List.last(aliases)

acc =
if last in excluded_last or first in excluded_first,
do: acc,
else: Map.update(acc, aliases, false, fn _ -> true end)

{:skip, zipper, acc}

zipper, acc ->
{:cont, zipper, acc}
end)
|> elem(1)
# if we have `Foo.Bar.Baz` and `Foo.Bar.Bop.Baz` both not aliased, we'll create a collision by extracting both.
# grouping by last alias lets us detect these collisions
|> Enum.group_by(fn {aliases, _} -> List.last(aliases) end)
|> Enum.filter(fn
# only extract if it doesn't conflict and was used more than once
{_, [{_, true}]} -> true
_ -> false
end)
|> MapSet.new(fn {_, [{aliases, _}]} -> aliases end)
end

defp use_aliases(ast, new_aliases) do
if Enum.empty?(new_aliases) do
ast
else
ast
|> Zipper.zip()
|> Zipper.traverse(fn
{{:alias, _, [{:__aliases__, _, [_, _, _ | _] = aliases}]}, _} = zipper ->
# the alias was aliased deeper down. we've lifted that alias to a root, so delete this alias
if aliases in new_aliases, do: Zipper.remove(zipper), else: zipper

{{:__aliases__, meta, [_, _, _ | _] = aliases}, z_meta} = zipper ->
if aliases in new_aliases, do: {{:__aliases__, meta, [List.last(aliases)]}, z_meta}, else: zipper

zipper ->
zipper
end)
|> Zipper.node()
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/zipper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ defmodule Styler.Zipper do
{leftmost, %{meta | l: [], r: r}}
end

def leftmost(zipper), do: zipper
def leftmost({_, _} = zipper), do: zipper

@doc """
Returns the zipper of the right sibling of the node at this zipper, or nil.
Expand All @@ -141,7 +141,7 @@ defmodule Styler.Zipper do
{rightmost, %{meta | l: l, r: []}}
end

def rightmost(zipper), do: zipper
def rightmost({_, _} = zipper), do: zipper

@doc """
Replaces the current node in the zipper with a new node.
Expand Down

0 comments on commit 7159e04

Please sign in to comment.