Skip to main content

forge_lint/sol/info/
imports.rs

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