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, VariableId},
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        // A pair of comparisons can cover the complete type range even when neither
33        // comparison is tautological on its own, e.g. `x > 0 || x == 0` for `uint`.
34        if op.kind == BinOpKind::Or
35            && let (Some(left), Some(right)) = (comparison_of(hir, left), comparison_of(hir, right))
36            && is_boundary_composition(&left, &right)
37        {
38            ctx.emit(&TYPE_BASED_TAUTOLOGY, expr.span);
39            return;
40        }
41
42        // Only relational/equality comparisons can produce tautologies via type bounds.
43        if !matches!(
44            op.kind,
45            BinOpKind::Lt
46                | BinOpKind::Le
47                | BinOpKind::Gt
48                | BinOpKind::Ge
49                | BinOpKind::Eq
50                | BinOpKind::Ne
51        ) {
52            return;
53        }
54
55        // var op const
56        if let Some(elem_ty) = elem_type_of(hir, left)
57            && let Some((val_neg, val_mag)) = lit_value_of(right)
58            && is_tautology(elem_ty, val_neg, val_mag, op.kind)
59        {
60            ctx.emit(&TYPE_BASED_TAUTOLOGY, expr.span);
61            return;
62        }
63
64        // const op var: swap operands and flip the operator
65        if let Some((val_neg, val_mag)) = lit_value_of(left)
66            && let Some(elem_ty) = elem_type_of(hir, right)
67            && is_tautology(elem_ty, val_neg, val_mag, flip(op.kind))
68        {
69            ctx.emit(&TYPE_BASED_TAUTOLOGY, expr.span);
70        }
71    }
72}
73
74#[derive(Clone)]
75struct Comparison {
76    variable: VariableId,
77    cast_path: Vec<ElementaryType>,
78    range: IntegerRange,
79    op: BinOpKind,
80    val_neg: bool,
81    val_mag: U256,
82}
83
84#[derive(Clone, Copy, PartialEq, Eq)]
85struct IntegerRange {
86    lower: (bool, U256),
87    upper: (bool, U256),
88}
89
90/// Extracts a comparison over one resolved integer variable, normalizing constants on the left.
91fn comparison_of<'hir>(
92    hir: &'hir hir::Hir<'hir>,
93    expr: &'hir hir::Expr<'hir>,
94) -> Option<Comparison> {
95    let ExprKind::Binary(left, op, right) = expr.peel_parens().kind else { return None };
96    if !matches!(
97        op.kind,
98        BinOpKind::Lt
99            | BinOpKind::Le
100            | BinOpKind::Gt
101            | BinOpKind::Ge
102            | BinOpKind::Eq
103            | BinOpKind::Ne
104    ) {
105        return None;
106    }
107
108    if let (Some((variable, cast_path, range)), Some((val_neg, val_mag))) =
109        (comparison_operand_of(hir, left), lit_value_of(right))
110    {
111        return Some(Comparison { variable, cast_path, range, op: op.kind, val_neg, val_mag });
112    }
113
114    if let (Some((val_neg, val_mag)), Some((variable, cast_path, range))) =
115        (lit_value_of(left), comparison_operand_of(hir, right))
116    {
117        return Some(Comparison {
118            variable,
119            cast_path,
120            range,
121            op: flip(op.kind),
122            val_neg,
123            val_mag,
124        });
125    }
126
127    None
128}
129
130/// Returns true for boundary comparisons whose union covers the complete integer type range.
131fn is_boundary_composition(left: &Comparison, right: &Comparison) -> bool {
132    if left.variable != right.variable
133        || left.cast_path != right.cast_path
134        || left.range != right.range
135    {
136        return false;
137    }
138
139    let lower = left.range.lower;
140    let upper = left.range.upper;
141
142    // Values greater than the minimum plus the minimum itself cover the whole range.
143    (matches_comparison(left, BinOpKind::Gt, lower) && is_lower_point(right, lower))
144        || (matches_comparison(right, BinOpKind::Gt, lower) && is_lower_point(left, lower))
145        // Values below the maximum plus the maximum itself cover the whole range.
146        || (matches_comparison(left, BinOpKind::Lt, upper) && is_upper_point(right, upper))
147        || (matches_comparison(right, BinOpKind::Lt, upper) && is_upper_point(left, upper))
148        // Strict comparisons against opposite boundaries cover the whole range.
149        || (matches_comparison(left, BinOpKind::Gt, lower)
150            && matches_comparison(right, BinOpKind::Lt, upper))
151        || (matches_comparison(right, BinOpKind::Gt, lower)
152            && matches_comparison(left, BinOpKind::Lt, upper))
153}
154
155fn matches_comparison(comparison: &Comparison, op: BinOpKind, value: (bool, U256)) -> bool {
156    comparison.op == op && comparison.val_neg == value.0 && comparison.val_mag == value.1
157}
158
159fn is_lower_point(comparison: &Comparison, lower: (bool, U256)) -> bool {
160    (comparison.op == BinOpKind::Eq || comparison.op == BinOpKind::Le)
161        && comparison.val_neg == lower.0
162        && comparison.val_mag == lower.1
163}
164
165fn is_upper_point(comparison: &Comparison, upper: (bool, U256)) -> bool {
166    (comparison.op == BinOpKind::Eq || comparison.op == BinOpKind::Ge)
167        && comparison.val_neg == upper.0
168        && comparison.val_mag == upper.1
169}
170
171fn integer_bounds(ty: ElementaryType) -> Option<IntegerRange> {
172    match ty {
173        ElementaryType::UInt(size) => {
174            let bits = size.bits();
175            let upper =
176                if bits == 256 { U256::MAX } else { (U256::from(1u8) << bits) - U256::from(1u8) };
177            Some(IntegerRange { lower: (false, U256::ZERO), upper: (false, upper) })
178        }
179        ElementaryType::Int(size) => {
180            let half = U256::from(1u8) << (size.bits() - 1);
181            Some(IntegerRange { lower: (true, half), upper: (false, half - U256::from(1u8)) })
182        }
183        _ => None,
184    }
185}
186
187/// Returns the equivalent operator after swapping left and right operands.
188/// e.g. `const < var` rewritten as `var > const` needs `Gt`.
189const fn flip(op: BinOpKind) -> BinOpKind {
190    match op {
191        BinOpKind::Lt => BinOpKind::Gt,
192        BinOpKind::Le => BinOpKind::Ge,
193        BinOpKind::Gt => BinOpKind::Lt,
194        BinOpKind::Ge => BinOpKind::Le,
195        BinOpKind::Eq | BinOpKind::Ne => op, // symmetric
196        _ => unreachable!(),
197    }
198}
199
200/// Returns true if `var <op> val` is always true or always false for every value in the
201/// type's range.
202///
203/// The constant is represented as a sign bit (`val_neg`) and a magnitude (`val_mag`), matching
204/// how solar stores negated literals (e.g. `-128` -> `Unary(Neg, Lit(128))`).
205fn is_tautology(ty: ElementaryType, val_neg: bool, val_mag: U256, op: BinOpKind) -> bool {
206    match ty {
207        ElementaryType::UInt(size) => {
208            // lo = 0, hi = 2^bits - 1
209            let bits = size.bits();
210            let hi =
211                if bits == 256 { U256::MAX } else { (U256::from(1u8) << bits) - U256::from(1u8) };
212            let val_lt_lo = val_neg && val_mag != U256::ZERO; // val < 0
213            let val_le_lo = val_neg || val_mag == U256::ZERO; // val <= 0
214            let hi_lt_val = !val_neg && val_mag > hi; // val > hi
215            let hi_le_val = !val_neg && val_mag >= hi; // val >= hi
216            match op {
217                BinOpKind::Gt | BinOpKind::Le => hi_le_val || val_lt_lo,
218                BinOpKind::Ge | BinOpKind::Lt => val_le_lo || hi_lt_val,
219                BinOpKind::Eq | BinOpKind::Ne => hi_lt_val || val_lt_lo,
220                _ => false,
221            }
222        }
223        ElementaryType::Int(size) => {
224            // lo = -(2^(bits-1)), hi = 2^(bits-1) - 1
225            let bits = size.bits();
226            let half = U256::from(1u8) << (bits - 1); // 2^(bits-1)
227            let hi = half - U256::from(1u8); // 2^(bits-1) - 1
228            let val_lt_lo = val_neg && val_mag > half; // val < -half
229            let val_le_lo = val_neg && val_mag >= half; // val <= -half
230            let hi_lt_val = !val_neg && val_mag > hi; // val > hi
231            let hi_le_val = !val_neg && val_mag >= hi; // val >= hi
232            match op {
233                BinOpKind::Gt | BinOpKind::Le => hi_le_val || val_lt_lo,
234                BinOpKind::Ge | BinOpKind::Lt => val_le_lo || hi_lt_val,
235                BinOpKind::Eq | BinOpKind::Ne => hi_lt_val || val_lt_lo,
236                _ => false,
237            }
238        }
239        _ => false,
240    }
241}
242
243/// Extracts the elementary integer type from a variable reference or explicit cast.
244fn elem_type_of<'hir>(
245    hir: &'hir hir::Hir<'hir>,
246    expr: &'hir hir::Expr<'hir>,
247) -> Option<ElementaryType> {
248    match &expr.peel_parens().kind {
249        ExprKind::Ident(resolutions) => {
250            if let Some(Res::Item(ItemId::Variable(var_id))) = resolutions.first()
251                && let TypeKind::Elementary(ty) = hir.variable(*var_id).ty.kind
252            {
253                return Some(ty);
254            }
255            None
256        }
257        // Explicit cast: `uint8(x)`, the cast type determines the effective range.
258        ExprKind::Call(call_expr, _, _) => {
259            if let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) =
260                &call_expr.kind
261            {
262                return Some(*ty);
263            }
264            None
265        }
266        _ => None,
267    }
268}
269
270/// Extracts a stable variable identity and the reachable integer range of an operand.
271///
272/// Explicit casts can change the range used for the comparison, but not necessarily the
273/// underlying value being compared. Keeping the cast path as part of the identity lets boundary
274/// compositions recognize identical nested casts without treating different conversions as
275/// identical.
276fn comparison_operand_of<'hir>(
277    hir: &'hir hir::Hir<'hir>,
278    expr: &'hir hir::Expr<'hir>,
279) -> Option<(VariableId, Vec<ElementaryType>, IntegerRange)> {
280    match &expr.peel_parens().kind {
281        ExprKind::Ident(resolutions) => {
282            if let Some(Res::Item(ItemId::Variable(variable))) = resolutions.first()
283                && let TypeKind::Elementary(ty) = hir.variable(*variable).ty.kind
284            {
285                return integer_bounds(ty).map(|range| (*variable, Vec::new(), range));
286            }
287        }
288        ExprKind::Call(call_expr, args, _) => {
289            let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) = &call_expr.kind
290            else {
291                return None;
292            };
293            if !matches!(ty, ElementaryType::Int(_) | ElementaryType::UInt(_)) {
294                return None;
295            }
296
297            let mut exprs = args.exprs();
298            let inner = exprs.next()?;
299            if exprs.next().is_some() {
300                return None;
301            }
302            let (variable, mut cast_path, source_range) = comparison_operand_of(hir, inner)?;
303            let source_type = expression_type(hir, inner)?;
304            let range = effective_range_for_cast(source_type, source_range, *ty)?;
305            // A same-signed widening cast preserves the operand's value and does not
306            // need to distinguish this comparison from the uncast expression.
307            if !is_value_preserving_widening(source_type, *ty) {
308                cast_path.push(*ty);
309            }
310            return Some((variable, cast_path, range));
311        }
312        _ => {}
313    }
314    None
315}
316
317fn effective_range_for_cast(
318    source_type: ElementaryType,
319    source_range: IntegerRange,
320    target_type: ElementaryType,
321) -> Option<IntegerRange> {
322    if is_value_preserving_widening(source_type, target_type) {
323        Some(source_range)
324    } else {
325        integer_bounds(target_type)
326    }
327}
328
329const fn is_value_preserving_widening(
330    source_type: ElementaryType,
331    target_type: ElementaryType,
332) -> bool {
333    match (source_type, target_type) {
334        (ElementaryType::UInt(source), ElementaryType::UInt(target))
335        | (ElementaryType::Int(source), ElementaryType::Int(target)) => {
336            source.bits() <= target.bits()
337        }
338        _ => false,
339    }
340}
341
342fn expression_type<'hir>(
343    hir: &'hir hir::Hir<'hir>,
344    expr: &'hir hir::Expr<'hir>,
345) -> Option<ElementaryType> {
346    match &expr.peel_parens().kind {
347        ExprKind::Ident(resolutions) => {
348            if let Some(Res::Item(ItemId::Variable(variable))) = resolutions.first()
349                && let TypeKind::Elementary(ty) = hir.variable(*variable).ty.kind
350            {
351                return Some(ty);
352            }
353        }
354        ExprKind::Call(call_expr, _, _) => {
355            if let ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ty), .. }) =
356                &call_expr.kind
357            {
358                return Some(*ty);
359            }
360        }
361        _ => {}
362    }
363    None
364}
365
366/// Extracts a signed constant from a numeric literal or negated numeric literal,
367/// returning `(is_negative, magnitude)`.
368fn lit_value_of(expr: &hir::Expr<'_>) -> Option<(bool, U256)> {
369    match &expr.peel_parens().kind {
370        ExprKind::Lit(lit) => {
371            if let LitKind::Number(n) = lit.kind {
372                return Some(normalize_zero(false, n));
373            }
374            None
375        }
376        ExprKind::Unary(op, inner) if op.kind == UnOpKind::Neg => {
377            if let ExprKind::Lit(lit) = &inner.peel_parens().kind
378                && let LitKind::Number(n) = lit.kind
379            {
380                return Some(normalize_zero(true, n));
381            }
382            None
383        }
384        _ => None,
385    }
386}
387
388fn normalize_zero(is_negative: bool, magnitude: U256) -> (bool, U256) {
389    if magnitude.is_zero() { (false, U256::ZERO) } else { (is_negative, magnitude) }
390}