Skip to main content

forge_lint/sol/info/
unused_error.rs

1use crate::{
2    linter::{Lint, ProjectLintEmitter, ProjectLintPass, ProjectSource},
3    sol::{Severity, SolLint, info::UnusedError},
4};
5use solar::{
6    ast::ContractKind,
7    interface::{data_structures::Never, source_map::FileName},
8    sema::{
9        Gcx,
10        hir::{self, Visit as _},
11    },
12};
13use std::{
14    collections::{HashMap, HashSet},
15    ops::ControlFlow,
16};
17
18declare_forge_lint!(UNUSED_ERROR, Severity::Info, "unused-error", "custom error is never used");
19
20impl<'ast> ProjectLintPass<'ast> for UnusedError {
21    fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) {
22        if !ctx.is_lint_enabled(UNUSED_ERROR.id()) {
23            return;
24        }
25        let gcx = ctx.gcx();
26
27        // Only errors declared in user-provided files are reported, while uses are collected
28        // across the whole unit, so an error declared here and reverted in a dependency (or the
29        // other way around) is attributed correctly.
30        let input_source_idx: HashMap<_, _> = gcx
31            .hir
32            .sources_enumerated()
33            .filter_map(|(sid, src)| {
34                let FileName::Real(path) = &src.file.name else { return None };
35                Some((sid, sources.iter().position(|s| &s.path == path)?))
36            })
37            .collect();
38        if input_source_idx.is_empty() {
39            return;
40        }
41
42        let mut collector = UsedErrorCollector { gcx, used: HashSet::new() };
43        for source_id in gcx.hir.source_ids() {
44            let _ = collector.visit_nested_source(source_id);
45        }
46
47        for error_id in gcx.hir.error_ids() {
48            let error = gcx.hir.error(error_id);
49            let Some(&src_idx) = input_source_idx.get(&error.source) else { continue };
50            // Errors declared in interfaces and abstract contracts are ABI surface meant for
51            // implementers and off-chain consumers, which may live outside the compiled sources.
52            let abi_surface = error.contract.is_some_and(|id| {
53                matches!(
54                    gcx.hir.contract(id).kind,
55                    ContractKind::Interface | ContractKind::AbstractContract
56                )
57            });
58            if !abi_surface && !collector.used.contains(&error_id) {
59                ctx.emit(&sources[src_idx], &UNUSED_ERROR, error.span);
60            }
61        }
62    }
63}
64
65/// Collects every error referenced by an expression anywhere in the unit.
66struct UsedErrorCollector<'gcx> {
67    gcx: Gcx<'gcx>,
68    used: HashSet<hir::ErrorId>,
69}
70
71impl<'gcx> hir::Visit<'gcx> for UsedErrorCollector<'gcx> {
72    type BreakValue = Never;
73
74    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
75        &self.gcx.hir
76    }
77
78    fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
79        if let Some(hir::Res::Item(hir::ItemId::Error(error_id))) = self.gcx.resolved_expr(expr) {
80            self.used.insert(error_id);
81        }
82        self.walk_expr(expr)
83    }
84}