Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

##### Enhancements

- Added the `--retain-equatable-properties` and `--retain-hashable-properties` options to retain all properties on `Equatable` and `Hashable` types.
- 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.
- Added a `--bazel-query` option to override the default Bazel top-level target query.

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [Unused Imports](#unused-imports)
- [Objective-C](#objective-c)
- [Codable](#codable)
- [Equatable and Hashable](#equatable-and-hashable)
- [XCTestCase](#xctestcase)
- [Interface Builder](#interface-builder)
- [SPI (System Programming Interface)](#spi-system-programming-interface)
Expand Down Expand Up @@ -312,6 +313,10 @@ Swift synthesizes additional code for `Codable` types that is not visible to Per

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"`.

### Equatable and Hashable

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`.

### XCTestCase

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.
Expand Down
1 change: 1 addition & 0 deletions Sources/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ swift_library(
"SourceGraph/Mutators/DynamicMemberRetainer.swift",
"SourceGraph/Mutators/EntryPointAttributeRetainer.swift",
"SourceGraph/Mutators/EnumCaseReferenceBuilder.swift",
"SourceGraph/Mutators/EquatableHashablePropertyRetainer.swift",
"SourceGraph/Mutators/ExtensionReferenceBuilder.swift",
"SourceGraph/Mutators/ExternalOverrideRetainer.swift",
"SourceGraph/Mutators/ExternalTypeProtocolConformanceReferenceRemover.swift",
Expand Down
9 changes: 8 additions & 1 deletion Sources/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ public final class Configuration {
@Setting(key: "retain_encodable_properties", defaultValue: false)
public var retainEncodableProperties: Bool

@Setting(key: "retain_equatable_properties", defaultValue: false)
public var retainEquatableProperties: Bool

@Setting(key: "retain_hashable_properties", defaultValue: false)
public var retainHashableProperties: Bool

@Setting(key: "verbose", defaultValue: false)
public var verbose: Bool

Expand Down Expand Up @@ -226,7 +232,8 @@ public final class Configuration {
$externalEncodableProtocols, $externalCodableProtocols, $externalTestCaseClasses, $verbose, $quiet, $color,
$disableUpdateCheck, $strict, $indexStorePath,
$skipBuild, $skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults,
$jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $baseline, $writeBaseline,
$jsonPackageManifestPath, $retainCodableProperties, $retainEncodableProperties, $retainEquatableProperties,
$retainHashableProperties, $baseline, $writeBaseline,
$writeResults, $genericProjectConfig, $bazel, $bazelFilter, $bazelQuery, $bazelIndexStore, $bazelCheckVisibility,
]

Expand Down
8 changes: 8 additions & 0 deletions Sources/Frontend/Commands/ScanCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ struct ScanCommand: ParsableCommand {
@Flag(help: "Retain properties on Encodable types only")
var retainEncodableProperties: Bool = defaultConfiguration.$retainEncodableProperties.defaultValue

@Flag(help: "Retain properties on Equatable types, including Hashable types")
var retainEquatableProperties: Bool = defaultConfiguration.$retainEquatableProperties.defaultValue

@Flag(help: "Retain properties on Hashable types")
var retainHashableProperties: Bool = defaultConfiguration.$retainHashableProperties.defaultValue

@Flag(help: "Clean existing build artifacts before building")
var cleanBuild: Bool = defaultConfiguration.$cleanBuild.defaultValue

Expand Down Expand Up @@ -217,6 +223,8 @@ struct ScanCommand: ParsableCommand {
configuration.apply(\.$relativeResults, relativeResults)
configuration.apply(\.$retainCodableProperties, retainCodableProperties)
configuration.apply(\.$retainEncodableProperties, retainEncodableProperties)
configuration.apply(\.$retainEquatableProperties, retainEquatableProperties)
configuration.apply(\.$retainHashableProperties, retainHashableProperties)
configuration.apply(\.$jsonPackageManifestPath, jsonPackageManifestPath)
configuration.apply(\.$baseline, baseline)
configuration.apply(\.$writeBaseline, writeBaseline)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Configuration
import Foundation
import Shared

final class EquatableHashablePropertyRetainer: SourceGraphMutator {
private let graph: SourceGraph
private let configuration: Configuration

required init(graph: SourceGraph, configuration: Configuration, swiftVersion _: SwiftVersion) {
self.graph = graph
self.configuration = configuration
}

func mutate() {
for decl in graph.declarations(ofKinds: Declaration.Kind.discreteConformableKinds) {
guard decl.kind != .class, shouldRetainProperties(of: decl) else { continue }

for decl in decl.declarations {
guard decl.kind == .varInstance else { continue }

graph.markRetained(decl)
}
}
}

private func shouldRetainProperties(of decl: Declaration) -> Bool {
if configuration.retainEquatableProperties, graph.isEquatable(decl) {
return true
}

if configuration.retainHashableProperties, graph.isHashable(decl) {
return true
}

return false
}
}
14 changes: 14 additions & 0 deletions Sources/SourceGraph/SourceGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,4 +403,18 @@ public final class SourceGraph {
[.protocol, .typealias].contains($0.declarationKind) && encodableTypes.contains($0.name)
}
}

func isEquatable(_ decl: Declaration) -> Bool {
let equatableTypes = ["Equatable", "Hashable"]

return inheritedTypeReferences(of: decl).contains {
[.protocol, .typealias].contains($0.declarationKind) && equatableTypes.contains($0.name)
}
}

func isHashable(_ decl: Declaration) -> Bool {
inheritedTypeReferences(of: decl).contains {
[.protocol, .typealias].contains($0.declarationKind) && $0.name == "Hashable"
}
}
}
1 change: 1 addition & 0 deletions Sources/SourceGraph/SourceGraphMutatorRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public final class SourceGraphMutatorRunner {
PropertyWrapperRetainer.self,
ResultBuilderRetainer.self,
CodablePropertyRetainer.self,
EquatableHashablePropertyRetainer.self,
ExternalOverrideRetainer.self,

AncestralReferenceEliminator.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

public struct FixtureStruct222: Equatable {
let unused: Int

init(unused: Int) {
self.unused = unused
}
}

public struct FixtureStruct223: Hashable {
let unused: Int

init(unused: Int) {
self.unused = unused
}
}

public final class FixtureClass222: Equatable {
let unused: Int

init(unused: Int) {
self.unused = unused
}

public static func == (lhs: FixtureClass222, rhs: FixtureClass222) -> Bool {
true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

public struct FixtureStruct224: Hashable {
let unused: Int

init(unused: Int) {
self.unused = unused
}
}
58 changes: 58 additions & 0 deletions Tests/PeripheryTests/RetentionTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,64 @@ final class RetentionTest: FixtureSourceGraphTestCase {
}
}

func testRetainsEquatableProperties() {
analyze(
retainPublic: true,
retainEquatableProperties: false,
retainAssignOnlyProperties: false
) {
assertReferenced(.struct("FixtureStruct222")) {
self.assertNotReferenced(.functionConstructor("init(unused:)"))
self.assertAssignOnlyProperty(.varInstance("unused"))
}
}

analyze(
retainPublic: true,
retainEquatableProperties: true
) {
assertReferenced(.struct("FixtureStruct222")) {
self.assertNotReferenced(.functionConstructor("init(unused:)"))
self.assertReferenced(.varInstance("unused"))
self.assertNotAssignOnlyProperty(.varInstance("unused"))
}

assertReferenced(.struct("FixtureStruct223")) {
self.assertNotReferenced(.functionConstructor("init(unused:)"))
self.assertReferenced(.varInstance("unused"))
self.assertNotAssignOnlyProperty(.varInstance("unused"))
}

assertReferenced(.class("FixtureClass222")) {
self.assertAssignOnlyProperty(.varInstance("unused"))
}
}
}

func testRetainsHashableProperties() {
analyze(
retainPublic: true,
retainHashableProperties: false,
retainAssignOnlyProperties: false
) {
assertReferenced(.struct("FixtureStruct224")) {
self.assertNotReferenced(.functionConstructor("init(unused:)"))
self.assertAssignOnlyProperty(.varInstance("unused"))
}
}

analyze(
retainPublic: true,
retainHashableProperties: true
) {
assertReferenced(.struct("FixtureStruct224")) {
self.assertNotReferenced(.functionConstructor("init(unused:)"))
self.assertReferenced(.varInstance("unused"))
self.assertNotAssignOnlyProperty(.varInstance("unused"))
}
}
}

func testRetainsFilesOption() {
analyze(retainFiles: [testFixturePath.string]) {
assertReferenced(.class("FixtureClass100"))
Expand Down
4 changes: 4 additions & 0 deletions Tests/Shared/FixtureSourceGraphTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class FixtureSourceGraphTestCase: SPMSourceGraphTestCase {
superfluousIgnoreComments: Bool = true,
retainCodableProperties: Bool = false,
retainEncodableProperties: Bool = false,
retainEquatableProperties: Bool = false,
retainHashableProperties: Bool = false,
retainUnusedProtocolFuncParams: Bool = false,
retainAssignOnlyProperties: Bool = false,
retainAssignOnlyPropertyTypes: [String] = [],
Expand All @@ -40,6 +42,8 @@ class FixtureSourceGraphTestCase: SPMSourceGraphTestCase {
configuration.externalCodableProtocols = externalCodableProtocols
configuration.retainCodableProperties = retainCodableProperties
configuration.retainEncodableProperties = retainEncodableProperties
configuration.retainEquatableProperties = retainEquatableProperties
configuration.retainHashableProperties = retainHashableProperties
configuration.retainUnusedProtocolFuncParams = retainUnusedProtocolFuncParams
configuration.retainAssignOnlyPropertyTypes = retainAssignOnlyPropertyTypes
configuration.externalTestCaseClasses = externalTestCaseClasses
Expand Down
Loading