Skip to main content

forge_lint/sol/med/
tautology.rs

1use super::TypeBasedTautology;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use alloy_primitives::U256;
7use solar::{
8    ast::{BinOpKind, LitKind, UnOpKind},
9    sema::{
10        Gcx,
11        hir::{self, ElementaryType, ExprKind, ItemId, Res, TypeKind},
12    },
13};
14
15declare_forge_lint!(
16    TYPE_BASED_TAUTOLOGY,
17    Severity::Med,
18    "type-based-tautology",
19    "condition is always true or false based on the variable's type"
20);
21
22impl<'hir> LateLintPass<'hir> for TypeBasedTautology {
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        let ExprKind::Binary(left, op, right) = &expr.kind else { return };
31
32        // Only relational/equality comparisons can produce tautologies via type bounds.
33        if !matches!(
34            op.kind,
35            BinOpKind::Lt
36                | BinOpKind::Le
37                | BinOpKind::Gt
38                | BinOpKind::Ge
39                | BinOpKind::Eq
40                | BinOpKind::Ne
41        ) {
42            return;
43        }
44
45        // var op const
46        if let Some(elem_ty) = elem_type_of(hir, left)
47            && let Some((val_neg, val_mag)) = lit_value_of(right)
48            && is_tautology(elem_ty, val_neg, val_mag, op.kind)
49        {
50            ctx.emit(&TYPE_BASED_TAUTOLOGY, expr.span);
51            return;
52        }
53
54        // const op var: swap operands and flip the operator
55        if let Some((val_neg, val_mag)) = lit_value_of(left)
56            && let Some(elem_ty) = elem_type_of(hir, right)
57            && is_tautology(elem_ty, val_neg, val_mag, flip(op.kind))
58        {
59            ctx.emit(&TYPE_BASED_TAUTOLOGY, expr.span);
60        }
61    }
62}
63
64/// Returns the equivalent operator after swapping left and right operands.
65/// e.g. `const < var` rewritten as `var > const` needs `Gt`.
66const fn flip(op: BinOpKind) -> BinOpKind {
67    match op {
68        BinOpKind::Lt => BinOpKind::Gt,
69        BinOpKind::Le => BinOpKind::Ge,
70        BinOpKind::Gt => BinOpKind::Lt,
71        BinOpKind::Ge => BinOpKind::Le,
72        BinOpKind::Eq | BinOpKind::Ne => op, // symmetric
73        _ => unreachable!(),
74    }
75}
76
77/// Returns true if `var <op> val` is always true or always false for every value in the
78/// type's range.
79///
80/// The constant is represented as a sign bit (`val_neg`) and a magnitude (`val_mag`), matching
81/// how solar stores negated literals (e.g. `-128` -> `Unary(Neg, Lit(128))`).
82fn is_tautology(ty: ElementaryType, val_neg: bool, val_mag: U256, op: BinOpKind) -> bool {
83    match ty {
84        ElementaryType::UInt(size) => {
85            // lo = 0, hi = 2^bits - 1
86            let bits = size.bits();
87            let hi =
88                if bits == 256 { U256::MAX } else { (U256::from(1u8) << bits) - U256::from(1u8) };
89            let val_lt_lo = val_neg && val_mag != U256::ZERO; // val < 0
90            let val_le_lo = val_neg || val_mag == U256::ZERO; // val <= 0
91            let hi_lt_val = !val_neg && val_mag > hi; // val > hi
92            let hi_le_val = !val_neg && val_mag >= hi; // val >= hi
93            match op {
94                BinOpKind::Gt | BinOpKind::Le => hi_le_val || val_lt_lo,
95                BinOpKind::Ge | BinOpKind::Lt => val_le_lo || hi_lt_val,
96                BinOpKind::Eq | BinOpKind::Ne => hi_lt_val || val_lt_lo,
97                _ => false,
98            }
99        }
100        ElementaryType::Int(size) => {
101            // lo = -(2^(bits-1)), hi = 2^(bits-1) - 1
102            let bits = size.bits();
103            let half = U256::from(1u8) << (bits - 1); // 2^(bits-1)
104            let hi = half - U256::from(1u8); // 2^(bits-1) - 1
105            let val_lt_lo = val_neg && val_mag > half; // val < -half
106            let val_le_lo = val_neg && val_mag >= half; // val <= -half
107            let hi_lt_val = !val_neg && val_mag > hi; // val > hi
108            let hi_le_val = !val_neg && val_mag >= hi; // val >= hi
109            match op {
110                BinOpKind::Gt | BinOpKind::Le => hi_le_val || val_lt_lo,
111                BinOpKind::Ge | BinOpKind::Lt => val_le_lo || hi_lt_val,
112                BinOpKind::Eq | BinOpKind::Ne => hi_lt_val || val_lt_lo,
113                _ => false,
114            }
115        }
116        _ => false,
117    }
118}
119
120/// Extracts the elementary integer type from a variable reference or explicit cast.
121fn elem_type_of<'hir>(
122    hir: &'hir hir::Hir<'hir>,
123    expr: &'hir hir::Expr<'hir>,
124) -> Option<ElementaryType> {
125    match &expr.peel_parens().kind {
126        ExprKind::Ident(resolutions) => {
127            if let Some(Res::Item(ItemId::Variable(var_id))) = resolutions.first()
128                && let TypeKind::Elementary(ty) = hir.variable(*var_id).ty.kind
129            {
130                return Some(ty);
131            }
132            None
133        }
134        // Explicit cast: `uint8(x)`, the cast type determines the effective range.
135        ExprKind::Call(call_expr, _, _) => {
136            if let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) =
137                &call_expr.kind
138            {
139                return Some(*ty);
140            }
141            None
142        }
143        _ => None,
144    }
145}
146
147/// Extracts a signed constant from a numeric literal or negated numeric literal,
148/// returning `(is_negative, magnitude)`.
149fn lit_value_of(expr: &hir::Expr<'_>) -> Option<(bool, U256)> {
150    match &expr.peel_parens().kind {
151        ExprKind::Lit(lit) => {
152            if let LitKind::Number(n) = lit.kind {
153                return Some((false, n));
154            }
155            None
156        }
157        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Neg => {
158            if let ExprKind::Lit(lit) = &inner.peel_parens().kind
159                && let LitKind::Number(n) = lit.kind
160            {
161                return Some((true, n));
162            }
163            None
164        }
165        _ => None,
166    }
167}