diff --git a/CHANGELOG.md b/CHANGELOG.md index e81e6ba47..ac9824c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Add experimental unused import analysis. - The option `--external-encodable-protocols` is deprecated, use `--external-codable-protocols` instead. +- CodingKey enum case are now identified as unused if the corresponding property is also unused in the Codable type. ##### Bug Fixes diff --git a/Sources/PeripheryKit/SourceGraph/Mutators/CodingKeyEnumReferenceBuilder.swift b/Sources/PeripheryKit/SourceGraph/Mutators/CodingKeyEnumReferenceBuilder.swift index f7320dd65..93ac39b75 100644 --- a/Sources/PeripheryKit/SourceGraph/Mutators/CodingKeyEnumReferenceBuilder.swift +++ b/Sources/PeripheryKit/SourceGraph/Mutators/CodingKeyEnumReferenceBuilder.swift @@ -26,12 +26,28 @@ final class CodingKeyEnumReferenceBuilder: SourceGraphMutator { return [.protocol, .typealias].contains($0.kind) && codableTypes.contains(name) } - if isCodingKey && isParentCodable { - for usr in enumDeclaration.usrs { - let newReference = Reference(kind: .enum, usr: usr, location: enumDeclaration.location) - newReference.name = enumDeclaration.name - newReference.parent = parent - graph.add(newReference, from: parent) + guard isCodingKey, isParentCodable else { continue } + + // Build a reference from the Codable type to the CodingKey enum. + for usr in enumDeclaration.usrs { + let newReference = Reference(kind: .enum, usr: usr, location: enumDeclaration.location) + newReference.name = enumDeclaration.name + newReference.parent = parent + graph.add(newReference, from: parent) + } + + // For each property in the Codable type, build a reference to its corresponding + // CodingKey enum element. + for decl in parent.declarations { + guard decl.kind == .varInstance, + let enumCase = enumDeclaration.declarations.first(where: { $0.kind == .enumelement && $0.name == decl.name }) + else { continue } + + for usr in enumCase.usrs { + let newReference = Reference(kind: .enumelement, usr: usr, location: decl.location) + newReference.name = enumCase.name + newReference.parent = decl + graph.add(newReference, from: decl) } } } diff --git a/Sources/PeripheryKit/SourceGraph/Mutators/EncodablePropertyRetainer.swift b/Sources/PeripheryKit/SourceGraph/Mutators/EncodablePropertyRetainer.swift deleted file mode 100644 index 0408b6835..000000000 --- a/Sources/PeripheryKit/SourceGraph/Mutators/EncodablePropertyRetainer.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation -import Shared - -/// Retains properties of discrete conforming declarations that directly, or indirectly conform to `Encodable`. -/// -/// The Swift compiler synthesizes code for `Encodable` that is not exposed in the index store. We therefore must -/// assume that all properties are in use, as they may be referenced by synthesized code. -final class EncodablePropertyRetainer: SourceGraphMutator { - private let graph: SourceGraph - private let externalCodableProtocols: [String] - - required init(graph: SourceGraph, configuration: Configuration) { - self.graph = graph - self.externalCodableProtocols = configuration.externalEncodableProtocols + configuration.externalCodableProtocols - } - - func mutate() { - graph - .declarations(ofKinds: Declaration.Kind.discreteConformableKinds) - .lazy - .filter { self.hasEncodableConformance($0) } - .forEach { - $0.declarations - .lazy - .filter { $0.kind == .varInstance } - .forEach { graph.markRetained($0) } - } - } - - // MARK: - Private - - private func hasEncodableConformance(_ decl: Declaration) -> Bool { - graph - .inheritedTypeReferences(of: decl) - .contains { isEncodableReference($0) } - } - - private func isEncodableReference(_ ref: Reference) -> Bool { - if ref.kind == .protocol && ref.name == "Encodable" || ref.kind == .typealias && ref.name == "Codable" { - return true - } - - if let name = ref.name { - if graph.isExternal(ref), externalCodableProtocols.contains(name) { - return true - } - } - - return false - } -} diff --git a/Sources/PeripheryKit/SourceGraph/SourceGraphMutatorRunner.swift b/Sources/PeripheryKit/SourceGraph/SourceGraphMutatorRunner.swift index 91752a223..28e9b3014 100644 --- a/Sources/PeripheryKit/SourceGraph/SourceGraphMutatorRunner.swift +++ b/Sources/PeripheryKit/SourceGraph/SourceGraphMutatorRunner.swift @@ -38,7 +38,6 @@ public final class SourceGraphMutatorRunner { PubliclyAccessibleRetainer.self, XCTestRetainer.self, SwiftUIRetainer.self, - EncodablePropertyRetainer.self, StringInterpolationAppendInterpolationRetainer.self, PropertyWrapperRetainer.self, ResultBuilderRetainer.self, diff --git a/Tests/Fixtures/RetentionFixtures/testCodingKeyEnum.swift b/Tests/Fixtures/RetentionFixtures/testCodingKeyEnum.swift index f52c3e352..e4c5fafeb 100644 --- a/Tests/Fixtures/RetentionFixtures/testCodingKeyEnum.swift +++ b/Tests/Fixtures/RetentionFixtures/testCodingKeyEnum.swift @@ -62,3 +62,13 @@ public struct FixtureClass218: CustomStringConvertible { case someVar } } + +public class FixtureClass220: Encodable { + public var someUsedVar: String? + var someUnusedVar: String? + + enum CodingKeys: CodingKey { + case someUsedVar + case someUnusedVar + } +} diff --git a/Tests/PeripheryTests/RetentionTest.swift b/Tests/PeripheryTests/RetentionTest.swift index bd35faf08..389385f6d 100644 --- a/Tests/PeripheryTests/RetentionTest.swift +++ b/Tests/PeripheryTests/RetentionTest.swift @@ -574,6 +574,15 @@ final class RetentionTest: FixtureSourceGraphTestCase { assertReferenced(.struct("FixtureClass218")) { self.assertReferenced(.enum("CodingKeys")) } + assertReferenced(.class("FixtureClass220")) { + self.assertReferenced(.varInstance("someUsedVar")) + self.assertNotReferenced(.varInstance("someUnusedVar")) + + self.assertReferenced(.enum("CodingKeys")) { + self.assertReferenced(.enumelement("someUsedVar")) + self.assertNotReferenced(.enumelement("someUnusedVar")) + } + } } } @@ -951,43 +960,6 @@ final class RetentionTest: FixtureSourceGraphTestCase { } } - func testRetainsEncodableProperties() { - let configuration = Configuration.shared - // CustomStringConvertible doesn't actually inherit Encodable, we're just using it because we don't have an - // external module in which to declare our own type. - configuration.externalCodableProtocols = ["CustomStringConvertible"] - - analyze(retainPublic: true) { - self.assertReferenced(.class("FixtureClass204")) { - self.assertReferenced(.varInstance("someVar")) - } - - self.assertReferenced(.class("FixtureClass205")) { - self.assertReferenced(.varInstance("someVar")) - } - - self.assertReferenced(.class("FixtureClass206")) { - self.assertReferenced(.varInstance("someVar")) - } - - self.assertReferenced(.class("FixtureClass207")) { - self.assertReferenced(.varInstance("someVar")) - } - - self.assertReferenced(.class("FixtureClass208")) { - self.assertReferenced(.varInstance("someVar")) - } - - self.assertReferenced(.class("FixtureClass209")) { - self.assertReferenced(.varInstance("someVar")) - } - - self.assertReferenced(.class("FixtureClass210")) { - self.assertReferenced(.varInstance("someVar")) - } - } - } - func testCircularTypeInheritance() { analyze { // Intentionally blank.