Skip to content

Commit b4e202d

Browse files
Add Equatable and Hashable property retention (#1126)
Co-authored-by: Ian Leitch <ian@leitch.io>
1 parent 3125d42 commit b4e202d

12 files changed

Lines changed: 175 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
##### Enhancements
88

9+
- Added the `--retain-equatable-properties` and `--retain-hashable-properties` options to retain all properties on `Equatable` and `Hashable` types.
910
- Expose a stable `@periphery//bazel:generated` package group so Bazel projects can grant visibility to Periphery's generated scan target and use `--bazel-check-visibility` safely.
1011
- Added a `--bazel-query` option to override the default Bazel top-level target query.
1112

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- [Unused Imports](#unused-imports)
3232
- [Objective-C](#objective-c)
3333
- [Codable](#codable)
34+
- [Equatable and Hashable](#equatable-and-hashable)
3435
- [XCTestCase](#xctestcase)
3536
- [Interface Builder](#interface-builder)
3637
- [SPI (System Programming Interface)](#spi-system-programming-interface)
@@ -312,6 +313,10 @@ Swift synthesizes additional code for `Codable` types that is not visible to Per
312313

313314
If `Codable` conformance is declared by a protocol in an external module not scanned by Periphery, you can instruct Periphery to identify the protocols as `Codable` with `--external-codable-protocols "ExternalProtocol"`.
314315

316+
### Equatable and Hashable
317+
318+
Swift synthesizes additional code for `Equatable` and `Hashable` types that is not visible to Periphery and can result in false positives for properties not directly referenced from non-synthesized code. If your project contains many such types, you can retain all properties on `Equatable` types with `--retain-equatable-properties`, or all properties on `Hashable` types with `--retain-hashable-properties`. The `Equatable` option also retains properties on `Hashable` types because `Hashable` refines `Equatable`.
319+
315320
### XCTestCase
316321

317322
Any class that inherits `XCTestCase` is automatically retained along with its test methods. However, when a class inherits `XCTestCase` indirectly via another class, e.g., `UnitTestCase`, and that class resides in a target that isn't scanned by Periphery, you need to use the `--external-test-case-classes UnitTestCase` option to instruct Periphery to treat `UnitTestCase` as an `XCTestCase` subclass.

Sources/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ swift_library(
6868
"SourceGraph/Mutators/DynamicMemberRetainer.swift",
6969
"SourceGraph/Mutators/EntryPointAttributeRetainer.swift",
7070
"SourceGraph/Mutators/EnumCaseReferenceBuilder.swift",
71+
"SourceGraph/Mutators/EquatableHashablePropertyRetainer.swift",
7172
"SourceGraph/Mutators/ExtensionReferenceBuilder.swift",
7273
"SourceGraph/Mutators/ExternalOverrideRetainer.swift",
7374
"SourceGraph/Mutators/ExternalTypeProtocolConformanceReferenceRemover.swift",

Sources/Configuration/Configuration.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ public final class Configuration {
9595
@Setting(key: "retain_encodable_properties", defaultValue: false)
9696
public var retainEncodableProperties: Bool
9797

98+
@Setting(key: "retain_equatable_properties", defaultValue: false)
99+
public var retainEquatableProperties: Bool
100+
101+
@Setting(key: "retain_hashable_properties", defaultValue: false)
102+
public var retainHashableProperties: Bool
103+
98104
@Setting(key: "verbose", defaultValue: false)
99105
public var verbose: Bool
100106

@@ -226,7 +232,8 @@ public final class Configuration {
226232
$externalEncodableProtocols, $externalCodableProtocols, $externalTestCaseClasses, $verbose, $quiet, $color,
227233
$disableUpdateCheck, $strict, $indexStorePath,
228234
$skipBuild, $skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults,
229-
$jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $baseline, $writeBaseline,
235+
$jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $retainEquatableProperties,
236+
$retainHashableProperties, $baseline, $writeBaseline,
230237
$writeResults, $genericProjectConfig, $bazel, $bazelFilter, $bazelQuery, $bazelIndexStore, $bazelCheckVisibility,
231238
]
232239

Sources/Frontend/Commands/ScanCommand.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ struct ScanCommand: ParsableCommand {
105105
@Flag(help: "Retain properties on Encodable types only")
106106
var retainEncodableProperties: Bool = defaultConfiguration.$retainEncodableProperties.defaultValue
107107

108+
@Flag(help: "Retain properties on Equatable types, including Hashable types")
109+
var retainEquatableProperties: Bool = defaultConfiguration.$retainEquatableProperties.defaultValue
110+
111+
@Flag(help: "Retain properties on Hashable types")
112+
var retainHashableProperties: Bool = defaultConfiguration.$retainHashableProperties.defaultValue
113+
108114
@Flag(help: "Clean existing build artifacts before building")
109115
var cleanBuild: Bool = defaultConfiguration.$cleanBuild.defaultValue
110116

@@ -217,6 +223,8 @@ struct ScanCommand: ParsableCommand {
217223
configuration.apply(\.$relativeResults, relativeResults)
218224
configuration.apply(\.$retainCodableProperties, retainCodableProperties)
219225
configuration.apply(\.$retainEncodableProperties, retainEncodableProperties)
226+
configuration.apply(\.$retainEquatableProperties, retainEquatableProperties)
227+
configuration.apply(\.$retainHashableProperties, retainHashableProperties)
220228
configuration.apply(\.$jsonPackageManifestPath, jsonPackageManifestPath)
221229
configuration.apply(\.$baseline, baseline)
222230
configuration.apply(\.$writeBaseline, writeBaseline)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Configuration
2+
import Foundation
3+
import Shared
4+
5+
final class EquatableHashablePropertyRetainer: SourceGraphMutator {
6+
private let graph: SourceGraph
7+
private let configuration: Configuration
8+
9+
required init(graph: SourceGraph, configuration: Configuration, swiftVersion _: SwiftVersion) {
10+
self.graph = graph
11+
self.configuration = configuration
12+
}
13+
14+
func mutate() {
15+
for decl in graph.declarations(ofKinds: Declaration.Kind.discreteConformableKinds) {
16+
guard decl.kind != .class, shouldRetainProperties(of: decl) else { continue }
17+
18+
for decl in decl.declarations {
19+
guard decl.kind == .varInstance else { continue }
20+
21+
graph.markRetained(decl)
22+
}
23+
}
24+
}
25+
26+
private func shouldRetainProperties(of decl: Declaration) -> Bool {
27+
if configuration.retainEquatableProperties, graph.isEquatable(decl) {
28+
return true
29+
}
30+
31+
if configuration.retainHashableProperties, graph.isHashable(decl) {
32+
return true
33+
}
34+
35+
return false
36+
}
37+
}

Sources/SourceGraph/SourceGraph.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,4 +403,18 @@ public final class SourceGraph {
403403
[.protocol, .typealias].contains($0.declarationKind) && encodableTypes.contains($0.name)
404404
}
405405
}
406+
407+
func isEquatable(_ decl: Declaration) -> Bool {
408+
let equatableTypes = ["Equatable", "Hashable"]
409+
410+
return inheritedTypeReferences(of: decl).contains {
411+
[.protocol, .typealias].contains($0.declarationKind) && equatableTypes.contains($0.name)
412+
}
413+
}
414+
415+
func isHashable(_ decl: Declaration) -> Bool {
416+
inheritedTypeReferences(of: decl).contains {
417+
[.protocol, .typealias].contains($0.declarationKind) && $0.name == "Hashable"
418+
}
419+
}
406420
}

Sources/SourceGraph/SourceGraphMutatorRunner.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public final class SourceGraphMutatorRunner {
4444
PropertyWrapperRetainer.self,
4545
ResultBuilderRetainer.self,
4646
CodablePropertyRetainer.self,
47+
EquatableHashablePropertyRetainer.self,
4748
ExternalOverrideRetainer.self,
4849

4950
AncestralReferenceEliminator.self,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Foundation
2+
3+
public struct FixtureStruct222: Equatable {
4+
let unused: Int
5+
6+
init(unused: Int) {
7+
self.unused = unused
8+
}
9+
}
10+
11+
public struct FixtureStruct223: Hashable {
12+
let unused: Int
13+
14+
init(unused: Int) {
15+
self.unused = unused
16+
}
17+
}
18+
19+
public final class FixtureClass222: Equatable {
20+
let unused: Int
21+
22+
init(unused: Int) {
23+
self.unused = unused
24+
}
25+
26+
public static func == (lhs: FixtureClass222, rhs: FixtureClass222) -> Bool {
27+
true
28+
}
29+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
public struct FixtureStruct224: Hashable {
4+
let unused: Int
5+
6+
init(unused: Int) {
7+
self.unused = unused
8+
}
9+
}

0 commit comments

Comments
 (0)