forge_lint/sol/info/
imports.rs

1use solar_ast::{self as ast, SourceUnit, Span, Symbol, visit::Visit};
2use solar_data_structures::map::FxIndexSet;
3use solar_interface::SourceMap;
4use std::ops::ControlFlow;
5
6use super::Imports;
7use crate::{
8    linter::{EarlyLintPass, LintContext},
9    sol::{Severity, SolLint},
10};
11
12declare_forge_lint!(
13    UNUSED_IMPORT,
14    Severity::Info,
15    "unused-import",
16    "unused imports should be removed"
17);
18
19declare_forge_lint!(
20    UNALIASED_PLAIN_IMPORT,
21    Severity::Info,
22    "unaliased-plain-import",
23    "use named imports '{A, B}' or alias 'import \"..\" as X'"
24);
25
26impl<'ast> EarlyLintPass<'ast> for Imports {
27    fn check_import_directive(
28        &mut self,
29        ctx: &LintContext<'_>,
30        import: &'ast ast::ImportDirective<'ast>,
31    ) {
32        // Non-aliased plain imports like `import "File.sol";`.
33        if let ast::ImportItems::Plain(_) = &import.items
34            && import.source_alias().is_none()
35        {
36            ctx.emit(&UNALIASED_PLAIN_IMPORT, import.path.span);
37        }
38    }
39
40    fn check_full_source_unit(&mut self, ctx: &LintContext<'ast>, ast: &'ast SourceUnit<'ast>) {
41        let mut checker = UnusedChecker::new(ctx.session().source_map());
42        let _ = checker.visit_source_unit(ast);
43        checker.check_unused_imports(ast, ctx);
44        checker.clear();
45    }
46}
47
48/// Visitor that collects all used symbols in a source unit.
49struct UnusedChecker<'ast> {
50    used_symbols: FxIndexSet<Symbol>,
51    source_map: &'ast SourceMap,
52}
53
54impl<'ast> UnusedChecker<'ast> {
55    fn new(source_map: &'ast SourceMap) -> Self {
56        Self { source_map, used_symbols: Default::default() }
57    }
58
59    fn clear(&mut self) {
60        self.used_symbols.clear();
61    }
62
63    /// Mark a symbol as used in a source.
64    fn mark_symbol_used(&mut self, symbol: Symbol) {
65        self.used_symbols.insert(symbol);
66    }
67
68    /// Check for unused imports and emit warnings.
69    fn check_unused_imports(&self, ast: &SourceUnit<'_>, ctx: &LintContext<'_>) {
70        for item in ast.items.iter() {
71            let span = item.span;
72            let ast::ItemKind::Import(import) = &item.kind else { continue };
73            match &import.items {
74                ast::ImportItems::Plain(_) | ast::ImportItems::Glob(_) => {
75                    if let Some(alias) = import.source_alias()
76                        && !self.used_symbols.contains(&alias.name)
77                    {
78                        self.unused_import(ctx, span);
79                    }
80                }
81                ast::ImportItems::Aliases(symbols) => {
82                    for &(orig, alias) in symbols.iter() {
83                        let name = alias.unwrap_or(orig);
84                        if !self.used_symbols.contains(&name.name) {
85                            self.unused_import(ctx, orig.span.to(name.span));
86                        }
87                    }
88                }
89            }
90        }
91    }
92
93    fn unused_import(&self, ctx: &LintContext<'_>, span: Span) {
94        ctx.emit(&UNUSED_IMPORT, span);
95    }
96}
97
98impl<'ast> Visit<'ast> for UnusedChecker<'ast> {
99    type BreakValue = solar_data_structures::Never;
100
101    fn visit_item(&mut self, item: &'ast ast::Item<'ast>) -> ControlFlow<Self::BreakValue> {
102        if let ast::ItemKind::Import(_) = &item.kind {
103            return ControlFlow::Continue(());
104        }
105
106        self.walk_item(item)
107    }
108
109    fn visit_using_directive(
110        &mut self,
111        using: &'ast ast::UsingDirective<'ast>,
112    ) -> ControlFlow<Self::BreakValue> {
113        match &using.list {
114            ast::UsingList::Single(path) => {
115                self.mark_symbol_used(path.first().name);
116            }
117            ast::UsingList::Multiple(items) => {
118                for (path, _) in items.iter() {
119                    self.mark_symbol_used(path.first().name);
120                }
121            }
122        }
123
124        self.walk_using_directive(using)
125    }
126
127    fn visit_function_header(
128        &mut self,
129        header: &'ast solar_ast::FunctionHeader<'ast>,
130    ) -> ControlFlow<Self::BreakValue> {
131        // temporary workaround until solar also visits `override` and its paths <https://github.com/paradigmxyz/solar/pull/383>.
132        if let Some(ref override_) = header.override_ {
133            for path in override_.paths.iter() {
134                _ = self.visit_path(path);
135            }
136        }
137
138        self.walk_function_header(header)
139    }
140
141    fn visit_expr(&mut self, expr: &'ast ast::Expr<'ast>) -> ControlFlow<Self::BreakValue> {
142        if let ast::ExprKind::Ident(id) = expr.kind {
143            self.mark_symbol_used(id.name);
144        }
145
146        self.walk_expr(expr)
147    }
148
149    fn visit_path(&mut self, path: &'ast ast::PathSlice) -> ControlFlow<Self::BreakValue> {
150        for id in path.segments() {
151            self.mark_symbol_used(id.name);
152        }
153
154        self.walk_path(path)
155    }
156
157    fn visit_ty(&mut self, ty: &'ast ast::Type<'ast>) -> ControlFlow<Self::BreakValue> {
158        if let ast::TypeKind::Custom(path) = &ty.kind {
159            self.mark_symbol_used(path.first().name);
160        }
161
162        self.walk_ty(ty)
163    }
164
165    fn visit_doc_comment(
166        &mut self,
167        cmnt: &'ast solar_ast::DocComment,
168    ) -> ControlFlow<Self::BreakValue> {
169        if let Ok(snip) = self.source_map.span_to_snippet(cmnt.span) {
170            for line in snip.lines() {
171                if let Some((_, relevant)) = line.split_once("@inheritdoc") {
172                    self.mark_symbol_used(Symbol::intern(relevant.trim()));
173                }
174            }
175        }
176        ControlFlow::Continue(())
177    }
178}