diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c59d856..76f425b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ##### Enhancements - Unused import detection is now enabled by default. +- Added the `--retain-encodable-properties` option to retain all properties on `Encodable` types only. ##### Bug Fixes diff --git a/Sources/Frontend/Commands/ScanCommand.swift b/Sources/Frontend/Commands/ScanCommand.swift index fd3ea5d14..ccf9fb582 100644 --- a/Sources/Frontend/Commands/ScanCommand.swift +++ b/Sources/Frontend/Commands/ScanCommand.swift @@ -87,9 +87,12 @@ struct ScanCommand: FrontendCommand { @Flag(help: "Retain SwiftUI previews") var retainSwiftUIPreviews: Bool = defaultConfiguration.$retainSwiftUIPreviews.defaultValue - @Flag(help: "Retain properties on Codable types") + @Flag(help: "Retain properties on Codable types (including Encodable and Decodable)") var retainCodableProperties: Bool = defaultConfiguration.$retainCodableProperties.defaultValue + @Flag(help: "Retain properties on Encodable types only") + var retainEncodableProperties: Bool = defaultConfiguration.$retainEncodableProperties.defaultValue + @Flag(help: "Automatically remove code that can be done so safely without introducing build errors (experimental)") var autoRemove: Bool = defaultConfiguration.$autoRemove.defaultValue @@ -165,6 +168,7 @@ struct ScanCommand: FrontendCommand { configuration.apply(\.$buildArguments, buildArguments) configuration.apply(\.$relativeResults, relativeResults) configuration.apply(\.$retainCodableProperties, retainCodableProperties) + configuration.apply(\.$retainEncodableProperties, retainEncodableProperties) configuration.apply(\.$jsonPackageManifestPath, jsonPackageManifestPath) try scanBehavior.main { project in diff --git a/Sources/PeripheryKit/SourceGraph/Mutators/CodablePropertyRetainer.swift b/Sources/PeripheryKit/SourceGraph/Mutators/CodablePropertyRetainer.swift index fd088bd8f..fa67174ac 100644 --- a/Sources/PeripheryKit/SourceGraph/Mutators/CodablePropertyRetainer.swift +++ b/Sources/PeripheryKit/SourceGraph/Mutators/CodablePropertyRetainer.swift @@ -11,14 +11,23 @@ final class CodablePropertyRetainer: SourceGraphMutator { } func mutate() { - guard configuration.retainCodableProperties else { return } + if configuration.retainCodableProperties { + for decl in graph.declarations(ofKinds: Declaration.Kind.discreteConformableKinds) { + guard graph.isCodable(decl) else { continue } - for decl in graph.declarations(ofKinds: Declaration.Kind.discreteConformableKinds) { - guard graph.isCodable(decl) else { continue } + for decl in decl.declarations { + guard decl.kind == .varInstance else { continue } + graph.markRetained(decl) + } + } + } else if configuration.retainEncodableProperties { + for decl in graph.declarations(ofKinds: Declaration.Kind.discreteConformableKinds) { + guard graph.isEncodable(decl) else { continue } - for decl in decl.declarations { - guard decl.kind == .varInstance else { continue } - graph.markRetained(decl) + for decl in decl.declarations { + guard decl.kind == .varInstance else { continue } + graph.markRetained(decl) + } } } } diff --git a/Sources/PeripheryKit/SourceGraph/SourceGraph.swift b/Sources/PeripheryKit/SourceGraph/SourceGraph.swift index 7b47ccf5d..af3422dba 100644 --- a/Sources/PeripheryKit/SourceGraph/SourceGraph.swift +++ b/Sources/PeripheryKit/SourceGraph/SourceGraph.swift @@ -362,4 +362,13 @@ public final class SourceGraph { return [.protocol, .typealias].contains($0.kind) && codableTypes.contains(name) } } + + func isEncodable(_ decl: Declaration) -> Bool { + let encodableTypes = ["Encodable"] + configuration.externalEncodableProtocols + configuration.externalCodableProtocols + + return inheritedTypeReferences(of: decl).contains { + guard let name = $0.name else { return false } + return [.protocol, .typealias].contains($0.kind) && encodableTypes.contains(name) + } + } } diff --git a/Sources/Shared/Configuration.swift b/Sources/Shared/Configuration.swift index 2d60c92d1..ac1dff466 100644 --- a/Sources/Shared/Configuration.swift +++ b/Sources/Shared/Configuration.swift @@ -83,6 +83,9 @@ public final class Configuration { @Setting(key: "retain_codable_properties", defaultValue: false) public var retainCodableProperties: Bool + @Setting(key: "retain_encodable_properties", defaultValue: false) + public var retainEncodableProperties: Bool + @Setting(key: "auto_remove", defaultValue: false) public var autoRemove: Bool @@ -254,6 +257,10 @@ public final class Configuration { if $retainCodableProperties.hasNonDefaultValue { config[$retainCodableProperties.key] = retainCodableProperties } + + if $retainEncodableProperties.hasNonDefaultValue { + config[$retainEncodableProperties.key] = retainEncodableProperties + } if $jsonPackageManifestPath.hasNonDefaultValue { config[$jsonPackageManifestPath.key] = jsonPackageManifestPath @@ -344,6 +351,8 @@ public final class Configuration { $relativeResults.assign(value) case $retainCodableProperties.key: $retainCodableProperties.assign(value) + case $retainEncodableProperties.key: + $retainEncodableProperties.assign(value) case $jsonPackageManifestPath.key: $jsonPackageManifestPath.assign(value) default: @@ -386,6 +395,7 @@ public final class Configuration { $buildArguments.reset() $relativeResults.reset() $retainCodableProperties.reset() + $retainEncodableProperties.reset() $jsonPackageManifestPath.reset() } diff --git a/Tests/Fixtures/RetentionFixtures/testRetainsEncodableProperties.swift b/Tests/Fixtures/RetentionFixtures/testRetainsEncodableProperties.swift new file mode 100644 index 000000000..f3943fae1 --- /dev/null +++ b/Tests/Fixtures/RetentionFixtures/testRetainsEncodableProperties.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct FixtureStruct15: Encodable { + let unused: Int + + init(unused: Int) { + self.unused = unused + } +} diff --git a/Tests/PeripheryTests/RetentionTest.swift b/Tests/PeripheryTests/RetentionTest.swift index ff0c60d7a..ef32a5517 100644 --- a/Tests/PeripheryTests/RetentionTest.swift +++ b/Tests/PeripheryTests/RetentionTest.swift @@ -1034,6 +1034,28 @@ final class RetentionTest: FixtureSourceGraphTestCase { } } + func testRetainsEncodableProperties() { + configuration.retainEncodableProperties = false + configuration.retainAssignOnlyProperties = false + + analyze(retainPublic: true) { + assertReferenced(.struct("FixtureStruct15")) { + self.assertNotReferenced(.functionConstructor("init(unused:)")) + self.assertAssignOnlyProperty(.varInstance("unused")) + } + } + + configuration.retainEncodableProperties = true + + analyze(retainPublic: true) { + assertReferenced(.struct("FixtureStruct15")) { + self.assertNotReferenced(.functionConstructor("init(unused:)")) + self.assertReferenced(.varInstance("unused")) + self.assertNotAssignOnlyProperty(.varInstance("unused")) + } + } + } + func testRetainsFilesOption() { configuration.retainFiles = [testFixturePath.string]