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::{
7    ast::{Expr, ExprKind, Lit, LitKind, Stmt, StmtKind, visit::Visit},
8    data_structures::Never,
9};
10use std::ops::ControlFlow;
11
12declare_forge_lint!(
13    TOO_MANY_DIGITS,
14    Severity::Info,
15    "too-many-digits",
16    "numeric literal contains a long sequence of zeros"
17);
18
19impl<'ast> EarlyLintPass<'ast> for TooManyDigits {
20    fn check_stmt(&mut self, ctx: &LintContext, stmt: &'ast Stmt<'ast>) {
21        // Yul literals are not `Expr`s, so `check_expr` never sees them.
22        if let StmtKind::Assembly(assembly) = &stmt.kind {
23            let _ = YulLiterals { ctx }.visit_yul_block(&assembly.block);
24        }
25    }
26
27    fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) {
28        // Skip literals with a sub-denomination, e.g. `1000000 gwei`, `5 minutes`.
29        if let ExprKind::Lit(lit, None) = &expr.kind {
30            check_lit(ctx, lit);
31        }
32    }
33}
34
35fn check_lit(ctx: &LintContext, lit: &Lit<'_>) {
36    // Only plain integer literals; `LitKind::Address` is a distinct variant.
37    if !matches!(lit.kind, LitKind::Number(_)) {
38        return;
39    }
40    let s = lit.symbol.as_str();
41    let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"));
42    // Match Slither's detector: skip only address-shaped hex constants, not all hex constants
43    // (long padded masks/selectors are still hard to review), and scientific notation (`1e18`).
44    let is_hex_address =
45        hex.is_some_and(|h| h.len() == 40 && h.bytes().all(|b| b.is_ascii_hexdigit()));
46    let is_scientific = hex.is_none() && s.contains(['e', 'E']);
47    // 5+ consecutive zeros in the literal as written. Underscores are preserved, so
48    // `1_000_000` passes while `1_000000` is flagged.
49    if !is_hex_address && !is_scientific && s.contains("00000") {
50        ctx.emit(&TOO_MANY_DIGITS, lit.span);
51    }
52}
53
54/// Checks every literal of an assembly block, `case` labels included.
55struct YulLiterals<'a, 's> {
56    ctx: &'a LintContext<'s, 'a>,
57}
58
59impl<'ast> Visit<'ast> for YulLiterals<'_, '_> {
60    type BreakValue = Never;
61
62    fn visit_lit(&mut self, lit: &'ast Lit<'_>) -> ControlFlow<Self::BreakValue> {
63        check_lit(self.ctx, lit);
64        ControlFlow::Continue(())
65    }
66}