Comparisons must use ===/!== instead of ==/!=.
== coerces across numeric types — 1 == 1.0 is true — so a refactor that
changes a value from integer to float keeps every comparison silently passing.
=== only matches the same type and value, and fails loudly the moment types
drift.
# BAD
if user.age == 18, do: ...
if status != :active, do: ...
# GOOD
if user.age === 18, do: ...
if status !== :active, do: ...Ecto queries are exempt because the query DSL only compiles == and !=:
from(u in User, where: u.age == 18) # allowed
where(query, [u], u.age == 18) # allowed
dynamic([u], u.age == ^age) # allowedWhich calls are exempt is controlled by the :ignored_functions param. Bare and
imported calls match by function name; qualified calls are only exempt on
Ecto.Query itself or an alias of it (alias Ecto.Query, alias Ecto.Query, as: Q, alias Ecto.{Query, ...}), so Enum.join/2 sharing a name with the
:join entry never hides its arguments. Only the arguments of an exempt call
are skipped — a loose comparison beside a query call on the same line is still
reported.
Any comparison with Mix.env() on either side is deliberately allowed, in any
file — so the standard start_permanent: Mix.env() == :prod line in mix.exs
passes. Mix.env/0 returns an atom, which == cannot coerce, and calling
Mix.env/0 outside mix.exs is a different rule's job to catch.