Skip to main content

forge_lint/sol/info/
too_many_digits.rs

1use super::TooManyDigits;
2use crate::{
3    linter::{EarlyLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::ast::{Expr, ExprKind, Lit, LitKind, Stmt, StmtKind, yul};
7
8declare_forge_lint!(
9    TOO_MANY_DIGITS,
10    Severity::Info,
11    "too-many-digits",
12    "numeric literal with many digits is error-prone; \
13     use scientific notation, sub-denominations, or underscore separators"
14);
15
16impl<'ast> EarlyLintPass<'ast> for TooManyDigits {
17    fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) {
18        if let StmtKind::Assembly(assembly) = &stmt.kind {
19            check_yul_block(ctx, &assembly.block);
20        }
21    }
22
23    fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) {
24        let ExprKind::Lit(lit, sub_denom) = &expr.kind else { return };
25        check_lit(ctx, lit, sub_denom.is_some());
26    }
27}
28
29fn check_lit(ctx: &LintContext, lit: &Lit<'_>, has_sub_denom: bool) {
30    // Only plain integer literals. `LitKind::Address` (40-hex-digit address) is a
31    // distinct variant and is therefore skipped automatically.
32    if !matches!(lit.kind, LitKind::Number(_)) {
33        return;
34    }
35
36    // Skip literals with a sub-denomination, e.g. `1000000 gwei`, `5 minutes`.
37    if has_sub_denom {
38        return;
39    }
40
41    let s = lit.symbol.as_str();
42    let is_hex = is_hex_literal(s);
43
44    // Match Slither's detector: skip only address-shaped hex constants, not all hex
45    // constants. Long padded masks/selectors are still hard to review.
46    if is_hex_address(s) {
47        return;
48    }
49
50    // Skip if the user already used scientific notation (`1e18`).
51    if !is_hex && (s.contains('e') || s.contains('E')) {
52        return;
53    }
54
55    // 5+ consecutive zeros in the literal as written. Underscores are
56    // preserved, so `1_000_000` passes while `1_000000` is flagged.
57    if s.contains("00000") {
58        ctx.emit(&TOO_MANY_DIGITS, lit.span);
59    }
60}
61
62fn is_hex_literal(s: &str) -> bool {
63    s.starts_with("0x") || s.starts_with("0X")
64}
65
66fn is_hex_address(s: &str) -> bool {
67    let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) else { return false };
68    hex.len() == 40 && hex.bytes().all(|b| b.is_ascii_hexdigit())
69}
70
71fn check_yul_block(ctx: &LintContext, block: &yul::Block<'_>) {
72    for stmt in block.stmts.iter() {
73        check_yul_stmt(ctx, stmt);
74    }
75}
76
77fn check_yul_stmt(ctx: &LintContext, stmt: &yul::Stmt<'_>) {
78    match &stmt.kind {
79        yul::StmtKind::Block(block) => check_yul_block(ctx, block),
80        yul::StmtKind::AssignSingle(_, expr)
81        | yul::StmtKind::AssignMulti(_, expr)
82        | yul::StmtKind::Expr(expr) => check_yul_expr(ctx, expr),
83        yul::StmtKind::If(cond, block) => {
84            check_yul_expr(ctx, cond);
85            check_yul_block(ctx, block);
86        }
87        yul::StmtKind::For(for_stmt) => {
88            check_yul_block(ctx, &for_stmt.init);
89            check_yul_expr(ctx, &for_stmt.cond);
90            check_yul_block(ctx, &for_stmt.step);
91            check_yul_block(ctx, &for_stmt.body);
92        }
93        yul::StmtKind::Switch(switch) => {
94            check_yul_expr(ctx, &switch.selector);
95            for case in switch.cases.iter() {
96                if let Some(lit) = &case.constant {
97                    check_lit(ctx, lit, false);
98                }
99                check_yul_block(ctx, &case.body);
100            }
101        }
102        yul::StmtKind::FunctionDef(func) => check_yul_block(ctx, &func.body),
103        yul::StmtKind::VarDecl(_, Some(init)) => check_yul_expr(ctx, init),
104        yul::StmtKind::Leave
105        | yul::StmtKind::Break
106        | yul::StmtKind::Continue
107        | yul::StmtKind::VarDecl(_, None) => {}
108    }
109}
110
111fn check_yul_expr(ctx: &LintContext, expr: &yul::Expr<'_>) {
112    match &expr.kind {
113        yul::ExprKind::Call(call) => {
114            for arg in call.arguments.iter() {
115                check_yul_expr(ctx, arg);
116            }
117        }
118        yul::ExprKind::Lit(lit) => check_lit(ctx, lit, false),
119        yul::ExprKind::Path(_) => {}
120    }
121}