Skip to main content

forge_lint/sol/info/
imports.rs

1use super::Imports;
2use crate::{
3    linter::{EarlyLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::{self as ast, SourceUnit, Symbol, visit::Visit},
8    data_structures::{Never, map::FxHashSet},
9    interface::SourceMap,
10};
11use std::ops::ControlFlow;
12
13declare_forge_lint!(UNUSED_IMPORT, Severity::Info, "unused-import", "unused import");
14
15declare_forge_lint!(
16    UNALIASED_PLAIN_IMPORT,
17    Severity::Info,
18    "unaliased-plain-import",
19    "plain import has no alias; use named imports `{A, B}` or alias `import \"..\" as X`"
20);
21
22impl<'ast> EarlyLintPass<'ast> for Imports {
23    fn check_import_directive(
24        &mut self,
25        ctx: &LintContext,
26        import: &'ast ast::ImportDirective<'ast>,
27    ) {
28        // Non-aliased plain imports like `import "File.sol";`.
29        if let ast::ImportItems::Plain(_) = &import.items
30            && import.source_alias().is_none()
31        {
32            ctx.emit(&UNALIASED_PLAIN_IMPORT, import.path.span);
33        }
34    }
35
36    fn check_full_source_unit(&mut self, ctx: &LintContext<'ast, '_>, ast: &'ast SourceUnit<'ast>) {
37        // Disabled lints are filtered inside `ctx.emit()`, but the full traversal is expensive.
38        if !ctx.is_lint_enabled(UNUSED_IMPORT.id) {
39            return;
40        }
41        let mut checker =
42            UsedSymbols { source_map: ctx.session().source_map(), used: FxHashSet::default() };
43        let _ = checker.visit_source_unit(ast);
44        let used = checker.used;
45
46        for item in ast.items.iter() {
47            let ast::ItemKind::Import(import) = &item.kind else { continue };
48            match &import.items {
49                ast::ImportItems::Aliases(symbols) => {
50                    for &(orig, alias) in symbols.iter() {
51                        let name = alias.unwrap_or(orig);
52                        if !used.contains(&name.name) {
53                            ctx.emit(&UNUSED_IMPORT, orig.span.to(name.span));
54                        }
55                    }
56                }
57                ast::ImportItems::Plain(_) | ast::ImportItems::Glob(_) => {
58                    if let Some(alias) = import.source_alias()
59                        && !used.contains(&alias.name)
60                    {
61                        ctx.emit(&UNUSED_IMPORT, item.span);
62                    }
63                }
64            }
65        }
66    }
67}
68
69/// Collects every symbol a source unit refers to outside its import directives.
70struct UsedSymbols<'ast> {
71    source_map: &'ast SourceMap,
72    used: FxHashSet<Symbol>,
73}
74
75impl<'ast> Visit<'ast> for UsedSymbols<'ast> {
76    type BreakValue = Never;
77
78    fn visit_item(&mut self, item: &'ast ast::Item<'ast>) -> ControlFlow<Self::BreakValue> {
79        if let ast::ItemKind::Import(_) = &item.kind {
80            return ControlFlow::Continue(());
81        }
82        self.walk_item(item)
83    }
84
85    fn visit_using_directive(
86        &mut self,
87        using: &'ast ast::UsingDirective<'ast>,
88    ) -> ControlFlow<Self::BreakValue> {
89        match &using.list {
90            ast::UsingList::Single(path) => {
91                self.used.insert(path.first().name);
92            }
93            ast::UsingList::Multiple(items) => {
94                self.used.extend(items.iter().map(|(path, _)| path.first().name));
95            }
96        }
97        self.walk_using_directive(using)
98    }
99
100    fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
101        if let ast::ExprKind::Ident(id) = expr.kind {
102            self.used.insert(id.name);
103        }
104        self.walk_expr(expr)
105    }
106
107    fn visit_path(&mut self, path: &'ast ast::PathSlice) -> ControlFlow<Self::BreakValue> {
108        self.used.extend(path.segments().iter().map(|id| id.name));
109        self.walk_path(path)
110    }
111
112    fn visit_ty(&mut self, ty: &'ast ast::Type<'ast>) -> ControlFlow<Self::BreakValue> {
113        if let ast::TypeKind::Custom(path) = &ty.kind {
114            self.used.insert(path.first().name);
115        }
116        self.walk_ty(ty)
117    }
118
119    fn visit_doc_comment(&mut self, cmnt: &'ast ast::DocComment) -> ControlFlow<Self::BreakValue> {
120        if let Ok(snip) = self.source_map.span_to_snippet(cmnt.span) {
121            for line in snip.lines() {
122                if let Some((_, relevant)) = line.split_once("@inheritdoc") {
123                    self.used.insert(Symbol::intern(relevant.trim()));
124                }
125            }
126        }
127        ControlFlow::Continue(())
128    }
129}