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}