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}