Skip to main content

forge_lint/sol/high/
incorrect_exp.rs

1use super::IncorrectExp;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use alloy_primitives::U256;
7use solar::{
8    ast::{BinOpKind, LitKind},
9    sema::{
10        Gcx,
11        hir::{self, ElementaryType, Expr, ExprKind, TypeKind},
12    },
13};
14
15declare_forge_lint!(
16    INCORRECT_EXP,
17    Severity::High,
18    "incorrect-exp",
19    "`^` is bitwise xor, not exponentiation; use `**`"
20);
21
22impl<'hir> LateLintPass<'hir> for IncorrectExp {
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        // `a ^ b` between integer literals is almost always a mistake for `a ** b`: `^` is bitwise
31        // xor in Solidity, so `10 ^ 18` is `24`, not `10 ** 18`.
32        //
33        // To stay precise, the base is restricted to `2` and `10` (bit widths and decimals, the
34        // only bases people write as powers) and hex operands are left alone. This mirrors GCC's
35        // and Clang's `-Wxor-used-as-pow`; Clippy's `suspicious_xor_used_as_pow`, which drops the
36        // base restriction, is allow-by-default precisely because of the resulting false positives.
37        if let ExprKind::Binary(lhs, op, rhs) = &expr.kind
38            && matches!(op.kind, BinOpKind::BitXor)
39            && let Some(base) = plain_decimal_int_lit(ctx, lhs)
40            && (base == U256::from(2u64) || base == U256::from(10u64))
41            && plain_decimal_int_lit(ctx, rhs).is_some()
42        {
43            ctx.emit(&INCORRECT_EXP, expr.span);
44        }
45    }
46}
47
48/// Returns the value of a plain decimal integer literal, looking through parentheses and integer
49/// casts (`uint256(10)`), or `None` for anything else.
50///
51/// Only literals written as plain decimal digits (`10`, `1_000`) qualify. Hex literals (`0x..`) are
52/// bitwise intent, and scientific notation (`1e1`, which solar evaluates to `10`) is not the plain
53/// integer literal the `^`/`**` typo involves. A sub-denomination (`2 wei`, `2 seconds`) is dropped
54/// from the HIR but still present in the source span, so it is filtered out too. All of these are
55/// left alone: this lint prefers a false negative to a false positive that would annoy developers.
56fn plain_decimal_int_lit(ctx: &LintContext, expr: &Expr<'_>) -> Option<U256> {
57    let expr = peel_int_casts(expr);
58    if let ExprKind::Lit(lit) = &expr.kind
59        && let LitKind::Number(value) = &lit.kind
60    {
61        let s = lit.symbol.as_str();
62        if !s.is_empty()
63            && s.bytes().all(|b| b.is_ascii_digit() || b == b'_')
64            // The source span must be exactly those digits. This rejects a sub-denomination such as
65            // `2 wei` (dropped from the HIR but still in the source). If the source is unavailable,
66            // err toward not flagging.
67            && ctx.span_to_snippet(expr.span).is_some_and(|src| src.trim() == s)
68        {
69            return Some(*value);
70        }
71    }
72    None
73}
74
75/// Looks through parentheses and integer casts (`uint256(x)`, `int8(x)`), returning the innermost
76/// operand. A misplaced `^` can hide behind such a cast (`uint256(10) ^ 18`), which a bare literal
77/// check would miss. Non-integer casts (`bytes32(x)`, `address(x)`) are left alone, since xor of a
78/// `bytesN` bit pattern is a legitimate operation.
79fn peel_int_casts<'a, 'hir>(expr: &'a Expr<'hir>) -> &'a Expr<'hir> {
80    let expr = expr.peel_parens();
81    if let ExprKind::Call(callee, args, _) = &expr.kind
82        && let ExprKind::Type(hir::Type {
83            kind: TypeKind::Elementary(ElementaryType::Int(_) | ElementaryType::UInt(_)),
84            ..
85        }) = &callee.peel_parens().kind
86        && args.len() == 1
87        && let Some(inner) = args.exprs().next()
88    {
89        return peel_int_casts(inner);
90    }
91    expr
92}