Skip to content

Commit 63ba7fa

Browse files
authored
Merge pull request #590 from onflow/bastian/add-unused-import-analyzer
[lint] Add a new analyzer to detect unused imports
2 parents d8701a5 + 52d7ee7 commit 63ba7fa

3 files changed

Lines changed: 1039 additions & 0 deletions

File tree

lint/analyzer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const (
3131
UnnecessaryCastCategory = "unnecessary-cast-hint"
3232
UnnecessaryTypeAnnotationCategory = "unnecessary-type-annotation-hint"
3333
UnusedResultCategory = "unused-result-hint"
34+
UnusedImportCategory = "unused-import-hint"
3435
UnusedVariableCategory = "unused-variable-hint"
3536
DeprecatedCategory = "deprecated"
3637
CadenceV1Category = "cadence-v1"

lint/unused_import_analyzer.go

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/*
2+
* Cadence lint - The Cadence linter
3+
*
4+
* Copyright Flow Foundation
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package lint
20+
21+
import (
22+
"fmt"
23+
"sort"
24+
25+
"github.com/onflow/cadence/ast"
26+
"github.com/onflow/cadence/tools/analysis"
27+
)
28+
29+
var UnusedImportAnalyzer = (func() *analysis.Analyzer {
30+
31+
return &analysis.Analyzer{
32+
Description: "Detects unused imports",
33+
Requires: []*analysis.Analyzer{
34+
analysis.InspectorAnalyzer,
35+
},
36+
Run: func(pass *analysis.Pass) interface{} {
37+
inspector := pass.ResultOf[analysis.InspectorAnalyzer].(*ast.Inspector)
38+
39+
program := pass.Program
40+
location := program.Location
41+
elaboration := program.Checker.Elaboration
42+
report := pass.Report
43+
44+
type importInfo struct {
45+
importDecl *ast.ImportDeclaration
46+
// For explicit imports (e.g., import A from 0x1), this is the specific Import.
47+
// For implicit imports (e.g., import 0x1), this is nil.
48+
explicitImport *ast.Import
49+
}
50+
51+
importedNames := map[string]importInfo{}
52+
usedImports := map[string]struct{}{}
53+
54+
// Collect all imports using resolved locations
55+
56+
inspector.Preorder(
57+
[]ast.Element{
58+
(*ast.ImportDeclaration)(nil),
59+
},
60+
func(element ast.Element) {
61+
importDecl := element.(*ast.ImportDeclaration)
62+
63+
// If there are explicit imports,
64+
// build a map from original name to import for them
65+
66+
var explicitImportMap map[string]*ast.Import
67+
if len(importDecl.Imports) > 0 {
68+
explicitImportMap = make(map[string]*ast.Import)
69+
for i := range importDecl.Imports {
70+
imp := &importDecl.Imports[i]
71+
explicitImportMap[imp.Identifier.Identifier] = imp
72+
}
73+
}
74+
75+
resolvedLocations := elaboration.ImportDeclarationResolvedLocations(importDecl)
76+
aliases := elaboration.ImportDeclarationAliases(importDecl)
77+
78+
for _, resolved := range resolvedLocations {
79+
for _, identifier := range resolved.Identifiers {
80+
originalName := identifier.Identifier
81+
name := originalName
82+
83+
if alias, hasAlias := aliases[originalName]; hasAlias {
84+
name = alias
85+
}
86+
87+
importedNames[name] = importInfo{
88+
importDecl: importDecl,
89+
explicitImport: explicitImportMap[originalName],
90+
}
91+
}
92+
}
93+
},
94+
)
95+
96+
// Track usage in identifier expressions and nominal types
97+
98+
inspector.Preorder(
99+
[]ast.Element{
100+
(*ast.IdentifierExpression)(nil),
101+
(*ast.NominalType)(nil),
102+
},
103+
func(element ast.Element) {
104+
var name string
105+
106+
switch element := element.(type) {
107+
case *ast.IdentifierExpression:
108+
name = element.Identifier.Identifier
109+
110+
case *ast.NominalType:
111+
name = element.Identifier.Identifier
112+
113+
default:
114+
return
115+
}
116+
117+
if _, isImported := importedNames[name]; isImported {
118+
usedImports[name] = struct{}{}
119+
}
120+
},
121+
)
122+
123+
// Collect unused imports.
124+
// For implicit imports, we only report if ALL imports from that declaration are unused
125+
126+
type unusedImport struct {
127+
name string
128+
info importInfo
129+
}
130+
var unused []unusedImport
131+
132+
// First, for implicit imports, check if any name from each declaration is used
133+
implicitDeclsWithSomeUsed := map[*ast.ImportDeclaration]struct{}{}
134+
for name, info := range importedNames {
135+
_, used := usedImports[name]
136+
if info.explicitImport == nil && used {
137+
implicitDeclsWithSomeUsed[info.importDecl] = struct{}{}
138+
}
139+
}
140+
141+
// Now collect unused imports.
142+
// For implicit imports, we only want to report the declaration once,
143+
// not once per unused name, so track which declarations we've already added.
144+
implicitDeclsReported := map[*ast.ImportDeclaration]struct{}{}
145+
for name, info := range importedNames {
146+
if _, used := usedImports[name]; !used {
147+
// For implicit imports, skip if any name from the same declaration is used
148+
_, ok := implicitDeclsWithSomeUsed[info.importDecl]
149+
if info.explicitImport == nil && ok {
150+
continue
151+
}
152+
153+
// For implicit imports, deduplicate by declaration
154+
if info.explicitImport == nil {
155+
if _, reported := implicitDeclsReported[info.importDecl]; reported {
156+
continue
157+
}
158+
implicitDeclsReported[info.importDecl] = struct{}{}
159+
}
160+
161+
unused = append(
162+
unused,
163+
unusedImport{
164+
name: name,
165+
info: info,
166+
},
167+
)
168+
}
169+
}
170+
171+
// Sort by source position for deterministic ordering.
172+
// If positions are equal (e.g., implicit imports), sort by name
173+
174+
sort.Slice(unused, func(i, j int) bool {
175+
var posA, posB ast.Position
176+
177+
unusedA := unused[i].info
178+
if unusedA.explicitImport != nil {
179+
posA = unusedA.explicitImport.Identifier.StartPosition()
180+
} else {
181+
posA = unusedA.importDecl.StartPosition()
182+
}
183+
184+
unusedB := unused[j].info
185+
if unusedB.explicitImport != nil {
186+
posB = unusedB.explicitImport.Identifier.StartPosition()
187+
} else {
188+
posB = unusedB.importDecl.StartPosition()
189+
}
190+
191+
cmp := posA.Compare(posB)
192+
if cmp != 0 {
193+
return cmp < 0
194+
}
195+
return unused[i].name < unused[j].name
196+
})
197+
198+
// Report in sorted order
199+
for _, u := range unused {
200+
var (
201+
diagRange ast.Range
202+
message string
203+
suggestedFixes []analysis.SuggestedFix
204+
)
205+
206+
importDecl := u.info.importDecl
207+
explicitImport := u.info.explicitImport
208+
209+
if explicitImport != nil {
210+
// For explicit imports, report just the identifier
211+
diagRange = ast.NewRangeFromPositioned(nil, explicitImport.Identifier)
212+
message = fmt.Sprintf("unused import '%s'", u.name)
213+
214+
// Only provide a fix if this is the only import in the declaration.
215+
// For multiple imports, removing one is complex due to comma handling.
216+
if len(importDecl.Imports) == 1 {
217+
// Remove entire declaration including leading whitespace and trailing newline
218+
declRange := ast.NewRangeFromPositioned(nil, importDecl)
219+
220+
startPos := declRange.StartPos
221+
endPos := declRange.EndPos
222+
223+
suggestedFixes = []analysis.SuggestedFix{
224+
{
225+
Message: "Remove unused import",
226+
TextEdits: []analysis.TextEdit{
227+
{
228+
Range: ast.Range{
229+
StartPos: ast.Position{
230+
Offset: startPos.Offset - startPos.Column,
231+
Line: startPos.Line,
232+
Column: 0,
233+
},
234+
EndPos: ast.Position{
235+
// +1 to include newline
236+
Offset: endPos.Offset + 1,
237+
Line: endPos.Line + 1,
238+
Column: 0,
239+
},
240+
},
241+
Replacement: "",
242+
},
243+
},
244+
},
245+
}
246+
}
247+
} else {
248+
declRange := ast.NewRangeFromPositioned(nil, importDecl)
249+
250+
// For implicit imports, report the whole declaration
251+
diagRange = declRange
252+
message = "unused import"
253+
254+
// Remove entire declaration including leading whitespace and trailing newline
255+
startPos := declRange.StartPos
256+
endPos := declRange.EndPos
257+
258+
suggestedFixes = []analysis.SuggestedFix{
259+
{
260+
Message: "Remove unused import",
261+
TextEdits: []analysis.TextEdit{
262+
{
263+
Range: ast.Range{
264+
StartPos: ast.Position{
265+
Offset: startPos.Offset - startPos.Column,
266+
Line: startPos.Line,
267+
Column: 0,
268+
},
269+
EndPos: ast.Position{
270+
// +1 to include newline
271+
Offset: endPos.Offset + 1,
272+
Line: endPos.Line + 1,
273+
Column: 0,
274+
},
275+
},
276+
Replacement: "",
277+
},
278+
},
279+
},
280+
}
281+
}
282+
283+
report(
284+
analysis.Diagnostic{
285+
Location: location,
286+
Range: diagRange,
287+
Category: UnusedImportCategory,
288+
Message: message,
289+
SuggestedFixes: suggestedFixes,
290+
},
291+
)
292+
}
293+
294+
return nil
295+
},
296+
}
297+
})()
298+
299+
func init() {
300+
RegisterAnalyzer(
301+
"unused-import",
302+
UnusedImportAnalyzer,
303+
)
304+
}

0 commit comments

Comments
 (0)