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, Lit, 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<'gcx> LateLintPass<'gcx> for IncorrectExp {
23    fn check_expr(&mut self, ctx: &LintContext, _gcx: Gcx<'gcx>, expr: &'gcx hir::Expr<'gcx>) {
24        // `a ^ b` between integer literals is almost always a mistake for `a ** b`: `^` is bitwise
25        // xor in Solidity, so `10 ^ 18` is `24`, not `10 ** 18`.
26        //
27        // To stay precise, the base is restricted to `2` and `10` (bit widths and decimals, the
28        // only bases people write as powers) and hex operands are left alone. This mirrors GCC's
29        // and Clang's `-Wxor-used-as-pow`; Clippy's `suspicious_xor_used_as_pow`, which drops the
30        // base restriction, is allow-by-default precisely because of the resulting false positives.
31        if let ExprKind::Binary(lhs, op, rhs) = &expr.kind
32            && op.kind == BinOpKind::BitXor
33            && let Some(base) = plain_decimal_int_lit(ctx, lhs)
34            && (base == U256::from(2u64) || base == U256::from(10u64))
35            && plain_decimal_int_lit(ctx, rhs).is_some()
36        {
37            ctx.emit(&INCORRECT_EXP, expr.span);
38        }
39    }
40}
41
42/// Returns the value of a plain decimal integer literal, looking through parentheses and integer
43/// casts (`uint256(10)`), or `None` for anything else.
44///
45/// Only literals written as plain decimal digits (`10`, `1_000`) qualify. Hex literals (`0x..`) are
46/// bitwise intent, and scientific notation (`1e1`, which solar evaluates to `10`) is not the plain
47/// integer literal the `^`/`**` typo involves. A sub-denomination (`2 wei`, `2 seconds`) is dropped
48/// from the HIR but still present in the source span, so requiring the span to be exactly the
49/// digits filters it out too. All of these are left alone: this lint prefers a false negative to
50/// a false positive that would annoy developers.
51fn plain_decimal_int_lit(ctx: &LintContext, expr: &Expr<'_>) -> Option<U256> {
52    let expr = peel_int_casts(expr);
53    let ExprKind::Lit(Lit { kind: LitKind::Number(value), symbol, .. }) = &expr.kind else {
54        return None;
55    };
56    let s = symbol.as_str();
57    (!s.is_empty()
58        && s.bytes().all(|b| b.is_ascii_digit() || b == b'_')
59        && ctx.span_to_snippet(expr.span).is_some_and(|src| src.trim() == s))
60    .then_some(*value)
61}
62
63/// Looks through parentheses and integer casts (`uint256(x)`, `int8(x)`), returning the innermost
64/// operand. A misplaced `^` can hide behind such a cast (`uint256(10) ^ 18`), which a bare literal
65/// check would miss. Non-integer casts (`bytes32(x)`, `address(x)`) are left alone, since xor of a
66/// `bytesN` bit pattern is a legitimate operation.
67fn peel_int_casts<'a, 'gcx>(expr: &'a Expr<'gcx>) -> &'a Expr<'gcx> {
68    let expr = expr.peel_parens();
69    if let ExprKind::Call(callee, args, _) = &expr.kind
70        && let ExprKind::Type(hir::Type {
71            kind: TypeKind::Elementary(ElementaryType::Int(_) | ElementaryType::UInt(_)),
72            ..
73        }) = &callee.peel_parens().kind
74        && args.len() == 1
75        && let Some(inner) = args.exprs().next()
76    {
77        return peel_int_casts(inner);
78    }
79    expr
80}