forge_lint/sol/info/
imports.rs1use 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 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 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
54struct 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 fn mark_symbol_used(&mut self, symbol: Symbol) {
71 self.used_symbols.insert(symbol);
72 }
73
74 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}