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, LitKind};
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_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) {
18        let ExprKind::Lit(lit, sub_denom) = &expr.kind else { return };
19
20        // Only plain integer literals. `LitKind::Address` (40-hex-digit address) is a
21        // distinct variant and is therefore skipped automatically.
22        if !matches!(lit.kind, LitKind::Number(_)) {
23            return;
24        }
25
26        // Skip literals with a sub-denomination, e.g. `1000000 gwei`, `5 minutes`.
27        if sub_denom.is_some() {
28            return;
29        }
30
31        let s = lit.symbol.as_str();
32
33        // Skip hex literals — long zero runs in hex are usually intentional (masks,
34        // selectors, bit patterns) and there is no scientific-notation alternative.
35        if s.starts_with("0x") || s.starts_with("0X") {
36            return;
37        }
38
39        // Skip if the user already used scientific notation (`1e18`).
40        if s.contains('e') || s.contains('E') {
41            return;
42        }
43
44        // 5+ consecutive zeros in the literal as written. Underscores are
45        // preserved, so `1_000_000` passes while `1_000000` is flagged.
46        if s.contains("00000") {
47            ctx.emit(&TOO_MANY_DIGITS, lit.span);
48        }
49    }
50}