Current section
Files
Jump to
Current section
Files
lib/pattern/prefer_explicit_binary_arithmetic.ex
defmodule Credence.Pattern.PreferExplicitBinaryArithmetic do
@moduledoc """
Readability rule: flags piping into binary arithmetic functions like `rem/2`
or `div/2`. Piping obscures which argument is the dividend — prefer an
explicit call for clarity.
## Bad
String.length(input_string) |> rem(3)
numerator |> div(denominator)
## Good
rem(String.length(input_string), 3)
div(numerator, denominator)
"""
use Credence.Pattern.Rule
alias Credence.Issue
alias Credence.RuleHelpers
# Kernel binary arithmetic functions that are clearer as explicit calls.
@binary_arithmetic_fns ~w(rem div)a
@impl true
def check(ast, _opts) do
chained = chained_arith_pipes(ast)
{_ast, issues} =
Macro.prewalk(ast, [], fn
{:|>, pipe_meta, [left, {fun, fun_meta, [right]}]} = node, issues
when fun in @binary_arithmetic_fns and is_list(fun_meta) and
is_list(pipe_meta) and right != nil and left != nil ->
if standalone?(node, left, chained) do
line = Keyword.get(fun_meta, :line) || Keyword.get(pipe_meta, :line)
{node,
[
%Issue{
rule: :prefer_explicit_binary_arithmetic,
message:
"Piping into `#{fun}/2` obscures which argument is the dividend. " <>
"Use `#{fun}(a, b)` for clarity.",
meta: %{line: line}
}
| issues
]}
else
{node, issues}
end
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
@impl true
def fix_patches(ast, opts) do
chained = chained_arith_pipes(ast)
source = Keyword.get(opts, :source) || Sourceror.to_string(ast)
RuleHelpers.patches_from_ast_transform(ast, source, fn tree ->
Macro.prewalk(tree, fn
{:|>, _pipe_meta, [left, {fun, _fun_meta, [right]}]} = node
when fun in @binary_arithmetic_fns and right != nil and left != nil ->
if standalone?(node, left, chained), do: {fun, [], [left, right]}, else: node
node ->
node
end)
end)
end
# Only a *single* pipe (`x |> div(n)`) is flagged — not a div/rem step inside a
# longer chain, where the pipe form reads naturally
# (`diff |> div(1000) |> Integer.to_string()`).
defp standalone?(node, left, chained) do
not match?({:|>, _, _}, left) and node not in chained
end
# div/rem pipes that are the left operand of an enclosing pipe — i.e. piped
# onward into another step, so part of a chain rather than a single pipe.
defp chained_arith_pipes(ast) do
{_ast, acc} =
Macro.prewalk(ast, [], fn
{:|>, _, [{:|>, _, [_, {lfun, _, [_]}]} = inner, _right]} = node, acc
when lfun in @binary_arithmetic_fns ->
{node, [inner | acc]}
node, acc ->
{node, acc}
end)
acc
end
end