Skip to main content

forge_lint/sol/med/
tautological_compare.rs

1use super::TautologicalCompare;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::{BinOpKind, Lit, LitKind},
8    sema::{
9        Gcx,
10        hir::{self, ElementaryType, Expr, ExprKind, TypeKind},
11        ty::TyKind,
12    },
13};
14
15declare_forge_lint!(
16    TAUTOLOGICAL_COMPARE,
17    Severity::Med,
18    "tautological-compare",
19    "comparing an expression with itself is always true or false"
20);
21
22impl<'hir> LateLintPass<'hir> for TautologicalCompare {
23    fn check_expr(
24        &mut self,
25        ctx: &LintContext,
26        gcx: Gcx<'hir>,
27        _hir: &'hir hir::Hir<'hir>,
28        expr: &'hir hir::Expr<'hir>,
29    ) {
30        if let ExprKind::Binary(left, op, right) = &expr.kind
31            && matches!(
32                op.kind,
33                BinOpKind::Lt
34                    | BinOpKind::Le
35                    | BinOpKind::Gt
36                    | BinOpKind::Ge
37                    | BinOpKind::Eq
38                    | BinOpKind::Ne
39            )
40            && exprs_equal(left, right)
41            && !operand_is_udvt(gcx, left)
42        {
43            ctx.emit(&TAUTOLOGICAL_COMPARE, expr.span);
44        }
45    }
46}
47
48/// Returns `true` if `expr`'s type is a user-defined value type (UDVT).
49///
50/// A UDVT can only be compared through a user-defined operator (`using {f as ==} for T global`),
51/// which dispatches to an arbitrary function instead of built-in equality, so `x == x` is not
52/// guaranteed to be tautological. Built-in comparisons only apply to elementary types, so skipping
53/// UDVT operands removes that false positive without missing any real self-comparison.
54/// See <https://soliditylang.org/blog/2023/02/22/user-defined-operators/>.
55fn operand_is_udvt<'hir>(gcx: Gcx<'hir>, expr: &Expr<'hir>) -> bool {
56    gcx.type_of_expr(expr.peel_parens().id)
57        .is_some_and(|ty| matches!(ty.peel_refs().kind, TyKind::Udvt(..)))
58}
59
60/// Structural equality for the side-effect-free expressions a self-comparison can involve:
61/// identifiers, member access, indexing (by an equal index), binary operations, elementary-type
62/// casts, `payable(...)`, the pure unary operators (`-`, `!`, `~`), and the ternary `c ? a : b`.
63/// Anything else (notably arbitrary calls, which may return different values or have side effects,
64/// and the `++`/`--` unary ops, which mutate) is treated as unequal, so the lint never fires on a
65/// comparison whose two sides could legitimately differ.
66fn exprs_equal<'hir>(a: &Expr<'hir>, b: &Expr<'hir>) -> bool {
67    match (&a.peel_parens().kind, &b.peel_parens().kind) {
68        (ExprKind::Ident(ra), ExprKind::Ident(rb)) => ra == rb,
69        (ExprKind::Lit(la), ExprKind::Lit(lb)) => literals_equal(la, lb),
70        (ExprKind::Member(ba, na), ExprKind::Member(bb, nb)) => {
71            na.name == nb.name && exprs_equal(ba, bb)
72        }
73        (ExprKind::Index(ba, ia), ExprKind::Index(bb, ib)) => {
74            exprs_equal(ba, bb) && opt_exprs_equal(*ia, *ib)
75        }
76        // Same binary operator over structurally-equal, side-effect-free operands (`a + b == a +
77        // b`, `x & mask == x & mask`). The operands' purity is enforced by the recursion.
78        (ExprKind::Binary(la, opa, ra), ExprKind::Binary(lb, opb, rb)) => {
79            opa.kind == opb.kind && exprs_equal(la, lb) && exprs_equal(ra, rb)
80        }
81        // Casts to the *same* elementary type (`uint256(x)`, `address(this)`) are pure conversions,
82        // so two such casts of structurally-equal operands are equal. The cast types must match:
83        // `uint256(x) == uint8(x)` is not tautological because the narrower cast can truncate.
84        // Restricted to elementary-type casts, never arbitrary calls (which may have side effects
85        // or return different values).
86        (ExprKind::Call(ca, args_a, _), ExprKind::Call(cb, args_b, _)) => {
87            match (cast_elem_type(ca), cast_elem_type(cb)) {
88                (Some(ea), Some(eb)) if ea == eb => {
89                    args_a.len() == 1
90                        && args_b.len() == 1
91                        && match (args_a.exprs().next(), args_b.exprs().next()) {
92                            (Some(ia), Some(ib)) => exprs_equal(ia, ib),
93                            _ => false,
94                        }
95                }
96                _ => false,
97            }
98        }
99        // `payable(x)` is a pure conversion to `address payable`; its operand's purity is enforced
100        // by the recursion.
101        (ExprKind::Payable(a), ExprKind::Payable(b)) => exprs_equal(a, b),
102        // Same unary operator, with no side effects: `++`/`--` mutate their operand, so
103        // `++x == ++x` is not tautological and must not be flagged.
104        (ExprKind::Unary(opa, a), ExprKind::Unary(opb, b)) => {
105            opa.kind == opb.kind && !opa.kind.has_side_effects() && exprs_equal(a, b)
106        }
107        // `c ? a : b` is side-effect-free when its three operands are.
108        (ExprKind::Ternary(ca, ta, fa), ExprKind::Ternary(cb, tb, fb)) => {
109            exprs_equal(ca, cb) && exprs_equal(ta, tb) && exprs_equal(fa, fb)
110        }
111        _ => false,
112    }
113}
114
115/// If `callee` is an elementary-type name used as a cast (`uint256`, `address`, `bytesN`, ...),
116/// returns that type, else `None`.
117fn cast_elem_type<'a>(callee: &'a Expr<'_>) -> Option<&'a ElementaryType> {
118    match &callee.peel_parens().kind {
119        ExprKind::Type(hir::Type { kind: TypeKind::Elementary(e), .. }) => Some(e),
120        _ => None,
121    }
122}
123
124fn literals_equal(a: &Lit<'_>, b: &Lit<'_>) -> bool {
125    match (&a.kind, &b.kind) {
126        (LitKind::Str(ak, av, _), LitKind::Str(bk, bv, _)) => ak == bk && av == bv,
127        (LitKind::Number(a), LitKind::Number(b)) => a == b,
128        (LitKind::Rational(a), LitKind::Rational(b)) => a == b,
129        (LitKind::Address(a), LitKind::Address(b)) => a == b,
130        (LitKind::Bool(a), LitKind::Bool(b)) => a == b,
131        _ => false,
132    }
133}
134
135fn opt_exprs_equal<'hir>(a: Option<&Expr<'hir>>, b: Option<&Expr<'hir>>) -> bool {
136    match (a, b) {
137        (Some(a), Some(b)) => exprs_equal(a, b),
138        (None, None) => true,
139        _ => false,
140    }
141}