Current section

Files

Jump to
credence lib rule descriptive_names.ex
Raw

lib/rule/descriptive_names.ex

defmodule Credence.Rule.DescriptiveNames do
@moduledoc """
Maintainability rule: Flags single-letter variable names in function signatures.
Using names like `a`, `b`, or `n` makes the code harder to reason about.
Replacing them with descriptive names like `index`, `accumulator`, or `previous_value`
improves readability and reduces cognitive load.
## Bad
def process(a, b), do: a + b
## Good
def process(base_value, increment), do: base_value + increment
"""
@behaviour Credence.Rule
alias Credence.Issue
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
{kind, meta, [{_name, _, args}, _body]}, issues when kind in [:def, :defp] ->
# args can be nil for functions with no arguments like `def start, do: :ok`
found_names = find_short_params(args || [], [])
{nil, format_issues(found_names, meta) ++ issues}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
# Case 1: A list (the argument list itself or pattern matches like [h | t])
defp find_short_params(args, acc) when is_list(args) do
Enum.reduce(args, acc, &find_short_params/2)
end
# Case 2: A variable (3-tuple where the 3rd element is an atom/nil, e.g., {:x, meta, nil})
defp find_short_params({name, _meta, context}, acc) when is_atom(name) and is_atom(context) do
str_name = Atom.to_string(name)
if String.length(str_name) == 1 and str_name != "_" do
[str_name | acc]
else
acc
end
end
# Case 3: A 3-tuple AST node (e.g., {:+, meta, [args]}) - recurse into the args list
defp find_short_params({_name, _meta, args}, acc) when is_list(args) do
find_short_params(args, acc)
end
# Case 4: A 2-tuple literal (e.g., {a, b}) - correctly pass the accumulator through
defp find_short_params({left, right}, acc) do
acc = find_short_params(left, acc)
find_short_params(right, acc)
end
# Case 5: Catch-all for literals or things we don't care about
defp find_short_params(_, acc), do: acc
defp format_issues(names, meta) do
names
|> Enum.uniq()
|> Enum.map(fn name ->
%Issue{
rule: :descriptive_names,
severity: :warning,
message: "The parameter `#{name}` is a single letter. Use a more descriptive name.",
meta: %{line: Keyword.get(meta, :line)}
}
end)
end
end