# Copyright 2024 Adobe. All rights reserved. # Copyright 2025 SmartRent. 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 Quokka.Style.SingleNode do @moduledoc """ Simple 1-1 rewrites all crammed into one module to make for more efficient traversals Credo Rules addressed: * Credo.Check.Consistency.ParameterPatternMatching * Credo.Check.Readability.LargeNumbers * Credo.Check.Readability.ParenthesesOnZeroArityDefs * Credo.Check.Readability.PreferImplicitTry * Credo.Check.Readability.StringSigils * Credo.Check.Readability.WithSingleClause * Credo.Check.Refactor.CondStatements * Credo.Check.Refactor.RedundantWithClauseResult * Credo.Check.Refactor.WithClauses * Credo.Check.Warning.ExpensiveEmptyEnumCheck """ @behaviour Quokka.Style alias Quokka.Zipper @closing_delimiters [~s|"|, ")", "}", "|", "]", "'", ">", "/"] # `|> Timex.now()` => `|> Timex.now()` # skip over pipes into `Timex.now/1` so that we don't accidentally rewrite it as DateTime.utc_now/1 def run({{:|>, _, [_, {{:., _, [{:__aliases__, _, [:Timex]}, :now]}, _, []}]}, _} = zipper, ctx), do: {:skip, zipper, ctx} # `|> Timex.today()` => `|> Timex.today()` # skip over pipes into `Timex.today/1` so that we don't accidentally rewrite it as Date.utc_today/1 def run({{:|>, _, [_, {{:., _, [{:__aliases__, _, [:Timex]}, :today]}, _, []}]}, _} = zipper, ctx), do: {:skip, zipper, ctx} # A pipe-fed `Enum.reduce/3` (`lhs |> Enum.reduce(acc, reducer)`) reaches node # styling as its inner `Enum.reduce(acc, reducer)`, which is shaped exactly # like `Enum.reduce/2`. The reduce/2 => Enum.sum rewrite would treat the # accumulator as the enumerable and emit `Enum.sum(acc)`; re-piped that is # `Enum.sum/2`, which exists in no Elixir version. The zero-accumulator pure # sum is handled at the pipe node itself (`|> Enum.reduce(0, &+/2)` => # `|> Enum.sum()`); here we only decline the rewrite for the genuine reduce/3 # form, while still traversing so the rest of the tree is styled. def run({{{:., _, [{:__aliases__, _, [:Enum]}, :reduce]}, _, [_, _]} = reduce, meta} = zipper, ctx) do case Zipper.up(zipper) do {{:|>, _, [_, ^reduce]}, _} -> {:cont, zipper, ctx} _ -> {:cont, {style(reduce), meta}, ctx} end end # Skip expensive empty enum check rewrites when inside guard clauses def run({node, meta} = zipper, ctx) when elem(node, 0) in [:>, :<, :==, :===, :!=] do if in_guard?(zipper) do {:cont, {style_guard(node), meta}, ctx} else {:cont, {style(node), meta}, ctx} end end def run({node, meta}, ctx), do: {:cont, {style(node), meta}, ctx} # rewrite double-quote strings with >= 4 escaped double-quotes as sigils defp style({:__block__, [{:delimiter, ~s|"|} | meta], [string]} = node) when is_binary(string) do # running a regex against every double-quote delimited string literal in a codebase doesn't have too much impact # on adobe's internal codebase, but perhaps other codebases have way more literals where this'd have an impact? if string =~ ~r/".*".*".*"/ do # choose whichever delimiter would require the least # of escapes, # ties being broken by our stylish ordering of delimiters (reflected in the 1-8 values) {closer, _} = string |> String.codepoints() |> Stream.filter(&(&1 in @closing_delimiters)) |> Stream.concat(@closing_delimiters) |> Enum.frequencies() |> Enum.min_by(fn {~s|"|, count} -> {count, 1} {")", count} -> {count, 2} {"}", count} -> {count, 3} {"|", count} -> {count, 4} {"]", count} -> {count, 5} {"'", count} -> {count, 6} {">", count} -> {count, 7} {"/", count} -> {count, 8} end) delimiter = case closer do ")" -> "(" "}" -> "{" "]" -> "[" ">" -> "<" closer -> closer end {:sigil_s, [{:delimiter, delimiter} | meta], [{:<<>>, [line: meta[:line]], [string]}, []]} else node end end # Add / Correct `_` location in large numbers. Formatter handles large number (>5 digits) rewrites, # but doesn't rewrite typos like `100_000_0`, so it's worthwhile to have Quokka do this # # `?-` isn't part of the number node - it's its parent - so all numbers are positive at this point defp style({:__block__, meta, [number]}) when is_number(number) do if number > Quokka.Config.large_numbers_gt() do # Checking here rather than in the anonymous function due to compiler bug https://github.com/elixir-lang/elixir/issues/10485 integer? = is_integer(number) meta = Keyword.update!(meta, :token, fn "0x" <> _ = token -> token "0b" <> _ = token -> token "0o" <> _ = token -> token token when integer? -> delimit(token) # is float token -> [int_token, decimals] = String.split(token, ".") "#{delimit(int_token)}.#{decimals}" end) {:__block__, meta, [number]} else {:__block__, meta, [number]} end end ## INEFFICIENT FUNCTION REWRITES # Keep in mind when rewriting a `/n::pos_integer` arity function here that it should also be added # to the pipes rewriting rules, where it will appear as `/n-1` # Enum.into(enum, empty_map[, ...]) => Map.new(enum[, ...]) defp style({{:., _, [{:__aliases__, _, [:Enum]}, :into]} = into, m, [enum, collectable | rest]} = node) do if replacement = replace_into(into, collectable, rest), do: {replacement, m, [enum | rest]}, else: node end # lhs |> Enum.into(%{}, ...) => lhs |> Map.new(...) defp style({:|>, meta, [lhs, {{:., _, [{_, _, [:Enum]}, :into]} = into, m, [collectable | rest]}]} = node) do if replacement = replace_into(into, collectable, rest), do: {:|>, meta, [lhs, {replacement, m, rest}]}, else: node end for m <- [:Map, :Keyword] do # lhs |> Map/Keyword.merge(%{key: value}) => lhs |> Map/Keyword.put(key, value) defp style( {:|>, pm, [lhs, {{:., dm, [{_, _, [unquote(m)]} = module, :merge]}, m, [{:%{}, _, [{key, value}]}]}]} = node ) do if Quokka.Config.inefficient_function_rewrites?(), do: {:|>, pm, [lhs, {{:., dm, [module, :put]}, m, [key, value]}]}, else: node end # lhs |> Map/Keyword.merge(key: value) => lhs |> Map/Keyword.put(key, value) defp style({:|>, pm, [lhs, {{:., dm, [{_, _, [unquote(m)]} = module, :merge]}, m, [[{key, value}]]}]} = node) do if Quokka.Config.inefficient_function_rewrites?(), do: {:|>, pm, [lhs, {{:., dm, [module, :put]}, m, [key, value]}]}, else: node end # Map/Keyword.merge(lhs, %{key: value}) => Map/Keyword.put(lhs, key, value) defp style({{:., dm, [{_, _, [unquote(m)]} = module, :merge]}, m, [lhs, {:%{}, _, [{key, value}]}]} = node) do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., dm, [module, :put]}, m, [lhs, key, value]}, else: node end # Map/Keyword.merge(lhs, key: value) => Map/Keyword.put(lhs, key, value) defp style({{:., dm, [{_, _, [unquote(m)]} = module, :merge]}, m, [lhs, [{key, value}]]} = node) do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., dm, [module, :put]}, m, [lhs, key, value]}, else: node end # lhs |> Map/Keyword.drop([key]) => lhs |> Map/Keyword.delete(key) defp style({{:., dm, [{_, _, [unquote(m)]} = module, :drop]}, m, [{:__block__, _, [[{op, _, _} = key]]}]} = node) when op != :| do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., dm, [module, :delete]}, m, [key]}, else: node end # Map/Keyword.drop(lhs, [one_key]) => Map.delete(lhs, one_key) defp style( {{:., dm, [{_, _, [unquote(m)]} = module, :drop]}, m, [lhs, {:__block__, _, [[{op, _, _} = key]]}]} = node ) when op != :| do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., dm, [module, :delete]}, m, [lhs, key]}, else: node end # Map/Keyword.get(lhs, key, nil) => Map/Keyword.get(lhs, key) defp style({{:., dm, [{_, _, [unquote(m)]} = module, :get]}, m, [lhs, key, {:__block__, _, [nil]}]}) do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., dm, [module, :get]}, m, [lhs, key]}, else: {{:., dm, [module, :get]}, m, [lhs, key, {:__block__, m, [nil]}]} end # lhs |> Map/Keyword.get(key, nil) => lhs |> Map/Keyword.get(key) defp style({:|>, pm, [lhs, {{:., dm, [{_, _, [unquote(m)]} = module, :get]}, m, [key, {:__block__, _, [nil]}]}]}) do if Quokka.Config.inefficient_function_rewrites?(), do: {:|>, pm, [lhs, {{:., dm, [module, :get]}, m, [key]}]}, else: {:|>, pm, [lhs, {{:., dm, [module, :get]}, m, [key, {:__block__, m, [nil]}]}]} end end # Timex.now() => DateTime.utc_now() defp style({{:., dm, [{:__aliases__, am, [:Timex]}, :now]}, funm, []} = node) do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., dm, [{:__aliases__, am, [:DateTime]}, :utc_now]}, funm, []}, else: node end # Timex.today() => Date.utc_today() defp style({{:., dm, [{:__aliases__, am, [:Timex]}, :today]}, funm, []} = node) do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., dm, [{:__aliases__, am, [:Date]}, :utc_today]}, funm, []}, else: node end # {DateTime,NaiveDateTime,Time,Date}.compare(a, b) == :lt => {DateTime,NaiveDateTime,Time,Date}.before?(a, b) # {DateTime,NaiveDateTime,Time,Date}.compare(a, b) == :gt => {DateTime,NaiveDateTime,Time,Date}.after?(a, b) defp style( {:==, _, [{{:., dm, [{:__aliases__, am, [mod]}, :compare]}, funm, args}, {:__block__, _, [result]}]} = node ) when mod in ~w[DateTime NaiveDateTime Time Date]a and result in [:lt, :gt] do if Quokka.Config.inefficient_function_rewrites?() do fun = if result == :lt, do: :before?, else: :after? {{:., dm, [{:__aliases__, am, [mod]}, fun]}, funm, args} else node end end # assert Repo.one(query) => assert Repo.exists?(query) defp style({:assert, am, [{{:., dm, [{:__aliases__, alias_meta, modules}, :one]}, funm, args}]} = node) do if Quokka.Config.inefficient_function_rewrites?() and List.last(modules) == :Repo do {:assert, am, [{{:., dm, [{:__aliases__, alias_meta, modules}, :exists?]}, funm, args}]} else node end end # assert query |> Repo.one() => assert query |> Repo.exists?() defp style( {:assert, am, [{:|>, pipe_meta, [lhs, {{:., dm, [{:__aliases__, alias_meta, modules}, :one]}, funm, args}]}]} = node ) do if Quokka.Config.inefficient_function_rewrites?() and List.last(modules) == :Repo do {:assert, am, [{:|>, pipe_meta, [lhs, {{:., dm, [{:__aliases__, alias_meta, modules}, :exists?]}, funm, args}]}]} else node end end # refute Repo.one(query) => refute Repo.exists?(query) defp style({:refute, rm, [{{:., dm, [{:__aliases__, alias_meta, modules}, :one]}, funm, args}]} = node) do if Quokka.Config.inefficient_function_rewrites?() and List.last(modules) == :Repo do {:refute, rm, [{{:., dm, [{:__aliases__, alias_meta, modules}, :exists?]}, funm, args}]} else node end end # refute query |> Repo.one() => refute query |> Repo.exists?() defp style( {:refute, rm, [{:|>, pipe_meta, [lhs, {{:., dm, [{:__aliases__, alias_meta, modules}, :one]}, funm, args}]}]} = node ) do if Quokka.Config.inefficient_function_rewrites?() and List.last(modules) == :Repo do {:refute, rm, [{:|>, pipe_meta, [lhs, {{:., dm, [{:__aliases__, alias_meta, modules}, :exists?]}, funm, args}]}]} else node end end # if Repo.one(query) do => if Repo.exists?(query) do defp style({:if, im, [condition, body]} = node) do if Quokka.Config.inefficient_function_rewrites?() do new_condition = rewrite_repo_one_in_conditional(condition) {:if, im, [new_condition, body]} else node end end # unless Repo.one(query) do => unless Repo.exists?(query) do defp style({:unless, um, [condition, body]} = node) do if Quokka.Config.inefficient_function_rewrites?() do new_condition = rewrite_repo_one_in_conditional(condition) {:unless, um, [new_condition, body]} else node end end # `Credo.Check.Readability.PreferImplicitTry` defp style({def, dm, [head, [{_, {:try, _, [try_children]}}]]}) when def in ~w(def defp)a, do: style({def, dm, [head, try_children]}) # Remove parens from 0 arity funs (Credo.Check.Readability.ParenthesesOnZeroArityDefs) defp style({def, dm, [{fun, funm, []} | rest]} = node) when def in ~w(def defp)a and is_atom(fun) do if Quokka.Config.zero_arity_parens?() == false, do: style({def, dm, [{fun, Keyword.delete(funm, :closing), nil} | rest]}), else: node end # Add parens to 0 arity funs (Credo.Check.Readability.ParenthesesOnZeroArityDefs) defp style({def, dm, [{fun, funm, nil} | rest]} = node) when def in ~w(def defp)a and is_atom(fun) do if Quokka.Config.zero_arity_parens?() == true, do: {def, dm, [{fun, Keyword.put(funm, :closing, line: funm[:line]), []} | rest]}, else: node end defp style({def, dm, [{fun, funm, params} | rest]}) when def in ~w(def defp)a, do: {def, dm, [{fun, funm, put_matches_on_right(params)} | rest]} # `Enum.reverse(foo) ++ bar` => `Enum.reverse(foo, bar)` defp style({:++, _, [{{:., _, [{_, _, [:Enum]}, :reverse]} = reverse, r_meta, [lhs]}, rhs]}), do: {reverse, r_meta, [lhs, rhs]} # `Enum.reduce(enum, 0, fn x, acc -> x + acc end)` => `Enum.sum(enum)` defp style({{:., dm, [{:__aliases__, am, [:Enum]}, :reduce]}, m, [enum, {:__block__, _, [0]}, reducer]} = node) do if Quokka.Config.inefficient_function_rewrites?() and sum_reducer?(reducer), do: {{:., dm, [{:__aliases__, am, [:Enum]}, :sum]}, m, [enum]}, else: node end # `Enum.reduce(enum, fn x, acc -> x + acc end)` => `Enum.sum(enum)` # Enum.reduce/2 uses the first element as the accumulator; summing the rest is # equivalent to summing the whole enum. defp style({{:., dm, [{:__aliases__, am, [:Enum]}, :reduce]}, m, [enum, reducer]} = node) do if Quokka.Config.inefficient_function_rewrites?() and sum_reducer?(reducer), do: {{:., dm, [{:__aliases__, am, [:Enum]}, :sum]}, m, [enum]}, else: node end # `enum |> Enum.reduce(0, fn x, acc -> x + acc end)` => `enum |> Enum.sum()` defp style( {:|>, pm, [lhs, {{:., dm, [{:__aliases__, am, [:Enum]}, :reduce]}, m, [{:__block__, _, [0]}, reducer]}]} = node ) do if Quokka.Config.inefficient_function_rewrites?() and sum_reducer?(reducer), do: {:|>, pm, [lhs, {{:., dm, [{:__aliases__, am, [:Enum]}, :sum]}, m, []}]}, else: node end # `enum |> Enum.reduce(fn x, acc -> x + acc end)` => `enum |> Enum.sum()` defp style({:|>, pm, [lhs, {{:., dm, [{:__aliases__, am, [:Enum]}, :reduce]}, m, [reducer]}]} = node) do if Quokka.Config.inefficient_function_rewrites?() and sum_reducer?(reducer), do: {:|>, pm, [lhs, {{:., dm, [{:__aliases__, am, [:Enum]}, :sum]}, m, []}]}, else: node end @literal_zero_pattern quote do: {:__block__, _, [0]} @enum_count_pattern quote do: {{:., var!(m), [{_, _, [:Enum]}, :count]}, _, [var!(enum)]} @enum_count_with_fn_pattern quote do: {{:., var!(m), [{_, _, [:Enum]}, :count]}, _, [var!(enum), var!(func)]} @length_pattern quote do: {:length, var!(m), [var!(enum)]} for {lhs, rhs} <- [ # Enum.count(enum) == 0 => Enum.empty?(enum) {@enum_count_pattern, @literal_zero_pattern}, # 0 == Enum.count(enum) => Enum.empty?(enum) {@literal_zero_pattern, @enum_count_pattern}, # length(enum) == 0 => Enum.empty?(enum) {@length_pattern, @literal_zero_pattern}, # 0 == length(enum) => Enum.empty?(enum) {@literal_zero_pattern, @length_pattern} ] do defp style({op, _, [unquote(lhs), unquote(rhs)]} = node) when op in [:==, :===] do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., m, [{:__aliases__, m, [:Enum]}, :empty?]}, m, [enum]}, else: node end end for {lhs, rhs} <- [ # Enum.count(enum, fn) == 0 => not Enum.any?(enum, fn) {@enum_count_with_fn_pattern, @literal_zero_pattern}, # 0 == Enum.count(enum, fn) => not Enum.any?(enum, fn) {@literal_zero_pattern, @enum_count_with_fn_pattern} ] do defp style({op, _, [unquote(lhs), unquote(rhs)]} = node) when op in [:==, :===] do if Quokka.Config.inefficient_function_rewrites?(), do: {:not, m, [{{:., m, [{:__aliases__, m, [:Enum]}, :any?]}, m, [enum, func]}]}, else: node end end @pipe_to_length_pattern quote do: {:|>, var!(pm), [var!(lhs), {:length, var!(m), []}]} @pipe_to_count_pattern quote do: {:|>, var!(pm), [var!(lhs), {{:., var!(m), [{_, _, [:Enum]}, :count]}, _, []}]} @pipe_to_count_with_fn_pattern quote do: {:|>, var!(pm), [ var!(lhs), {{:., var!(m), [{_, _, [:Enum]}, :count]}, _, [var!(func)]} ]} for {lhs, rhs} <- [ # foo |> bar() |> length() == 0 => foo |> bar() |> Enum.empty?() {@pipe_to_length_pattern, @literal_zero_pattern}, # 0 == foo |> bar() |> length() => foo |> bar() |> Enum.empty?() {@literal_zero_pattern, @pipe_to_length_pattern}, # foo |> bar() |> Enum.count() == 0 => foo |> bar() |> Enum.empty?() {@pipe_to_count_pattern, @literal_zero_pattern}, # 0 == foo |> bar() |> Enum.count() => foo |> bar() |> Enum.empty?() {@literal_zero_pattern, @pipe_to_count_pattern} ] do defp style({op, _, [unquote(lhs), unquote(rhs)]} = node) when op in [:==, :===] do if Quokka.Config.inefficient_function_rewrites?(), do: {:|>, pm, [lhs, {{:., m, [{:__aliases__, m, [:Enum]}, :empty?]}, m, []}]}, else: node end end for {lhs, rhs, op} <- [ # foo |> bar() |> Enum.count(&my_fn/1) > 0 => foo |> bar() |> Enum.any?(&my_fn/1) {@pipe_to_count_with_fn_pattern, @literal_zero_pattern, :>}, # 0 < foo |> bar() |> Enum.count(&my_fn/1) => foo |> bar() |> Enum.any?(&my_fn/1) {@literal_zero_pattern, @pipe_to_count_with_fn_pattern, :<}, # foo |> bar() |> Enum.count(fn v -> length(v) end) != 0 => foo |> bar() |> Enum.any?(fn v -> length(v) end) {@pipe_to_count_with_fn_pattern, @literal_zero_pattern, :!=}, # 0 != foo |> bar() |> Enum.count(fn v -> length(v) end) => foo |> bar() |> Enum.any?(fn v -> length(v) end) {@literal_zero_pattern, @pipe_to_count_with_fn_pattern, :!=} ] do defp style({unquote(op), _, [unquote(lhs), unquote(rhs)]} = node) do if Quokka.Config.inefficient_function_rewrites?(), do: {:|>, pm, [lhs, {{:., m, [{:__aliases__, m, [:Enum]}, :any?]}, m, [func]}]}, else: node end end for {lhs, rhs, op} <- [ # Enum.count(enum) > 0 => not Enum.empty?(enum) {@enum_count_pattern, @literal_zero_pattern, :>}, # Enum.count(enum) != 0 => not Enum.empty?(enum) {@enum_count_pattern, @literal_zero_pattern, :!=}, # 0 < Enum.count(enum) => not Enum.empty?(enum) {@literal_zero_pattern, @enum_count_pattern, :<}, # 0 != Enum.count(enum) => not Enum.empty?(enum) {@literal_zero_pattern, @enum_count_pattern, :!=}, # length(enum) > 0 => not Enum.empty?(enum) {@length_pattern, @literal_zero_pattern, :>}, # length(enum) != 0 => not Enum.empty?(enum) {@length_pattern, @literal_zero_pattern, :!=}, # 0 < length(enum) => not Enum.empty?(enum) {@literal_zero_pattern, @length_pattern, :<}, # 0 != length(enum) => not Enum.empty?(enum) {@literal_zero_pattern, @length_pattern, :!=} ] do defp style({unquote(op), _, [unquote(lhs), unquote(rhs)]} = node) do if Quokka.Config.inefficient_function_rewrites?(), do: {:not, m, [{{:., m, [{:__aliases__, m, [:Enum]}, :empty?]}, m, [enum]}]}, else: node end end for {lhs, rhs, op} <- [ # Enum.count(enum, fn) > 0 => Enum.any?(enum, fn) {@enum_count_with_fn_pattern, @literal_zero_pattern, :>}, # Enum.count(enum, fn) != 0 => Enum.any?(enum, fn) {@enum_count_with_fn_pattern, @literal_zero_pattern, :!=}, # 0 < Enum.count(enum, fn) => Enum.any?(enum, fn) {@literal_zero_pattern, @enum_count_with_fn_pattern, :<}, # 0 != Enum.count(enum, fn) => Enum.any?(enum, fn) {@literal_zero_pattern, @enum_count_with_fn_pattern, :!=} ] do defp style({unquote(op), _, [unquote(lhs), unquote(rhs)]} = node) do if Quokka.Config.inefficient_function_rewrites?(), do: {{:., m, [{:__aliases__, m, [:Enum]}, :any?]}, m, [enum, func]}, else: node end end # ARROW REWRITES # `with`, `for` left arrow - if only we could write something this trivial for `->`! defp style({:<-, cm, [lhs, rhs]}), do: {:<-, cm, [put_matches_on_right(lhs), rhs]} # there's complexity to `:->` due to `cond` also utilizing the symbol but with different semantics. # thus, we have to have a clause for each place that `:->` can show up # `with` elses defp style({{:__block__, _, [:else]} = else_, arrows}), do: {else_, rewrite_arrows(arrows)} defp style({:case, cm, [head, [{do_, arrows}]]}), do: {:case, cm, [head, [{do_, rewrite_arrows(arrows)}]]} defp style({:fn, m, arrows}), do: {:fn, m, rewrite_arrows(arrows)} defp style({:to_timeout, meta, [[{{:__block__, um, [unit]}, {:*, _, [left, right]}}]]} = node) when unit in ~w(day hour minute second millisecond)a do [l, r] = Enum.map([left, right], fn {_, _, [x]} -> x _ -> nil end) {step, next_unit} = case unit do :day -> {7, :week} :hour -> {24, :day} :minute -> {60, :hour} :second -> {60, :minute} :millisecond -> {1000, :second} end if step in [l, r] do n = if l == step, do: right, else: left style({:to_timeout, meta, [[{{:__block__, um, [next_unit]}, n}]]}) else node end end defp style({:to_timeout, meta, [[{{:__block__, um, [unit]}, {:__block__, tm, [n]}}]]} = node) do step_up = case {unit, n} do {:day, 7} -> :week {:hour, 24} -> :day {:minute, 60} -> :hour {:second, 60} -> :minute {:millisecond, 1000} -> :second _ -> nil end if step_up do {:to_timeout, meta, [[{{:__block__, um, [step_up]}, {:__block__, [token: "1", line: tm[:line]], [1]}}]]} else node end end # Rewrite &variable.(&1) to just the variable when the first argument is # a simple variable name, but not an anonymous function argument. # # Do this: # &some_variable.(&1) => some_variable # But not: # & &1.(&2) => &1 (totally invalid syntax) defp style({:&, meta, [{{:., dot_meta, [{var_name, _meta, _context} = var]}, fun_meta, [{:&, arg_meta, [arg_num]}]}]}) when is_integer(arg_num) do if is_atom(var_name) and not anonymous_arg?(var_name) do var else {:&, meta, [{{:., dot_meta, [var]}, fun_meta, [{:&, arg_meta, [arg_num]}]}]} end end defp style({:&, meta, [{fun, fun_meta, [{:&, _, [arg_num]}]}]} = node) when is_integer(arg_num) do if Quokka.Config.inefficient_function_rewrites?() do fun_name = case fun do name when is_atom(name) -> {name, fun_meta, Elixir} {:., dot_meta, [module, method]} -> updated_meta = [no_parens: true] ++ (fun_meta |> Keyword.take([:line])) {{:., dot_meta, [module, method]}, updated_meta, []} end arity_meta = fun_meta |> Keyword.take([:line]) |> Keyword.put(:token, Integer.to_string(arg_num)) arity_block = {:__block__, arity_meta, [arg_num]} div_meta = Keyword.take(fun_meta, [:line]) {:&, meta, [{:/, div_meta, [fun_name, arity_block]}]} else node end end defp style(node), do: node # True when the name is something like :"&1" or :"&2" defp anonymous_arg?(name) when is_atom(name) do name |> to_string() |> String.starts_with?("&") end defp anonymous_arg?(_), do: false # Helper function to rewrite Repo.one calls in expressions defp rewrite_repo_one_in_conditional(ast) do # If the condition is a pattern match, don't rewrite any Repo.one calls inside it if is_pattern_match?(ast) do ast else Macro.prewalk(ast, fn {{:., dm, [{:__aliases__, alias_metadata, modules}, :one]}, function_metadata, args} = node -> if List.last(modules) == :Repo do {{:., dm, [{:__aliases__, alias_metadata, modules}, :exists?]}, function_metadata, args} else node end node -> node end) end end # Check if the AST represents a pattern match (assignment) defp is_pattern_match?({:=, _, _}), do: true defp is_pattern_match?(_), do: false # True when the reducer function just sums its two arguments (in either order). # Matches `fn a, b -> a + b end`, `fn a, b -> b + a end`, `&(&1 + &2)`, `&(&2 + &1)`, # `&+/2`, and `&Kernel.+/2`. defp sum_reducer?({:fn, _, [{:->, _, [[{a, _, ca}, {b, _, cb}], {:+, _, [{x, _, cx}, {y, _, cy}]}]}]}) when is_atom(a) and is_atom(b) and is_atom(x) and is_atom(y) and a != :_ and b != :_ and a != b do ({a, ca} == {x, cx} and {b, cb} == {y, cy}) or ({a, ca} == {y, cy} and {b, cb} == {x, cx}) end defp sum_reducer?({:&, _, [{:+, _, [{:&, _, [n1]}, {:&, _, [n2]}]}]}) when is_integer(n1) and is_integer(n2), do: {n1, n2} in [{1, 2}, {2, 1}] defp sum_reducer?({:&, _, [{:/, _, [{:+, _, nil}, {:__block__, _, [2]}]}]}), do: true defp sum_reducer?({:&, _, [{:/, _, [{{:., _, [{:__aliases__, _, [:Kernel]}, :+]}, _, []}, {:__block__, _, [2]}]}]}), do: true defp sum_reducer?(_), do: false defp replace_into({:., dm, [{_, am, _} = enum, _]}, collectable, rest) do case Quokka.Config.inefficient_function_rewrites?() and collectable do {{:., _, [{_, _, [mod]}, :new]}, _, []} when mod in ~w(Map Keyword MapSet)a -> {:., dm, [{:__aliases__, am, [mod]}, :new]} {:%{}, _, []} -> {:., dm, [{:__aliases__, am, [:Map]}, :new]} {:__block__, _, [[]]} -> if Enum.empty?(rest), do: {:., dm, [enum, :to_list]}, else: {:., dm, [enum, :map]} _ -> nil end end defp rewrite_arrows(arrows) when is_list(arrows), do: Enum.map(arrows, fn {:->, m, [lhs, rhs]} -> {:->, m, [put_matches_on_right(lhs), rhs]} end) defp rewrite_arrows(macros_or_something_crazy_oh_no_abooort), do: macros_or_something_crazy_oh_no_abooort defp put_matches_on_right(ast) do Macro.prewalk(ast, fn # `_ = var ->` => `var ->` {:=, _, [{:_, _, nil}, var]} -> var # `var = _ ->` => `var ->` {:=, _, [var, {:_, _, nil}]} -> var # `var = *match*` -> `*match -> var` {:=, m, [{_, _, nil} = var, match]} -> {:=, m, [match, var]} node -> node end) end defp delimit(token) do if Quokka.Config.exclude_nums_with_underscores?() and String.contains?(token, "_") do token else token |> String.to_charlist() |> remove_underscores([]) |> add_underscores([]) end end defp remove_underscores([?_ | rest], acc), do: remove_underscores(rest, acc) defp remove_underscores([digit | rest], acc), do: remove_underscores(rest, [digit | acc]) defp remove_underscores([], reversed_list), do: reversed_list defp add_underscores([a, b, c, d | rest], acc), do: add_underscores([d | rest], [?_, c, b, a | acc]) defp add_underscores(reversed_list, acc), do: reversed_list |> Enum.reverse(acc) |> to_string() # Guard-specific rewrites for length/1 comparisons # length(enum) == 0 => enum == [] defp style_guard({:==, m, [{:length, _, [enum]}, {:__block__, _, [0]}]}), do: {:==, m, [enum, {:__block__, m, [[]]}]} defp style_guard({:==, m, [{:__block__, _, [0]}, {:length, _, [enum]}]}), do: {:==, m, [{:__block__, m, [[]]}, enum]} # length(enum) != 0 => is_list(enum) and enum != [] defp style_guard({:!=, m, [{:length, _, [enum]}, {:__block__, _, [0]}]}), do: {:and, m, [{:is_list, m, [enum]}, {:!=, m, [enum, {:__block__, m, [[]]}]}]} defp style_guard({:!=, m, [{:__block__, _, [0]}, {:length, _, [enum]}]}), do: {:and, m, [{:is_list, m, [enum]}, {:!=, m, [{:__block__, m, [[]]}, enum]}]} # length(enum) > 0 => is_list(enum) and enum != [] defp style_guard({:>, m, [{:length, _, [enum]}, {:__block__, _, [0]}]}), do: {:and, m, [{:is_list, m, [enum]}, {:!=, m, [enum, {:__block__, m, [[]]}]}]} defp style_guard({:<, m, [{:__block__, _, [0]}, {:length, _, [enum]}]}), do: {:and, m, [{:is_list, m, [enum]}, {:!=, m, [{:__block__, m, [[]]}, enum]}]} # Fallback - no transformation defp style_guard(node), do: node # Check if the current node is inside a guard clause defp in_guard?(zipper) do in_guard?(zipper, false) end defp in_guard?(nil, found?), do: found? defp in_guard?(zipper, found?) do case Zipper.node(zipper) do # Function definition with guard {:def, _, [{:when, _, _} | _]} -> true {:defp, _, [{:when, _, _} | _]} -> true # Guard expression itself {:when, _, _} -> true # Abort the search upward when we hit the beginning of a block {{:__block__, _, [:do]}, _} -> false # Continue searching up the tree _ -> in_guard?(Zipper.up(zipper), found?) end end end