Skip to main content

forge_lint/sol/info/
literal_instead_of_constant.rs

1use super::LiteralInsteadOfConstant;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use alloy_primitives::{Address, U256};
7use solar::{
8    ast::{BinOpKind, LitKind, StrKind, UnOpKind},
9    interface::Span,
10    sema::{
11        Gcx,
12        hir::{self, Expr, ExprKind, Hir, Lit, Stmt, StmtKind, Visit},
13        ty::TyKind,
14    },
15};
16use std::{collections::HashMap, convert::Infallible, ops::ControlFlow};
17
18declare_forge_lint!(
19    LITERAL_INSTEAD_OF_CONSTANT,
20    Severity::Info,
21    "literal-instead-of-constant",
22    "this literal appears multiple times in the contract; declare a named constant for it"
23);
24
25impl<'hir> LateLintPass<'hir> for LiteralInsteadOfConstant {
26    fn check_nested_contract(
27        &mut self,
28        ctx: &LintContext,
29        gcx: Gcx<'hir>,
30        hir: &'hir Hir<'hir>,
31        id: hir::ContractId,
32    ) {
33        // Group the literals of the contract's own functions and modifiers by semantic value;
34        // inherited items group with their declaring contract. Collection covers the executable
35        // expressions: the body statements, and the modifier and base-constructor arguments of
36        // the header. Parameter and return types stay out, so a fixed array size in a signature
37        // is a type annotation rather than a repeated value.
38        let mut collector = LiteralCollector { gcx, hir, groups: HashMap::new() };
39        for item_id in hir.contract(id).items {
40            if let hir::ItemId::Function(function_id) = item_id {
41                let function = hir.function(*function_id);
42                for modifier in function.modifiers {
43                    let _ = collector.visit_call_args(&modifier.args);
44                }
45                if let Some(body) = &function.body {
46                    for stmt in body.stmts {
47                        let _ = collector.visit_stmt(stmt);
48                    }
49                }
50            }
51        }
52        // A value used in one single place is fine: only repetitions report. Emissions are
53        // sorted by position so the output does not depend on the map's iteration order.
54        let mut repeated: Vec<Span> = Vec::new();
55        for spans in collector.groups.into_values() {
56            if spans.len() > 1 {
57                repeated.extend(spans);
58            }
59        }
60        repeated.sort_by_key(|span| span.lo());
61        for span in repeated {
62            ctx.emit(&LITERAL_INSTEAD_OF_CONSTANT, span);
63        }
64    }
65}
66
67/// The semantic value of a literal, the grouping key: two spellings of the same number
68/// (`100`, `0x64`, `1e2`) or the same unit-scaled amount (`1 ether`, `1e18`) are one value.
69/// A numeric literal under a value-changing unary operator denotes a DISTINCT constant, so
70/// `-5` and `~5` never group with the bare `5`.
71#[derive(PartialEq, Eq, Hash)]
72enum LiteralValue {
73    Number(U256),
74    NegNumber(U256),
75    BitNotNumber(U256),
76    Address(Address),
77    HexString(Vec<u8>),
78}
79
80/// Collects the grouping-relevant literals of a subtree: numbers above 2, address literals
81/// and hex string literals. A bare literal indexing an array-like value or bounding a slice
82/// stays out as positional, matching Aderyn; a mapping key counts, it is configuration data.
83struct LiteralCollector<'hir> {
84    gcx: Gcx<'hir>,
85    hir: &'hir Hir<'hir>,
86    groups: HashMap<LiteralValue, Vec<Span>>,
87}
88
89impl<'hir> LiteralCollector<'hir> {
90    /// Records one literal under its semantic grouping key.
91    fn record_lit(&mut self, lit: &Lit<'_>) {
92        let key = match &lit.kind {
93            // `0`, `1` and `2` are structural rather than configuration values.
94            LitKind::Number(v) if *v > U256::from(2u64) => Some(LiteralValue::Number(*v)),
95            LitKind::Address(address) => Some(LiteralValue::Address(*address)),
96            LitKind::Str(StrKind::Hex, bytes, _) => {
97                Some(LiteralValue::HexString(bytes.as_byte_str().to_vec()))
98            }
99            _ => None,
100        };
101        if let Some(key) = key {
102            self.groups.entry(key).or_default().push(lit.span);
103        }
104    }
105
106    /// Whether an indexed base is a mapping, whose keys are configuration values rather than
107    /// positions. The type checker tells mappings apart from arrays, bytes and fixed bytes.
108    fn base_is_mapping(&self, base: &Expr<'_>) -> bool {
109        matches!(
110            self.gcx.type_of_expr(base.peel_parens().id).map(|ty| ty.peel_refs().kind),
111            Some(TyKind::Mapping(..))
112        )
113    }
114}
115
116impl<'hir> Visit<'hir> for LiteralCollector<'hir> {
117    type BreakValue = Infallible;
118
119    fn hir(&self) -> &'hir Hir<'hir> {
120        self.hir
121    }
122
123    fn visit_stmt(&mut self, stmt: &'hir Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
124        // A Yul `case 500 {}` label is a literal like any other, but `case.constant` is a
125        // `Lit` rather than an expression, so the default visitor never reaches it.
126        if let StmtKind::Switch(switch) = &stmt.kind {
127            for case in switch.cases {
128                if let Some(constant) = &case.constant {
129                    self.record_lit(constant);
130                }
131            }
132        }
133        self.walk_stmt(stmt)
134    }
135
136    fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
137        match &expr.kind {
138            // A bare literal indexing an array-like value (`arr[3]`) is positional, not a
139            // magic value; a mapping key (`m[500]`) is configuration data and counts.
140            ExprKind::Index(base, index) => {
141                let _ = self.visit_expr(base);
142                if let Some(index) = index
143                    && (self.base_is_mapping(base)
144                        || !matches!(index.peel_parens().kind, ExprKind::Lit(..)))
145                {
146                    let _ = self.visit_expr(index);
147                }
148                return ControlFlow::Continue(());
149            }
150            // A bare literal shift amount is structural rather than a configuration value
151            // (`x << 128`, `acc >>= 128`): the shifted operand is walked normally, the
152            // amount only when it is computed, so the literals inside it still count.
153            ExprKind::Binary(lhs, op, rhs)
154                if matches!(op.kind, BinOpKind::Shl | BinOpKind::Shr | BinOpKind::Sar) =>
155            {
156                let _ = self.visit_expr(lhs);
157                if !matches!(rhs.peel_parens().kind, ExprKind::Lit(..)) {
158                    let _ = self.visit_expr(rhs);
159                }
160                return ControlFlow::Continue(());
161            }
162            ExprKind::Assign(lhs, Some(op), rhs)
163                if matches!(op.kind, BinOpKind::Shl | BinOpKind::Shr | BinOpKind::Sar) =>
164            {
165                let _ = self.visit_expr(lhs);
166                if !matches!(rhs.peel_parens().kind, ExprKind::Lit(..)) {
167                    let _ = self.visit_expr(rhs);
168                }
169                return ControlFlow::Continue(());
170            }
171            // Slice bounds share the positional rationale: slices only exist on array-like
172            // values, so a bare literal bound (`d[555:600]`) is never a magic value.
173            ExprKind::Slice(base, start, end) => {
174                let _ = self.visit_expr(base);
175                // Each bound present is walked unless it is a bare literal.
176                for bound in [start, end].into_iter().flatten() {
177                    if !matches!(bound.peel_parens().kind, ExprKind::Lit(..)) {
178                        let _ = self.visit_expr(bound);
179                    }
180                }
181                return ControlFlow::Continue(());
182            }
183            // A value-changing unary operator on a numeric literal denotes a DISTINCT constant, so
184            // `-5`/`~5` must not group with the bare `5`.
185            ExprKind::Unary(op, operand) if matches!(op.kind, UnOpKind::Neg | UnOpKind::BitNot) => {
186                match &operand.peel_parens().kind {
187                    // `-5` / `~5`: record the operator-qualified value; do not descend into the
188                    // operand, which would re-record the bare magnitude.
189                    ExprKind::Lit(lit) => {
190                        if let LitKind::Number(v) = &lit.kind
191                            && *v > U256::from(2u64)
192                        {
193                            let key = if op.kind == UnOpKind::Neg {
194                                LiteralValue::NegNumber(*v)
195                            } else {
196                                LiteralValue::BitNotNumber(*v)
197                            };
198                            self.groups.entry(key).or_default().push(expr.span);
199                        }
200                        return ControlFlow::Continue(());
201                    }
202                    // A nested unary over a literal (`-(-5)`, `~~5`) folds to a value that is
203                    // neither this operator's nor the bare literal's; canonicalizing it is not
204                    // worth it, so the chain is skipped rather than miss-keyed. A non-literal
205                    // operand deeper down (`-(-(x + 500))`) falls through so its own literals
206                    // are still recorded.
207                    ExprKind::Unary(inner, inner_operand)
208                        if matches!(inner.kind, UnOpKind::Neg | UnOpKind::BitNot)
209                            && matches!(inner_operand.peel_parens().kind, ExprKind::Lit(..)) =>
210                    {
211                        return ControlFlow::Continue(());
212                    }
213                    // Any other operand (`-x`, `-(a + 5)`): walk normally so a literal inside it is
214                    // still recorded with its own value.
215                    _ => {}
216                }
217            }
218            ExprKind::Lit(lit) => self.record_lit(lit),
219            _ => {}
220        }
221        self.walk_expr(expr)
222    }
223}