diff --git a/lint/unused_import_analyzer.go b/lint/unused_import_analyzer.go index 5c3ba943..973fb11c 100644 --- a/lint/unused_import_analyzer.go +++ b/lint/unused_import_analyzer.go @@ -119,6 +119,64 @@ var UnusedImportAnalyzer = (func() *analysis.Analyzer { } }, ) + // TODO: Remove the below section once https://github.com/onflow/cadence/issues/4470 is fixed. + // The Walk methods for composite, interface, and attachment declarations + // do not recurse into conformance lists, attachment base types, or + // entitlement access specifiers, so the inspector never visits the + // NominalType nodes there. Check those positions explicitly. + + markNominalTypeUsed := func(t *ast.NominalType) { + name := t.Identifier.Identifier + if _, isImported := importedNames[name]; isImported { + usedImports[name] = struct{}{} + } + } + + markEntitlementAccessUsed := func(access ast.Access) { + entAccess, ok := access.(ast.EntitlementAccess) + if !ok { + return + } + for _, e := range entAccess.EntitlementSet.Entitlements() { + markNominalTypeUsed(e) + } + } + + inspector.Preorder( + []ast.Element{ + (*ast.CompositeDeclaration)(nil), + (*ast.InterfaceDeclaration)(nil), + (*ast.AttachmentDeclaration)(nil), + (*ast.FunctionDeclaration)(nil), + (*ast.FieldDeclaration)(nil), + }, + func(element ast.Element) { + switch decl := element.(type) { + case *ast.CompositeDeclaration: + markEntitlementAccessUsed(decl.Access) + for _, c := range decl.Conformances { + markNominalTypeUsed(c) + } + case *ast.InterfaceDeclaration: + markEntitlementAccessUsed(decl.Access) + for _, c := range decl.Conformances { + markNominalTypeUsed(c) + } + case *ast.AttachmentDeclaration: + markEntitlementAccessUsed(decl.Access) + if decl.BaseType != nil { + markNominalTypeUsed(decl.BaseType) + } + for _, c := range decl.Conformances { + markNominalTypeUsed(c) + } + case *ast.FunctionDeclaration: + markEntitlementAccessUsed(decl.Access) + case *ast.FieldDeclaration: + markEntitlementAccessUsed(decl.Access) + } + }, + ) // Collect unused imports. // For implicit imports, we only report if ALL imports from that declaration are unused diff --git a/lint/unused_import_analyzer_test.go b/lint/unused_import_analyzer_test.go index ea092495..79a7915e 100644 --- a/lint/unused_import_analyzer_test.go +++ b/lint/unused_import_analyzer_test.go @@ -505,6 +505,96 @@ func TestUnusedImportAnalyzer(t *testing.T) { ) }) + t.Run("used only in composite conformance list", func(t *testing.T) { + t.Parallel() + + const code = ` + import Bar from 0x01 + + access(all) resource Foo: Bar.Interface {} + ` + + diagnostics := testAnalyzersWithImports(t, code, lint.UnusedImportAnalyzer) + require.Equal(t, + []analysis.Diagnostic(nil), + diagnostics, + ) + }) + + t.Run("used only in interface conformance list", func(t *testing.T) { + t.Parallel() + + const code = ` + import Bar from 0x01 + + access(all) resource interface MyInterface: Bar.Interface {} + ` + + diagnostics := testAnalyzersWithImports(t, code, lint.UnusedImportAnalyzer) + require.Equal(t, + []analysis.Diagnostic(nil), + diagnostics, + ) + }) + + t.Run("used only in attachment base type", func(t *testing.T) { + t.Parallel() + + const code = ` + import Bar from 0x01 + + access(all) attachment MyAttachment for Bar.Resource {} + ` + + diagnostics := testAnalyzersWithImports(t, code, lint.UnusedImportAnalyzer) + require.Equal(t, + []analysis.Diagnostic(nil), + diagnostics, + ) + }) + + t.Run("used only in entitlement access on function member", func(t *testing.T) { + t.Parallel() + + const code = ` + import Bar from 0x01 + + access(all) resource Foo { + access(Bar.Entitlement) fun method(): Int { + return 1 + } + } + ` + + diagnostics := testAnalyzersWithImports(t, code, lint.UnusedImportAnalyzer) + require.Equal(t, + []analysis.Diagnostic(nil), + diagnostics, + ) + }) + + t.Run("used only in entitlement access on field member", func(t *testing.T) { + t.Parallel() + + const code = ` + import Bar from 0x01 + + access(all) resource Foo { + access(Bar.Entitlement) let value: Int + + init() { + self.value = 0 + } + } + ` + + diagnostics := testAnalyzersWithImports(t, code, lint.UnusedImportAnalyzer) + require.Equal(t, + []analysis.Diagnostic(nil), + diagnostics, + ) + }) + t.Run("no imports", func(t *testing.T) { t.Parallel() @@ -559,6 +649,12 @@ func testAnalyzersWithImports(t *testing.T, code string, analyzers ...*analysis. barContract := ` access(all) contract Bar { + access(all) entitlement Entitlement + + access(all) resource interface Interface {} + + access(all) resource Resource {} + access(all) struct SomeStruct {} access(all) fun doSomething() {}