Current section

Files

Jump to
credence lib pattern no_kernel_shadowing.ex
Raw

lib/pattern/no_kernel_shadowing.ex

defmodule Credence.Pattern.NoKernelShadowing do
@moduledoc """
Idiomatic rule: warns when variables shadow `Kernel` functions.
Using `max` or `min` as variable names (e.g., in `Enum.reduce` or function
arguments) shadows the built-in Kernel functions.
While `max(max, value)` is valid Elixir, it is considered unidiomatic and
can lead to confusion or subtle bugs.
Prefer:
- `max_val`, `global_max`, or `limit` instead of `max`
- `min_val`, `local_min`, or `bottom` instead of `min`
"""
use Credence.Pattern.Rule
alias Credence.Issue
@shadowed [
:max,
:min,
:elem,
:hd,
:tl,
:length,
:abs,
:round,
:trunc,
:div,
:rem,
:tuple_size,
:map_size,
:byte_size,
:bit_size
]
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# Match operator (=)
{:=, meta, [lhs, _rhs]} = node, issues ->
new_issues = extract_vars(lhs, meta)
{node, new_issues ++ issues}
# Match fn parameters
{:fn, _meta, clauses} = node, issues ->
vars =
clauses
|> Enum.flat_map(fn {:->, meta, [params, _body]} ->
extract_vars_from_list(params, meta)
end)
{node, vars ++ issues}
# Match def/defp function arguments
{def_type, _meta, [{_name, meta, args}, _body]} = node, issues
when def_type in [:def, :defp] and is_list(args) ->
vars = extract_vars_from_list(args, meta)
{node, vars ++ issues}
node, issues ->
{node, issues}
end)
Enum.reverse(issues)
end
# --- Helpers ---
defp extract_vars(ast, meta) do
Macro.prewalk(ast, [], fn
{name, var_meta, ctx} = node, acc
when name in @shadowed and is_atom(ctx) ->
issue = trigger_issue(var_meta || meta, name)
{node, [issue | acc]}
node, acc ->
{node, acc}
end)
|> elem(1)
end
defp extract_vars_from_list(list, meta) do
Enum.flat_map(list, &extract_vars(&1, meta))
end
defp trigger_issue(meta, name) do
%Issue{
rule: :no_kernel_shadowing,
message: """
The variable `#{name}` shadows the built-in `Kernel.#{name}/2` function.
This is unidiomatic and can make code harder to read, especially when
calling `#{name}(#{name}, other)`.
Consider renaming this variable to `#{name}_val`, `global_#{name}`, or
something more descriptive.
""",
meta: %{line: Keyword.get(meta, :line)}
}
end
end