Skip to main content

forge_lint/linter/
late.rs

1use solar::{
2    interface::data_structures::Never,
3    sema::{Gcx, hir},
4};
5use std::ops::ControlFlow;
6
7use super::LintContext;
8
9/// Trait for lints that operate on the HIR (High-level Intermediate Representation).
10/// Its methods mirror `hir::visit::Visit`, with the addition of `LintContext`.
11pub trait LateLintPass<'hir>: Send + Sync {
12    fn check_nested_source(
13        &mut self,
14        _ctx: &LintContext,
15        _gcx: Gcx<'hir>,
16        _hir: &'hir hir::Hir<'hir>,
17        _id: hir::SourceId,
18    ) {
19    }
20    fn check_nested_item(
21        &mut self,
22        _ctx: &LintContext,
23        _hir: &'hir hir::Hir<'hir>,
24        _id: hir::ItemId,
25    ) {
26    }
27    fn check_nested_contract(
28        &mut self,
29        _ctx: &LintContext,
30        _gcx: Gcx<'hir>,
31        _hir: &'hir hir::Hir<'hir>,
32        _id: hir::ContractId,
33    ) {
34    }
35    fn check_nested_function(
36        &mut self,
37        _ctx: &LintContext,
38        _hir: &'hir hir::Hir<'hir>,
39        _id: hir::FunctionId,
40    ) {
41    }
42    fn check_nested_var(
43        &mut self,
44        _ctx: &LintContext,
45        _hir: &'hir hir::Hir<'hir>,
46        _id: hir::VariableId,
47    ) {
48    }
49    fn check_item(
50        &mut self,
51        _ctx: &LintContext,
52        _hir: &'hir hir::Hir<'hir>,
53        _item: hir::Item<'hir, 'hir>,
54    ) {
55    }
56    fn check_contract(
57        &mut self,
58        _ctx: &LintContext,
59        _gcx: Gcx<'hir>,
60        _hir: &'hir hir::Hir<'hir>,
61        _contract: &'hir hir::Contract<'hir>,
62    ) {
63    }
64    fn check_function(
65        &mut self,
66        _ctx: &LintContext,
67        _gcx: Gcx<'hir>,
68        _hir: &'hir hir::Hir<'hir>,
69        _func: &'hir hir::Function<'hir>,
70    ) {
71    }
72    fn check_modifier(
73        &mut self,
74        _ctx: &LintContext,
75        _hir: &'hir hir::Hir<'hir>,
76        _mod: &'hir hir::Modifier<'hir>,
77    ) {
78    }
79    fn check_var(
80        &mut self,
81        _ctx: &LintContext,
82        _hir: &'hir hir::Hir<'hir>,
83        _var: &'hir hir::Variable<'hir>,
84    ) {
85    }
86    fn check_expr(
87        &mut self,
88        _ctx: &LintContext,
89        _gcx: Gcx<'hir>,
90        _hir: &'hir hir::Hir<'hir>,
91        _expr: &'hir hir::Expr<'hir>,
92    ) {
93    }
94    fn check_call_args(
95        &mut self,
96        _ctx: &LintContext,
97        _hir: &'hir hir::Hir<'hir>,
98        _args: &'hir hir::CallArgs<'hir>,
99    ) {
100    }
101    fn check_stmt(
102        &mut self,
103        _ctx: &LintContext,
104        _gcx: Gcx<'hir>,
105        _hir: &'hir hir::Hir<'hir>,
106        _stmt: &'hir hir::Stmt<'hir>,
107    ) {
108    }
109    fn check_ty(
110        &mut self,
111        _ctx: &LintContext,
112        _hir: &'hir hir::Hir<'hir>,
113        _ty: &'hir hir::Type<'hir>,
114    ) {
115    }
116}
117
118/// Visitor struct for `LateLintPass`es
119pub struct LateLintVisitor<'a, 's, 'hir> {
120    ctx: &'a LintContext<'s, 'a>,
121    passes: &'a mut [Box<dyn LateLintPass<'hir> + 's>],
122    gcx: Gcx<'hir>,
123    hir: &'hir hir::Hir<'hir>,
124}
125
126impl<'a, 's, 'hir> LateLintVisitor<'a, 's, 'hir>
127where
128    's: 'hir,
129{
130    pub fn new(
131        ctx: &'a LintContext<'s, 'a>,
132        passes: &'a mut [Box<dyn LateLintPass<'hir> + 's>],
133        gcx: Gcx<'hir>,
134        hir: &'hir hir::Hir<'hir>,
135    ) -> Self {
136        Self { ctx, passes, gcx, hir }
137    }
138}
139
140impl<'s, 'hir> hir::Visit<'hir> for LateLintVisitor<'_, 's, 'hir>
141where
142    's: 'hir,
143{
144    type BreakValue = Never;
145
146    fn hir(&self) -> &'hir hir::Hir<'hir> {
147        self.hir
148    }
149
150    fn visit_nested_source(&mut self, id: hir::SourceId) -> ControlFlow<Self::BreakValue> {
151        for pass in self.passes.iter_mut() {
152            pass.check_nested_source(self.ctx, self.gcx, self.hir, id);
153        }
154        self.walk_nested_source(id)
155    }
156
157    fn visit_nested_item(&mut self, id: hir::ItemId) -> ControlFlow<Self::BreakValue> {
158        for pass in self.passes.iter_mut() {
159            pass.check_nested_item(self.ctx, self.hir, id);
160        }
161        self.walk_nested_item(id)
162    }
163
164    fn visit_nested_contract(&mut self, id: hir::ContractId) -> ControlFlow<Self::BreakValue> {
165        for pass in self.passes.iter_mut() {
166            pass.check_nested_contract(self.ctx, self.gcx, self.hir, id);
167        }
168        self.walk_nested_contract(id)
169    }
170
171    fn visit_nested_function(&mut self, id: hir::FunctionId) -> ControlFlow<Self::BreakValue> {
172        for pass in self.passes.iter_mut() {
173            pass.check_nested_function(self.ctx, self.hir, id);
174        }
175        self.walk_nested_function(id)
176    }
177
178    fn visit_nested_var(&mut self, id: hir::VariableId) -> ControlFlow<Self::BreakValue> {
179        for pass in self.passes.iter_mut() {
180            pass.check_nested_var(self.ctx, self.hir, id);
181        }
182        self.walk_nested_var(id)
183    }
184
185    fn visit_contract(
186        &mut self,
187        contract: &'hir hir::Contract<'hir>,
188    ) -> ControlFlow<Self::BreakValue> {
189        for pass in self.passes.iter_mut() {
190            pass.check_contract(self.ctx, self.gcx, self.hir, contract);
191        }
192        self.walk_contract(contract)
193    }
194
195    fn visit_function(&mut self, func: &'hir hir::Function<'hir>) -> ControlFlow<Self::BreakValue> {
196        for pass in self.passes.iter_mut() {
197            pass.check_function(self.ctx, self.gcx, self.hir, func);
198        }
199        self.walk_function(func)
200    }
201
202    fn visit_modifier(
203        &mut self,
204        modifier: &'hir hir::Modifier<'hir>,
205    ) -> ControlFlow<Self::BreakValue> {
206        for pass in self.passes.iter_mut() {
207            pass.check_modifier(self.ctx, self.hir, modifier);
208        }
209        self.walk_modifier(modifier)
210    }
211
212    fn visit_item(&mut self, item: hir::Item<'hir, 'hir>) -> ControlFlow<Self::BreakValue> {
213        for pass in self.passes.iter_mut() {
214            pass.check_item(self.ctx, self.hir, item);
215        }
216        self.walk_item(item)
217    }
218
219    fn visit_var(&mut self, var: &'hir hir::Variable<'hir>) -> ControlFlow<Self::BreakValue> {
220        for pass in self.passes.iter_mut() {
221            pass.check_var(self.ctx, self.hir, var);
222        }
223        self.walk_var(var)
224    }
225
226    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
227        for pass in self.passes.iter_mut() {
228            pass.check_expr(self.ctx, self.gcx, self.hir, expr);
229        }
230        self.walk_expr(expr)
231    }
232
233    fn visit_call_args(
234        &mut self,
235        args: &'hir hir::CallArgs<'hir>,
236    ) -> ControlFlow<Self::BreakValue> {
237        for pass in self.passes.iter_mut() {
238            pass.check_call_args(self.ctx, self.hir, args);
239        }
240        self.walk_call_args(args)
241    }
242
243    fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
244        for pass in self.passes.iter_mut() {
245            pass.check_stmt(self.ctx, self.gcx, self.hir, stmt);
246        }
247        self.walk_stmt(stmt)
248    }
249
250    fn visit_ty(&mut self, ty: &'hir hir::Type<'hir>) -> ControlFlow<Self::BreakValue> {
251        for pass in self.passes.iter_mut() {
252            pass.check_ty(self.ctx, self.hir, ty);
253        }
254        self.walk_ty(ty)
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::linter::LinterConfig;
262    use foundry_common::comments::inline_config::InlineConfig;
263    use foundry_config::lint::LintSpecificConfig;
264    use solar::{
265        interface::{Session, source_map::FileName},
266        sema::Compiler,
267    };
268    use std::sync::{Arc, Mutex};
269
270    #[derive(Debug, Default)]
271    struct HookCounts {
272        nested_item: usize,
273        nested_contract: usize,
274        nested_function: usize,
275        nested_var: usize,
276        modifier: usize,
277        call_args: usize,
278    }
279
280    struct RecordingPass {
281        counts: Arc<Mutex<HookCounts>>,
282    }
283
284    impl RecordingPass {
285        fn record(&self, update: impl FnOnce(&mut HookCounts)) {
286            update(&mut self.counts.lock().unwrap());
287        }
288    }
289
290    impl<'hir> LateLintPass<'hir> for RecordingPass {
291        fn check_nested_item(
292            &mut self,
293            _ctx: &LintContext,
294            _hir: &'hir hir::Hir<'hir>,
295            _id: hir::ItemId,
296        ) {
297            self.record(|counts| counts.nested_item += 1);
298        }
299
300        fn check_nested_contract(
301            &mut self,
302            _ctx: &LintContext,
303            _gcx: solar::sema::Gcx<'hir>,
304            _hir: &'hir hir::Hir<'hir>,
305            _id: hir::ContractId,
306        ) {
307            self.record(|counts| counts.nested_contract += 1);
308        }
309
310        fn check_nested_function(
311            &mut self,
312            _ctx: &LintContext,
313            _hir: &'hir hir::Hir<'hir>,
314            _id: hir::FunctionId,
315        ) {
316            self.record(|counts| counts.nested_function += 1);
317        }
318
319        fn check_nested_var(
320            &mut self,
321            _ctx: &LintContext,
322            _hir: &'hir hir::Hir<'hir>,
323            _id: hir::VariableId,
324        ) {
325            self.record(|counts| counts.nested_var += 1);
326        }
327
328        fn check_modifier(
329            &mut self,
330            _ctx: &LintContext,
331            _hir: &'hir hir::Hir<'hir>,
332            _modifier: &'hir hir::Modifier<'hir>,
333        ) {
334            self.record(|counts| counts.modifier += 1);
335        }
336
337        fn check_call_args(
338            &mut self,
339            _ctx: &LintContext,
340            _hir: &'hir hir::Hir<'hir>,
341            _args: &'hir hir::CallArgs<'hir>,
342        ) {
343            self.record(|counts| counts.call_args += 1);
344        }
345    }
346
347    #[test]
348    fn calls_hooks_for_nested_items_modifiers_and_call_args() {
349        let counts = Arc::new(Mutex::new(HookCounts::default()));
350        let inline = InlineConfig::default();
351        let lint_specific = LintSpecificConfig::default();
352        let source = r#"
353            pragma solidity ^0.8.20;
354
355            contract Base {
356                function hook(uint256 value) internal pure returns (uint256) {
357                    return value;
358                }
359            }
360
361            contract Test is Base {
362                uint256 stored;
363
364                modifier gated(uint256 amount) {
365                    _;
366                }
367
368                function run(uint256 amount) public gated(amount) returns (uint256) {
369                    return hook(amount + stored);
370                }
371            }
372        "#;
373
374        let mut compiler =
375            Compiler::new(Session::builder().with_buffer_emitter(Default::default()).build());
376        compiler
377            .enter_mut(|compiler| -> solar::interface::Result<()> {
378                let mut pcx = compiler.parse();
379                pcx.set_resolve_imports(false);
380                let file = compiler
381                    .sess()
382                    .source_map()
383                    .new_source_file(FileName::Stdin, source)
384                    .expect("failed to create source file");
385                pcx.add_file(file);
386                pcx.parse();
387
388                let ControlFlow::Continue(()) = compiler.lower_asts()? else {
389                    panic!("expected HIR lowering to continue");
390                };
391
392                let gcx = compiler.gcx();
393                let source_id = gcx.hir.source_ids().next().expect("expected one lowered source");
394                let ctx = LintContext::new(
395                    gcx.sess,
396                    false,
397                    false,
398                    LinterConfig { inline: &inline, lint_specific: &lint_specific },
399                    Vec::new(),
400                    None,
401                );
402                let mut passes: Vec<Box<dyn LateLintPass<'_>>> =
403                    vec![Box::new(RecordingPass { counts: counts.clone() })];
404                let mut visitor = LateLintVisitor::new(&ctx, &mut passes, gcx, &gcx.hir);
405                let _ = hir::Visit::visit_nested_source(&mut visitor, source_id);
406                Ok(())
407            })
408            .expect("failed to lower test source");
409
410        let counts = counts.lock().unwrap();
411        assert!(counts.nested_item > 0, "expected nested item hook to run");
412        assert!(counts.nested_contract > 0, "expected nested contract hook to run");
413        assert!(counts.nested_function > 0, "expected nested function hook to run");
414        assert!(counts.nested_var > 0, "expected nested var hook to run");
415        assert!(counts.modifier > 0, "expected modifier hook to run");
416        assert!(counts.call_args > 0, "expected call args hook to run");
417    }
418}