diff --git a/CHANGELOG.md b/CHANGELOG.md index 717fc5ff2..54918a70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -223,6 +223,11 @@ [SimplyDanny](https://github.com/SimplyDanny) [#5489](https://github.com/realm/SwiftLint/pull/5489) +* Ensure that declarations referenced only as extended types do not count as + used by means of the `unused_declaration` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5550](https://github.com/realm/SwiftLint/issues/5550) + * Fix some false positives in `multiline_literal_brackets` rule that would happen when comments are present. [Marcelo Fabri](https://github.com/marcelofabri) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift index 00e56b480..02ce72474 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift @@ -46,7 +46,7 @@ struct UnusedDeclarationRule: AnalyzerRule, CollectingRule { } return FileUSRs( - referenced: file.referencedUSRs(index: index), + referenced: file.referencedUSRs(index: index, editorOpen: editorOpen), declared: file.declaredUSRs(index: index, editorOpen: editorOpen, compilerArguments: compilerArguments, @@ -89,11 +89,16 @@ private extension SwiftLintFile { .map(SourceKittenDictionary.init) } - func referencedUSRs(index: SourceKittenDictionary) -> Set { - return Set(index.traverseEntitiesDepthFirst { _, entity -> String? in + func referencedUSRs(index: SourceKittenDictionary, editorOpen: SourceKittenDictionary) -> Set { + return Set(index.traverseEntitiesDepthFirst { parent, entity -> String? in if let usr = entity.usr, - let kind = entity.kind, - kind.starts(with: "source.lang.swift.ref") { + let kind = entity.kind, + kind.starts(with: "source.lang.swift.ref"), + !parent.extends(reference: entity), + let line = entity.line, + let column = entity.column, + let nameOffset = stringView.byteOffset(forLine: line, bytePosition: column), + editorOpen.propertyAtOffset(nameOffset, property: \.kind) != "source.lang.swift.decl.extension" { return usr } @@ -133,7 +138,8 @@ private extension SwiftLintFile { return nil } - if !configuration.includePublicAndOpen, [.public, .open].contains(editorOpen.aclAtOffset(nameOffset)) { + if !configuration.includePublicAndOpen, + [.public, .open].contains(editorOpen.propertyAtOffset(nameOffset, property: \.accessibility)) { return nil } @@ -230,14 +236,14 @@ private extension SourceKittenDictionary { return value["key.is_implicit"] as? Bool == true } - func aclAtOffset(_ offset: ByteCount) -> AccessControlLevel? { + func propertyAtOffset(_ offset: ByteCount, property: KeyPath) -> T? { if let nameOffset, nameOffset == offset, - let acl = accessibility { - return acl + let field = self[keyPath: property] { + return field } for child in substructure { - if let acl = child.aclAtOffset(offset) { + if let acl = child.propertyAtOffset(offset, property: property) { return acl } } @@ -296,6 +302,21 @@ private extension SourceKittenDictionary { return resultBuilderStaticMethods.contains(name) } + + func extends(reference other: Self) -> Bool { + if let kind, kind.starts(with: "source.lang.swift.decl.extension") { + let extendedKind = kind.components(separatedBy: ".").last + return extendedKind != nil && extendedKind == other.referencedKind + } + return false + } + + private var referencedKind: String? { + if let kind, kind.starts(with: "source.lang.swift.ref") { + return kind.components(separatedBy: ".").last + } + return nil + } } // Skip initializers, deinit, enum cases and subscripts since we can't reliably detect if they're used. diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRuleExamples.swift index 937838f2e..3118ed3ba 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRuleExamples.swift @@ -207,8 +207,23 @@ struct UnusedDeclarationRuleExamples { } _ = ComponentBuilder() + """), + Example(""" + protocol ↓Foo {} + extension Foo {} + """), + Example(""" + class ↓C {} + extension C {} """) - ] + platformSpecificTriggeringExamples + ] + ["actor", "enum", "class", "struct"].map { + Example(""" + protocol Foo {} + \($0) ↓FooImpl {} + extension FooImpl {} + extension FooImpl: Foo {} + """, excludeFromDocumentation: true) + } + platformSpecificTriggeringExamples #if os(macOS) private static let platformSpecificNonTriggeringExamples = [