Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match credo for alias lifting #6

Merged
merged 2 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/quokka/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
defmodule Quokka.Config do
@moduledoc false

alias Credo.Check.Design.AliasUsage
alias Credo.Check.Readability.AliasOrder
alias Credo.Check.Readability.BlockPipe
alias Credo.Check.Readability.LargeNumbers
Expand Down Expand Up @@ -82,6 +83,9 @@ defmodule Quokka.Config do
pipe_chain_start_excluded_functions: credo_opts[:pipe_chain_start_excluded_functions] || [],
pipe_chain_start_excluded_argument_types: credo_opts[:pipe_chain_start_excluded_argument_types] || [],
reorder_configs: reorder_configs,
lift_alias: credo_opts[:lift_alias] || false,
lift_alias_depth: credo_opts[:lift_alias_depth] || 0,
lift_alias_frequency: credo_opts[:lift_alias_frequency] || 0,
rewrite_multi_alias: credo_opts[:rewrite_multi_alias] || false,
single_pipe_flag: credo_opts[:single_pipe_flag] || false,
sort_order: credo_opts[:sort_order] || :alpha,
Expand Down Expand Up @@ -124,6 +128,18 @@ defmodule Quokka.Config do
get(:large_numbers_gt)
end

def lift_alias?() do
get(:lift_alias)
end

def lift_alias_depth() do
get(:lift_alias_depth)
end

def lift_alias_frequency() do
get(:lift_alias_frequency)
end

def line_length() do
get(:line_length)
end
Expand Down Expand Up @@ -164,6 +180,12 @@ defmodule Quokka.Config do
{AliasOrder, opts}, acc when is_list(opts) ->
Map.put(acc, :sort_order, opts[:sort_method])

{AliasUsage, opts}, acc when is_list(opts) ->
acc
|> Map.put(:lift_alias, true)
|> Map.put(:lift_alias_depth, opts[:if_nested_deeper_than])
|> Map.put(:lift_alias_frequency, opts[:if_called_more_often_than])

{BlockPipe, opts}, acc when is_list(opts) ->
acc
|> Map.put(:block_pipe_flag, true)
Expand Down
66 changes: 36 additions & 30 deletions lib/style/module_directives.ex
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ defmodule Quokka.Style.ModuleDirectives do
# now that we've sorted, it could be different!
dealiases = AliasEnv.define(aliases)
excluded = dealiases |> Map.keys() |> Enum.into(Quokka.Config.get(:lifting_excludes))
liftable = find_liftable_aliases(requires ++ nondirectives, excluded)
liftable = if Quokka.Config.lift_alias?(), do: find_liftable_aliases(requires ++ nondirectives, excluded), else: []

if Enum.any?(liftable) do
# This is a silly hack that helps comments stay put.
Expand Down Expand Up @@ -367,45 +367,51 @@ defmodule Quokka.Style.ModuleDirectives do
{{:quote, _, _}, _} = zipper, lifts ->
{:skip, zipper, lifts}

{{:__aliases__, _, [_, _, _ | _] = aliases}, _} = zipper, lifts ->
alias_string = Enum.join(aliases, ".")
{{:__aliases__, _, [first, _| _] = aliases}, _} = zipper, lifts ->
if Enum.all?(aliases, &is_atom/1) do
alias_string = Enum.join(aliases, ".")

excluded_regex_match =
excluded
|> Stream.filter(&is_struct(&1, Regex))
|> Enum.any?(&Regex.match?(&1, alias_string))
excluded_regex_match =
excluded
|> Stream.filter(&is_struct(&1, Regex))
|> Enum.any?(&Regex.match?(&1, alias_string))

last = List.last(aliases)
last = List.last(aliases)

lifts =
if excluded_regex_match or last in excluded or not Enum.all?(aliases, &is_atom/1) do
lifts
else
Map.update(lifts, last, {aliases, false}, fn
{^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
end)
end

{:skip, zipper, lifts}

{{:__aliases__, _, [first | _]}, _} = zipper, lifts ->
lifts =
if excluded_regex_match or last in excluded or not Enum.all?(aliases, &is_atom/1) or
length(aliases) <= Quokka.Config.lift_alias_depth() do
lifts
else
Map.update(lifts, last, {aliases, 1}, fn
{^aliases, count} -> {aliases, count + 1}
# 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
end)
end
# given:
# C.foo()
# A.B.C.foo()
# A.B.C.foo()
# C.foo()
#
# lifting A.B.C would create a collision with C.
{:skip, zipper, Map.put(lifts, first, :collision_with_first)}
{:skip, zipper, Map.put(lifts, first, :collision_with_first)}
else
{:skip, zipper, lifts}
end

zipper, lifts ->
{:cont, zipper, lifts}
end)
|> Enum.filter(&match?({_last, {_aliases, true}}, &1))
|> MapSet.new(fn {_, {aliases, true}} -> aliases end)
|> Enum.filter(fn {_last, value} ->
case value do
{_aliases, count} -> count > Quokka.Config.lift_alias_frequency()
_ -> false
end
end)
|> MapSet.new(fn {_, {aliases, _count}} -> aliases end)
end

