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