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}