From c86211bebae98a9d08b0829ae2167226f3a5db8c Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Wed, 28 May 2025 20:52:38 -0500 Subject: [PATCH 1/3] wip: unused imports --- crates/lint/src/linter.rs | 76 ++++++++++++++++-- crates/lint/src/sol/info/mixed_case.rs | 2 +- crates/lint/src/sol/info/mod.rs | 6 +- .../lint/src/sol/info/screaming_snake_case.rs | 2 +- crates/lint/src/sol/info/unused_import.rs | 78 +++++++++++++++++++ crates/lint/src/sol/mod.rs | 6 +- crates/lint/testdata/UnusedImport.sol | 24 ++++++ crates/lint/testdata/UnusedImport.stderr | 24 ++++++ 8 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 crates/lint/src/sol/info/unused_import.rs create mode 100644 crates/lint/testdata/UnusedImport.sol create mode 100644 crates/lint/testdata/UnusedImport.stderr diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index 2c11e0222a286..ba9460be6548c 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -1,12 +1,15 @@ use foundry_compilers::Language; use foundry_config::lint::Severity; -use solar_ast::{visit::Visit, Expr, ItemFunction, ItemStruct, VariableDefinition}; +use solar_ast::{ + visit::Visit, Expr, ImportDirective, ItemContract, ItemFunction, ItemStruct, Symbol, + UsingDirective, VariableDefinition, +}; use solar_interface::{ data_structures::Never, diagnostics::{DiagBuilder, DiagId, MultiSpan}, Session, Span, }; -use std::{ops::ControlFlow, path::PathBuf}; +use std::{collections::BTreeMap, ops::ControlFlow, path::PathBuf}; /// Trait representing a generic linter for analyzing and reporting issues in smart contract source /// code files. A linter can be implemented for any smart contract language supported by Foundry. @@ -37,11 +40,26 @@ pub trait Lint { pub struct LintContext<'s> { sess: &'s Session, desc: bool, + unused_imports: BTreeMap, } impl<'s> LintContext<'s> { pub fn new(sess: &'s Session, with_description: bool) -> Self { - Self { sess, desc: with_description } + Self { sess, desc: with_description, unused_imports: BTreeMap::new() } + } + + pub fn add_import(&mut self, import: (Symbol, Span)) { + self.unused_imports.insert(import.0, import.1); + } + + pub fn use_import(&mut self, import: Symbol) { + self.unused_imports.remove(&import); + } + + pub fn emit_unused_imports(&self, lint: &'static L) { + for (_, span) in &self.unused_imports { + self.emit(lint, span.to_owned()); + } } // Helper method to emit diagnostics easily from passes @@ -67,17 +85,35 @@ pub trait EarlyLintPass<'ast>: Send + Sync { fn check_item_function(&mut self, _ctx: &LintContext<'_>, _func: &'ast ItemFunction<'ast>) {} fn check_variable_definition( &mut self, - _ctx: &LintContext<'_>, + _ctx: &mut LintContext<'_>, _var: &'ast VariableDefinition<'ast>, ) { } + fn check_import_directive( + &mut self, + _ctx: &mut LintContext<'_>, + _import: &'ast ImportDirective<'ast>, + ) { + } + fn check_using_directive( + &mut self, + _ctx: &mut LintContext<'_>, + _using: &'ast UsingDirective<'ast>, + ) { + } + fn check_item_contract( + &mut self, + _ctx: &mut LintContext<'_>, + _contract: &'ast ItemContract<'ast>, + ) { + } // TODO: Add methods for each required AST node type } /// Visitor struct for `EarlyLintPass`es pub struct EarlyLintVisitor<'a, 's, 'ast> { - pub ctx: &'a LintContext<'s>, + pub ctx: &'a mut LintContext<'s>, pub passes: &'a mut [Box + 's>], } @@ -124,6 +160,36 @@ where self.walk_item_function(func) } + fn visit_import_directive( + &mut self, + import: &'ast ImportDirective<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_import_directive(self.ctx, import); + } + self.walk_import_directive(import) + } + + fn visit_using_directive( + &mut self, + using: &'ast UsingDirective<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_using_directive(self.ctx, using); + } + self.walk_using_directive(using) + } + + fn visit_item_contract( + &mut self, + contract: &'ast ItemContract<'ast>, + ) -> ControlFlow { + for pass in self.passes.iter_mut() { + pass.check_item_contract(self.ctx, contract); + } + self.walk_item_contract(contract) + } + // TODO: Add methods for each required AST node type, mirroring `solar_ast::visit::Visit` method // sigs + adding `LintContext` } diff --git a/crates/lint/src/sol/info/mixed_case.rs b/crates/lint/src/sol/info/mixed_case.rs index 5e839e9f313cf..2e73552172964 100644 --- a/crates/lint/src/sol/info/mixed_case.rs +++ b/crates/lint/src/sol/info/mixed_case.rs @@ -33,7 +33,7 @@ declare_forge_lint!( impl<'ast> EarlyLintPass<'ast> for MixedCaseVariable { fn check_variable_definition( &mut self, - ctx: &LintContext<'_>, + ctx: &mut LintContext<'_>, var: &'ast VariableDefinition<'ast>, ) { if var.mutability.is_none() { diff --git a/crates/lint/src/sol/info/mod.rs b/crates/lint/src/sol/info/mod.rs index 6c414864f9688..909238c0eda92 100644 --- a/crates/lint/src/sol/info/mod.rs +++ b/crates/lint/src/sol/info/mod.rs @@ -12,9 +12,13 @@ use pascal_case::PASCAL_CASE_STRUCT; mod screaming_snake_case; use screaming_snake_case::{SCREAMING_SNAKE_CASE_CONSTANT, SCREAMING_SNAKE_CASE_IMMUTABLE}; +mod unused_import; +pub use unused_import::UNUSED_IMPORT; + register_lints!( (PascalCaseStruct, (PASCAL_CASE_STRUCT)), (MixedCaseVariable, (MIXED_CASE_VARIABLE)), (MixedCaseFunction, (MIXED_CASE_FUNCTION)), - (ScreamingSnakeCase, (SCREAMING_SNAKE_CASE_CONSTANT, SCREAMING_SNAKE_CASE_IMMUTABLE)) + (ScreamingSnakeCase, (SCREAMING_SNAKE_CASE_CONSTANT, SCREAMING_SNAKE_CASE_IMMUTABLE)), + (UnusedImport, (UNUSED_IMPORT)) ); diff --git a/crates/lint/src/sol/info/screaming_snake_case.rs b/crates/lint/src/sol/info/screaming_snake_case.rs index ccc978029b8a4..996007b8b6bfa 100644 --- a/crates/lint/src/sol/info/screaming_snake_case.rs +++ b/crates/lint/src/sol/info/screaming_snake_case.rs @@ -23,7 +23,7 @@ declare_forge_lint!( impl<'ast> EarlyLintPass<'ast> for ScreamingSnakeCase { fn check_variable_definition( &mut self, - ctx: &LintContext<'_>, + ctx: &mut LintContext<'_>, var: &'ast VariableDefinition<'ast>, ) { if let (Some(name), Some(mutability)) = (var.name, var.mutability) { diff --git a/crates/lint/src/sol/info/unused_import.rs b/crates/lint/src/sol/info/unused_import.rs new file mode 100644 index 0000000000000..39ec94b14a3d4 --- /dev/null +++ b/crates/lint/src/sol/info/unused_import.rs @@ -0,0 +1,78 @@ +use solar_ast::{ImportItems, TypeKind, UsingList}; + +use super::UnusedImport; +use crate::{ + declare_forge_lint, + linter::{EarlyLintPass, LintContext}, + sol::{Severity, SolLint}, +}; + +declare_forge_lint!( + UNUSED_IMPORT, + Severity::Info, + "unused-import", + "unused imports should be removed" +); + +impl<'ast> EarlyLintPass<'ast> for UnusedImport { + fn check_import_directive( + &mut self, + ctx: &mut LintContext<'_>, + import: &'ast solar_ast::ImportDirective<'ast>, + ) { + // to begin with, only check explicit imports + if let ImportItems::Aliases(ref items) = import.items { + for item in &**items { + let (name, span) = if let Some(ref i) = &item.1 { + (&i.name, &i.span) + } else { + (&item.0.name, &item.0.span) + }; + + ctx.add_import((name.clone(), span.clone())); + } + } + } + + fn check_item_contract( + &mut self, + ctx: &mut LintContext<'_>, + contract: &'ast solar_ast::ItemContract<'ast>, + ) { + for modifier in &*contract.bases { + ctx.use_import(modifier.name.last().name.clone()); + } + } + + fn check_variable_definition( + &mut self, + ctx: &mut LintContext<'_>, + var: &'ast solar_ast::VariableDefinition<'ast>, + ) { + if let TypeKind::Custom(ty) = &var.ty.kind { + ctx.use_import(ty.last().name.clone()); + } + } + + fn check_using_directive( + &mut self, + ctx: &mut LintContext<'_>, + using: &'ast solar_ast::UsingDirective<'ast>, + ) { + match &using.list { + UsingList::Single(ty) => ctx.use_import(ty.last().name.clone()), + UsingList::Multiple(types) => { + for (ty, _operator) in &**types { + ctx.use_import(ty.last().name.clone()); + } + } + } + } +} + +#[cfg(test)] +mod test { + + #[test] + fn test_unused_imports() {} +} diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 463777d5b194a..4c89fd9f22eed 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -1,6 +1,7 @@ use crate::linter::{EarlyLintPass, EarlyLintVisitor, Lint, LintContext, Linter}; use foundry_compilers::solc::SolcLanguage; use foundry_config::lint::Severity; +use info::UNUSED_IMPORT; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use solar_ast::{visit::Visit, Arena}; use solar_interface::{ @@ -108,9 +109,10 @@ impl SolidityLinter { let ast = parser.parse_file().map_err(|e| e.emit())?; // Initialize and run the visitor - let ctx = LintContext::new(sess, self.with_description); - let mut visitor = EarlyLintVisitor { ctx: &ctx, passes: &mut passes }; + let mut ctx = LintContext::new(sess, self.with_description); + let mut visitor = EarlyLintVisitor { ctx: &mut ctx, passes: &mut passes }; _ = visitor.visit_source_unit(&ast); + ctx.emit_unused_imports(&UNUSED_IMPORT); Ok(()) }); diff --git a/crates/lint/testdata/UnusedImport.sol b/crates/lint/testdata/UnusedImport.sol new file mode 100644 index 0000000000000..8035223060c05 --- /dev/null +++ b/crates/lint/testdata/UnusedImport.sol @@ -0,0 +1,24 @@ +import { + symbol0 as mySymbol, + symbol1 as myOtherSymbol, + symbol2 as notUsed, //~NOTE: unused imports should be removed + symbol3, + symbol4, + symbol5, + symbolNotUsed, //~NOTE: unused imports should be removed + IContract, + IContractNotUsed //~NOTE: unused imports should be removed +} from "File.sol"; + +contract UnusedImport is IContract { + using mySymbol for address; + + struct FooBar { + symbol3 foo; + myOtherSymbol bar; + } + + symbol4 myVar; + + function foo(uint256 a, symbol5 b) public view {} +} diff --git a/crates/lint/testdata/UnusedImport.stderr b/crates/lint/testdata/UnusedImport.stderr new file mode 100644 index 0000000000000..f86984e203dca --- /dev/null +++ b/crates/lint/testdata/UnusedImport.stderr @@ -0,0 +1,24 @@ +note[unused-import]: unused imports should be removed + --> ROOT/testdata/UnusedImport.sol:LL:CC + | +4 | symbol2 as notUsed, + | ------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + +note[unused-import]: unused imports should be removed + --> ROOT/testdata/UnusedImport.sol:LL:CC + | +8 | symbolNotUsed, + | ------------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + +note[unused-import]: unused imports should be removed + --> ROOT/testdata/UnusedImport.sol:LL:CC + | +10 | IContractNotUsed + | ---------------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + From e2b30514a0bfb0664b1e4164ac96c9ce9dbcbe45 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Wed, 28 May 2025 23:13:43 -0500 Subject: [PATCH 2/3] track constant assignement --- crates/lint/src/sol/info/unused_import.rs | 15 +++++++-------- crates/lint/testdata/UnusedImport.sol | 7 +++++++ crates/lint/testdata/UnusedImport.stderr | 8 ++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/lint/src/sol/info/unused_import.rs b/crates/lint/src/sol/info/unused_import.rs index 39ec94b14a3d4..a83bded76b0bd 100644 --- a/crates/lint/src/sol/info/unused_import.rs +++ b/crates/lint/src/sol/info/unused_import.rs @@ -1,4 +1,4 @@ -use solar_ast::{ImportItems, TypeKind, UsingList}; +use solar_ast::{ExprKind, ImportItems, TypeKind, UsingList}; use super::UnusedImport; use crate::{ @@ -52,6 +52,12 @@ impl<'ast> EarlyLintPass<'ast> for UnusedImport { if let TypeKind::Custom(ty) = &var.ty.kind { ctx.use_import(ty.last().name.clone()); } + + if let Some(expr) = &var.initializer { + if let ExprKind::Ident(ident) = expr.kind { + ctx.use_import(ident.name.clone()); + } + } } fn check_using_directive( @@ -69,10 +75,3 @@ impl<'ast> EarlyLintPass<'ast> for UnusedImport { } } } - -#[cfg(test)] -mod test { - - #[test] - fn test_unused_imports() {} -} diff --git a/crates/lint/testdata/UnusedImport.sol b/crates/lint/testdata/UnusedImport.sol index 8035223060c05..9fbdfd107a3f2 100644 --- a/crates/lint/testdata/UnusedImport.sol +++ b/crates/lint/testdata/UnusedImport.sol @@ -10,9 +10,16 @@ import { IContractNotUsed //~NOTE: unused imports should be removed } from "File.sol"; +import { + CONSTANT_0, + CONSTANT_1 //~NOTE: unused imports should be removed +} from "Constants.sol"; + contract UnusedImport is IContract { using mySymbol for address; + uint256 constant MY_CONSTANT = CONSTANT_0; + struct FooBar { symbol3 foo; myOtherSymbol bar; diff --git a/crates/lint/testdata/UnusedImport.stderr b/crates/lint/testdata/UnusedImport.stderr index f86984e203dca..6484af993fab9 100644 --- a/crates/lint/testdata/UnusedImport.stderr +++ b/crates/lint/testdata/UnusedImport.stderr @@ -22,3 +22,11 @@ note[unused-import]: unused imports should be removed | = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import +note[unused-import]: unused imports should be removed + --> ROOT/testdata/UnusedImport.sol:LL:CC + | +15 | CONSTANT_1 + | ---------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + From 4194d2986f6f2c729589718ef5854d16fb38153f Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 29 May 2025 10:51:13 -0500 Subject: [PATCH 3/3] handle global imports --- crates/lint/src/linter.rs | 22 ++++--- crates/lint/src/sol/info/unused_import.rs | 70 +++++++++++++++++------ crates/lint/testdata/UnusedImport.sol | 30 +++++++++- crates/lint/testdata/UnusedImport.stderr | 32 +++++++++++ 4 files changed, 128 insertions(+), 26 deletions(-) diff --git a/crates/lint/src/linter.rs b/crates/lint/src/linter.rs index ba9460be6548c..a382008d4b884 100644 --- a/crates/lint/src/linter.rs +++ b/crates/lint/src/linter.rs @@ -9,7 +9,7 @@ use solar_interface::{ diagnostics::{DiagBuilder, DiagId, MultiSpan}, Session, Span, }; -use std::{collections::BTreeMap, ops::ControlFlow, path::PathBuf}; +use std::{collections::HashMap, ops::ControlFlow, path::PathBuf}; /// Trait representing a generic linter for analyzing and reporting issues in smart contract source /// code files. A linter can be implemented for any smart contract language supported by Foundry. @@ -40,12 +40,12 @@ pub trait Lint { pub struct LintContext<'s> { sess: &'s Session, desc: bool, - unused_imports: BTreeMap, + unused_imports: HashMap, } impl<'s> LintContext<'s> { pub fn new(sess: &'s Session, with_description: bool) -> Self { - Self { sess, desc: with_description, unused_imports: BTreeMap::new() } + Self { sess, desc: with_description, unused_imports: HashMap::new() } } pub fn add_import(&mut self, import: (Symbol, Span)) { @@ -56,13 +56,21 @@ impl<'s> LintContext<'s> { self.unused_imports.remove(&import); } - pub fn emit_unused_imports(&self, lint: &'static L) { - for (_, span) in &self.unused_imports { - self.emit(lint, span.to_owned()); + /// Helper method to easily emit diagnostics for unused imports. + /// Should be called after all passes have finished. + /// + /// Clears the `unused_imports` map. + pub fn emit_unused_imports(&mut self, lint: &'static L) { + let unused = std::mem::take(&mut self.unused_imports); + let mut spans = unused.into_values().collect::>(); + spans.sort(); + + for span in spans.into_iter() { + self.emit(lint, span); } } - // Helper method to emit diagnostics easily from passes + /// Helper method to emit diagnostics easily from passes pub fn emit(&self, lint: &'static L, span: Span) { let desc = if self.desc { lint.description() } else { "" }; let diag: DiagBuilder<'_, ()> = self diff --git a/crates/lint/src/sol/info/unused_import.rs b/crates/lint/src/sol/info/unused_import.rs index a83bded76b0bd..78ad4fd4ed911 100644 --- a/crates/lint/src/sol/info/unused_import.rs +++ b/crates/lint/src/sol/info/unused_import.rs @@ -1,4 +1,4 @@ -use solar_ast::{ExprKind, ImportItems, TypeKind, UsingList}; +use solar_ast::{Expr, ExprKind, ImportItems, PathSlice, Symbol, TypeKind, UsingList}; use super::UnusedImport; use crate::{ @@ -15,63 +15,99 @@ declare_forge_lint!( ); impl<'ast> EarlyLintPass<'ast> for UnusedImport { + /// Collects all the file imports and caches them in `LintContext`. fn check_import_directive( &mut self, ctx: &mut LintContext<'_>, import: &'ast solar_ast::ImportDirective<'ast>, ) { - // to begin with, only check explicit imports - if let ImportItems::Aliases(ref items) = import.items { - for item in &**items { - let (name, span) = if let Some(ref i) = &item.1 { - (&i.name, &i.span) - } else { - (&item.0.name, &item.0.span) - }; + match import.items { + ImportItems::Aliases(ref items) => { + for item in &**items { + let (name, span) = if let Some(ref i) = &item.1 { + (&i.name, &i.span) + } else { + (&item.0.name, &item.0.span) + }; - ctx.add_import((name.clone(), span.clone())); + ctx.add_import((*name, *span)); + } + } + ImportItems::Glob(ref ident) => { + ctx.add_import((ident.name, ident.span)); } + ImportItems::Plain(ref maybe) => match maybe { + Some(ident) => ctx.add_import((ident.name, ident.span)), + None => { + let path = import.path.value.to_string(); + let len = path.len() - 4; + ctx.add_import((Symbol::intern(&path[..len]), import.path.span)); + } + }, } } + /// Marks contract modifiers as used, effectively removing them from the `LintContext` cache. fn check_item_contract( &mut self, ctx: &mut LintContext<'_>, contract: &'ast solar_ast::ItemContract<'ast>, ) { for modifier in &*contract.bases { - ctx.use_import(modifier.name.last().name.clone()); + use_import_type(ctx, &modifier.name); } } + /// Marks variable definitions (both, variable type and initializer name) as used, + /// effectively removing them from the `LintContext` cache. fn check_variable_definition( &mut self, ctx: &mut LintContext<'_>, var: &'ast solar_ast::VariableDefinition<'ast>, ) { if let TypeKind::Custom(ty) = &var.ty.kind { - ctx.use_import(ty.last().name.clone()); + use_import_type(ctx, ty); } if let Some(expr) = &var.initializer { - if let ExprKind::Ident(ident) = expr.kind { - ctx.use_import(ident.name.clone()); - } + use_import_expr(ctx, expr); } } + /// Marks the types in a using directive as used, effectively removing them from the + /// `LintContext` cache. fn check_using_directive( &mut self, ctx: &mut LintContext<'_>, using: &'ast solar_ast::UsingDirective<'ast>, ) { match &using.list { - UsingList::Single(ty) => ctx.use_import(ty.last().name.clone()), + UsingList::Single(ty) => use_import_type(ctx, ty), UsingList::Multiple(types) => { for (ty, _operator) in &**types { - ctx.use_import(ty.last().name.clone()); + use_import_type(ctx, ty); } } } } } + +/// Marks the type as used. +/// If the type has more than one segment, it marks both, the first, and the last one. +fn use_import_type(ctx: &mut LintContext<'_>, ty: &&mut PathSlice) { + ctx.use_import(ty.last().name); + if ty.segments().len() != 1 { + ctx.use_import(ty.first().name); + } +} + +/// Marks the type as used. +/// If the type has more than one segment, it marks both, the first, and the last one. +fn use_import_expr<'ast>(ctx: &mut LintContext<'_>, expr: &&mut Expr<'ast>) { + match &expr.kind { + ExprKind::Ident(ident) => ctx.use_import(ident.name), + ExprKind::Member(ref expr, _) => use_import_expr(ctx, expr), + ExprKind::Call(ref expr, _) => use_import_expr(ctx, expr), + _ => (), + } +} diff --git a/crates/lint/testdata/UnusedImport.sol b/crates/lint/testdata/UnusedImport.sol index 9fbdfd107a3f2..cddfe405c2002 100644 --- a/crates/lint/testdata/UnusedImport.sol +++ b/crates/lint/testdata/UnusedImport.sol @@ -15,6 +15,22 @@ import { CONSTANT_1 //~NOTE: unused imports should be removed } from "Constants.sol"; +import { + MyTpe, + MyOtherType, + YetAnotherType //~NOTE: unused imports should be removed +} from "Types.sol"; + +import "SomeFile.sol"; +import "AnotherFile.sol"; //~NOTE: unused imports should be removed + +import "some_file_2.sol" as SomeFile2; +import "another_file_2.sol" as AnotherFile2; //~NOTE: unused imports should be removed + +import * as Utils from "utils.sol"; +import * as OtherUtils from "utils2.sol"; //~NOTE: unused imports should be removed + + contract UnusedImport is IContract { using mySymbol for address; @@ -25,7 +41,17 @@ contract UnusedImport is IContract { myOtherSymbol bar; } - symbol4 myVar; + SomeFile.Baz public myStruct; + SomeFile2.Baz public myStruct2; + symbol4 public myVar; + + function foo(uint256 a, symbol5 b) public view returns (uint256) { + uint256 c = Utils.calculate(a, b); + return c; + } - function foo(uint256 a, symbol5 b) public view {} + function convert(address addr) public pure returns (MyOtherType) { + MyType a = MyTpe.wrap(123); + return MyOtherType.wrap(a); + } } diff --git a/crates/lint/testdata/UnusedImport.stderr b/crates/lint/testdata/UnusedImport.stderr index 6484af993fab9..5b1cb488c5eb9 100644 --- a/crates/lint/testdata/UnusedImport.stderr +++ b/crates/lint/testdata/UnusedImport.stderr @@ -30,3 +30,35 @@ note[unused-import]: unused imports should be removed | = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import +note[unused-import]: unused imports should be removed + --> ROOT/testdata/UnusedImport.sol:LL:CC + | +21 | YetAnotherType + | -------------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + +note[unused-import]: unused imports should be removed + --> ROOT/testdata/UnusedImport.sol:LL:CC + | +25 | import "AnotherFile.sol"; + | ----------------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + +note[unused-import]: unused imports should be removed + --> ROOT/testdata/UnusedImport.sol:LL:CC + | +28 | import "another_file_2.sol" as AnotherFile2; + | ------------ + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + +note[unused-import]: unused imports should be removed + --> ROOT/testdata/UnusedImport.sol:LL:CC + | +31 | import * as OtherUtils from "utils2.sol"; + | ---------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import +