defp do_lift_aliases(ast, to_alias) do
Expand All @@ -422,14 +428,14 @@ defmodule Quokka.Style.ModuleDirectives do
|> Zipper.down()
|> Zipper.right()

{{:alias, _, [{:__aliases__, _, [_, _, _ | _] = aliases}]}, _} = zipper ->
{{: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 to_alias,
if aliases in to_alias and Enum.all?(aliases, &is_atom/1) and length(aliases) > Quokka.Config.lift_alias_depth(),
do: Zipper.remove(zipper),
else: zipper

{{:__aliases__, meta, [_, _, _ | _] = aliases}, _} = zipper ->
if aliases in to_alias,
{{:__aliases__, meta, [_, _ | _] = aliases}, _} = zipper ->
if aliases in to_alias and Enum.all?(aliases, &is_atom/1) and length(aliases) > Quokka.Config.lift_alias_depth(),
do: Zipper.replace(zipper, {:__aliases__, meta, [List.last(aliases)]}),
else: zipper

Expand Down
174 changes: 173 additions & 1 deletion test/style/module_directives/alias_lifting_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ defmodule Quokka.Style.ModuleDirectives.AliasLiftingTest do
@moduledoc false
use Quokka.StyleCase, async: false

setup do
Quokka.Config.set_for_test!(:lift_alias, true)
Quokka.Config.set_for_test!(:lift_alias_depth, 2)
Quokka.Config.set_for_test!(:lift_alias_frequency, 1)

on_exit(fn ->
Quokka.Config.set!([])
end)
:ok
end

test "lifts aliases repeated >=2 times from 3 deep" do
assert_style(
"""
Expand Down Expand Up @@ -300,8 +311,69 @@ defmodule Quokka.Style.ModuleDirectives.AliasLiftingTest do
end

describe "it doesn't lift" do
test "when flag is off" do
Quokka.Config.set_for_test!(:lift_alias, false)

assert_style(
"""
defmodule MyModule do
@moduledoc false

@spec foo :: A.B.C.t()
def foo do
A.B.C.f()
A.B.C.g()
end

def bar do
X.Y.Z.foo()
X.Y.Z.bar()
X.Y.Z.baz()
end

defmodule Nested do
@moduledoc false

def baz do
P.Q.R.one()
P.Q.R.two()
P.Q.R.three()
end
end
end
""",
"""
defmodule MyModule do
@moduledoc false

@spec foo :: A.B.C.t()
def foo() do
A.B.C.f()
A.B.C.g()
end

def bar() do
X.Y.Z.foo()
X.Y.Z.bar()
X.Y.Z.baz()
end

defmodule Nested do
@moduledoc false

def baz() do
P.Q.R.one()
P.Q.R.two()
P.Q.R.three()
end
end
end
"""
)
end

test "collisions with configured modules" do
Quokka.Config.set!(alias_lifting_exclude: ~w(C)a)
Quokka.Config.set!(alias_lifting_exclude: ~w(C)a, alias_lift: true)

assert_style """
alias Foo.Bar
Expand All @@ -315,6 +387,7 @@ defmodule Quokka.Style.ModuleDirectives.AliasLiftingTest do

test "collisions with configured regexes" do
Quokka.Config.set!(alias_lifting_exclude: [~r/A.B/])
Quokka.Config.set_for_test!(:lift_alias, true)

assert_style(
"""
Expand Down Expand Up @@ -528,4 +601,103 @@ defmodule Quokka.Style.ModuleDirectives.AliasLiftingTest do
"""
end
end

test "lifts all aliases when lift_alias_depth is 0" do
Quokka.Config.set_for_test!(:lift_alias_depth, 0)

assert_style(
"""
defmodule MyModule do
@moduledoc false

B.C.f()
B.C.g()
Y.Z.foo()
Y.Z.bar()
S.T.bar()
S.T.baz()
end
""",
"""
defmodule MyModule do
@moduledoc false

alias B.C
alias S.T
alias Y.Z

C.f()
C.g()
Z.foo()
Z.bar()
T.bar()
T.baz()
end
"""
)
end

describe "lift_alias_frequency configuration" do
test "only lifts aliases that meet frequency threshold" do
Quokka.Config.set_for_test!(:lift_alias_frequency, 2)

assert_style(
"""
defmodule MyModule do
@moduledoc false

A.B.C.foo()
A.B.C.bar()
X.Y.Z.one()
X.Y.Z.two()
X.Y.Z.three()
P.Q.R.single()
end
""",
"""
defmodule MyModule do
@moduledoc false

alias X.Y.Z

A.B.C.foo()
A.B.C.bar()
Z.one()
Z.two()
Z.three()
P.Q.R.single()
end
"""
)
end

test "lifts all aliases when frequency is 0" do
Quokka.Config.set_for_test!(:lift_alias_frequency, 0)

assert_style(
"""
defmodule MyModule do
@moduledoc false

A.B.C.foo()
X.Y.Z.one()
P.Q.R.single()
end
""",
"""
defmodule MyModule do
@moduledoc false

alias A.B.C
alias P.Q.R
alias X.Y.Z

C.foo()
Z.one()
R.single()
end
"""
)
end
end
end