Current section

Files

Jump to
credence lib rule no_is_prefix.ex
Raw

lib/rule/no_is_prefix.ex

defmodule Credence.Rule.NoIsPrefix do
@moduledoc """
Style rule: Detects functions named with an `is_` prefix that are not
guard-safe BIFs.
In Elixir, predicate functions use a trailing `?` by convention (e.g.
`valid?/1`, `palindrome?/1`). The `is_` prefix is reserved for guard-safe
type checks from Erlang (`is_list/1`, `is_integer/1`, etc.). User-defined
functions named `is_foo` look like guards but aren't usable in guards,
which misleads readers.
## Bad
def is_valid_palindrome(s), do: ...
defp is_empty(list), do: ...
## Good
def valid_palindrome?(s), do: ...
defp empty?(list), do: ...
"""
@behaviour Credence.Rule
alias Credence.Issue
# Guard-safe BIFs from Erlang that legitimately use is_ prefix.
# We skip these so we don't flag wrapper functions that shadow them.
@erlang_guards ~w(
is_atom is_binary is_bitstring is_boolean is_exception is_float
is_function is_integer is_list is_map is_map_key is_nil
is_number is_pid is_port is_reference is_struct is_tuple
)a
@impl true
def check(ast, _opts) do
{_ast, issues} =
Macro.prewalk(ast, [], fn
# 1. Guarded: def is_foo(args) when ..., do: ...
# (Matched first because it's more specific)
{kind, meta, [{:when, _, [{name, _, _params}, _guard]}, _body]} = node, issues
when kind in [:def, :defp] and is_atom(name) ->
{node, maybe_flag(name, kind, meta, issues)}
# 2. Unguarded: def is_foo(args), do: ...
# (Added `name != :when` to prevent swallowing guarded functions)
{kind, meta, [{name, _, _params}, _body]} = node, issues
when kind in [:def, :defp] and is_atom(name) and name != :when ->
{node, maybe_flag(name, kind, meta, issues)}
node, issues ->
{node, issues}
end)
# Deduplicate — multi-clause functions produce multiple AST nodes
issues
|> Enum.reverse()
|> Enum.uniq_by(fn issue -> issue.message end)
end
defp maybe_flag(name, _kind, meta, issues) do
name_str = Atom.to_string(name)
if String.starts_with?(name_str, "is_") and name not in @erlang_guards do
suggested = name_str |> String.trim_leading("is_") |> Kernel.<>("?")
issue = %Issue{
rule: :no_is_prefix,
severity: :info,
message:
"Function `#{name_str}/` uses an `is_` prefix. In Elixir, predicates use a `?` suffix " <>
"by convention (e.g. `#{suggested}`). The `is_` prefix is reserved for guard-safe type checks.",
meta: %{line: Keyword.get(meta, :line)}
}
[issue | issues]
else
issues
end
end
end