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::{Symbol, data_structures::Never, source_map::FileName},
8    sema::{
9        Gcx,
10        hir::{self, Visit as _},
11        ty::{Ty, TyKind},
12    },
13};
14use std::{
15    collections::{HashMap, HashSet},
16    ops::ControlFlow,
17};
18
19declare_forge_lint!(UNUSED_ERROR, Severity::Info, "unused-error", "custom error is never used");
20
21impl<'ast> ProjectLintPass<'ast> for UnusedError {
22    fn check_project(&mut self, ctx: &ProjectLintEmitter<'_, '_>, sources: &[ProjectSource<'ast>]) {
23        if !ctx.is_lint_enabled(UNUSED_ERROR.id()) {
24            return;
25        }
26
27        let gcx = ctx.gcx();
28        let hir = &gcx.hir;
29
30        // Map every input source's HIR `SourceId` to the corresponding `ProjectSource` index, so
31        // only errors declared in user-provided files are reported. Uses are still collected
32        // across the whole unit, so an error declared here and reverted in a dependency (or the
33        // other way around) is attributed correctly.
34        let input_source_idx: HashMap<hir::SourceId, usize> = hir
35            .sources_enumerated()
36            .filter_map(|(sid, src)| {
37                let path = match &src.file.name {
38                    FileName::Real(p) => p,
39                    _ => return None,
40                };
41                let idx = sources.iter().position(|s| &s.path == path)?;
42                Some((sid, idx))
43            })
44            .collect();
45
46        if input_source_idx.is_empty() {
47            return;
48        }
49
50        let used = collect_used_errors(gcx);
51
52        // Report input-file error declarations that no expression in the unit references.
53        for error_id in hir.error_ids() {
54            let error = hir.error(error_id);
55            let Some(&src_idx) = input_source_idx.get(&error.source) else { continue };
56            // Errors declared in interfaces and abstract contracts are ABI surface meant for
57            // implementers and off-chain consumers, which may live outside the compiled sources.
58            if let Some(contract_id) = error.contract
59                && matches!(
60                    hir.contract(contract_id).kind,
61                    ContractKind::Interface | ContractKind::AbstractContract
62                )
63            {
64                continue;
65            }
66            if !used.contains(&error_id) {
67                ctx.emit(&sources[src_idx], &UNUSED_ERROR, error.span);
68            }
69        }
70    }
71}
72
73/// Collects every [`hir::ErrorId`] referenced by an expression anywhere in the unit.
74///
75/// Resolved identifiers cover almost every use: the lowering resolves the full path of
76/// `revert Lib.Err(...)` into a single `Ident`, and `require(cond, Err(...))` or `Err.selector`
77/// reference the error through a resolved `Ident` as well. The one exception is a qualified
78/// member access such as `Lib.Err.selector`: the inner `Err` segment carries no resolution in
79/// the HIR, so it is resolved against the scope its base designates: the items of a contract,
80/// or, for a module alias, Solar's resolved source scope, which binds exactly the names the
81/// file declares and imports (aliases included) rather than the raw items of the transitively
82/// imported files.
83fn collect_used_errors(gcx: Gcx<'_>) -> HashSet<hir::ErrorId> {
84    let hir = &gcx.hir;
85    let mut used = HashSet::new();
86    // Walk every source of the unit: functions, modifiers, and variable initializers.
87    for source_id in hir.source_ids() {
88        let mut collector = UsedErrorCollector { gcx, hir, current_source: source_id, used };
89        let _ = collector.visit_nested_source(source_id);
90        used = collector.used;
91    }
92    used
93}
94
95/// A named scope a qualified member can resolve against.
96enum MemberScope {
97    /// A contract or library: its declared items.
98    Contract(hir::ContractId),
99    /// A module alias: Solar's resolved scope for that source.
100    Module(hir::SourceId),
101}
102
103struct UsedErrorCollector<'gcx> {
104    gcx: Gcx<'gcx>,
105    hir: &'gcx hir::Hir<'gcx>,
106    /// The source being walked: module member lookups are made from its viewpoint.
107    current_source: hir::SourceId,
108    used: HashSet<hir::ErrorId>,
109}
110
111impl<'gcx> hir::Visit<'gcx> for UsedErrorCollector<'gcx> {
112    type BreakValue = Never;
113
114    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
115        self.hir
116    }
117
118    fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
119        match &expr.kind {
120            // Direct resolved reference: `revert Err()`, `revert Lib.Err()` (fully resolved by
121            // the lowering), `require(cond, Err())`, `Err.selector`, ...
122            hir::ExprKind::Ident(resolutions) => {
123                // Symbols can be overloaded: consider every resolution.
124                for res in *resolutions {
125                    if let hir::Res::Item(hir::ItemId::Error(error_id)) = res {
126                        self.used.insert(*error_id);
127                    }
128                }
129            }
130            // `Lib.Err.selector`: the `Err` member carries no resolution, so resolve it against
131            // the scope its base designates.
132            hir::ExprKind::Member(base, member) => {
133                for scope in self.base_scopes(base) {
134                    match scope {
135                        MemberScope::Contract(contract_id) => {
136                            self.mark_named_error(contract_id, member.name);
137                        }
138                        // In the resolved scope an import alias binds under its local name to
139                        // the exact declaration: mark that error, not a same-name lookalike.
140                        MemberScope::Module(source_id) => {
141                            for member_ty in self.module_members_named(source_id, member.name) {
142                                if let TyKind::Error(_, error_id) = member_ty.kind {
143                                    self.used.insert(error_id);
144                                }
145                            }
146                        }
147                    }
148                }
149            }
150            _ => {}
151        }
152        self.walk_expr(expr)
153    }
154}
155
156impl<'gcx> UsedErrorCollector<'gcx> {
157    /// Marks as used the errors of the contract whose name matches `name`.
158    fn mark_named_error(&mut self, contract_id: hir::ContractId, name: Symbol) {
159        // Scopes cannot hold two same-name errors, but scan every item to stay conservative.
160        for item_id in self.hir.contract(contract_id).items {
161            if let hir::ItemId::Error(error_id) = item_id
162                && self.hir.error(*error_id).name.name == name
163            {
164                self.used.insert(*error_id);
165            }
166        }
167    }
168
169    /// Returns the types of the members named `name` in the resolved scope of module
170    /// `source_id`.
171    ///
172    /// `members_of` on a module type iterates Solar's resolved source scope: the file's own
173    /// declarations plus the names its imports actually bind, under their local (alias) names.
174    fn module_members_named(&self, source_id: hir::SourceId, name: Symbol) -> Vec<Ty<'gcx>> {
175        let module_ty = self.gcx.type_of_res(hir::Res::Namespace(source_id));
176        self.gcx
177            .members_of(module_ty, self.current_source, None)
178            .filter(|member| member.name == name)
179            .map(|member| member.ty)
180            .collect()
181    }
182
183    /// Returns the named scopes `expr` can designate: a contract or library through a resolved
184    /// identifier, a module alias, or a member chain leading to one (`NS.Lib`, `NS.Inner`).
185    fn base_scopes(&self, expr: &hir::Expr<'_>) -> Vec<MemberScope> {
186        let mut scopes = Vec::new();
187        match &expr.kind {
188            hir::ExprKind::Ident(resolutions) => {
189                for res in *resolutions {
190                    match res {
191                        hir::Res::Item(hir::ItemId::Contract(contract_id)) => {
192                            scopes.push(MemberScope::Contract(*contract_id));
193                        }
194                        hir::Res::Namespace(source_id) => {
195                            scopes.push(MemberScope::Module(*source_id));
196                        }
197                        _ => {}
198                    }
199                }
200            }
201            // A chained scope access (`NS.Lib`, `NS.Inner`): resolve the name in the scopes of
202            // the base. Contracts do not nest named scopes, so only module bases descend.
203            hir::ExprKind::Member(inner_base, name) => {
204                for scope in self.base_scopes(inner_base) {
205                    if let MemberScope::Module(source_id) = scope {
206                        for member_ty in self.module_members_named(source_id, name.name) {
207                            // A contract or library is a type-namespace item, so its member
208                            // type comes wrapped as `Type(Contract(..))`; a nested module
209                            // alias comes as a bare `Module(..)`.
210                            let member_ty = match member_ty.kind {
211                                TyKind::Type(inner) => inner,
212                                _ => member_ty,
213                            };
214                            match member_ty.kind {
215                                TyKind::Contract(contract_id) => {
216                                    scopes.push(MemberScope::Contract(contract_id));
217                                }
218                                TyKind::Module(inner_source) => {
219                                    scopes.push(MemberScope::Module(inner_source));
220                                }
221                                _ => {}
222                            }
223                        }
224                    }
225                }
226            }
227            _ => {}
228        }
229        scopes
230    }
231}