Skip to content
Open
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 @@ -8,6 +8,7 @@

- Added the `--retain-unused-imported-modules` option.
- Added the `--format gitlab-codemagic` formatting option for GitLabs Code Quality artifact reports
- Added the `--retain-ibaction` option and `retain_ibaction` configuration key to retain Interface Builder actions in Swift Package Manager targets.

##### Bug Fixes

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ Any class that inherits `XCTestCase` is automatically retained along with its te

If your project contains Interface Builder files (such as storyboards and XIBs), Periphery will take these into account when identifying unused declarations. However, Periphery currently only identifies unused classes. This limitation exists because Periphery does not yet fully parse Interface Builder files (see [issue #212](https://github.com/peripheryapp/periphery/issues/212)). Due to Periphery's design principle of avoiding false positives, it is assumed that if a class is referenced in an Interface Builder file, all of its `IBOutlets` and `IBActions` are used, even if they might not be in reality. This approach will be revised to accurately identify unused `IBActions` and `IBOutlets` once Periphery gains the capability to parse Interface Builder files.

If your Swift Package Manager targets include Interface Builder actions that aren't referenced during analysis, enable the `retain_ibaction` setting in `.periphery.yml`, or pass `--retain-ibaction` on the command line. With this setting, methods annotated with `@IBAction` or `@IBSegueAction` (and their containing types) are retained automatically.

## Comment Commands

For whatever reason, you may want to keep some unused code. Source code comment commands can be used to ignore specific declarations and exclude them from the results.
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/ExternalOverrideRetainer.swift",
"SourceGraph/Mutators/ExternalTypeProtocolConformanceReferenceRemover.swift",
"SourceGraph/Mutators/GenericClassAndStructConstructorReferenceBuilder.swift",
"SourceGraph/Mutators/InterfaceBuilderActionRetainer.swift",
"SourceGraph/Mutators/InterfaceBuilderPropertyRetainer.swift",
"SourceGraph/Mutators/ObjCAccessibleRetainer.swift",
"SourceGraph/Mutators/PropertyWrapperRetainer.swift",
Expand Down
5 changes: 4 additions & 1 deletion Sources/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public final class Configuration {
@Setting(key: "retain_swift_ui_previews", defaultValue: false)
public var retainSwiftUIPreviews: Bool

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

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

Expand Down Expand Up @@ -203,7 +206,7 @@ public final class Configuration {
lazy var settings: [any AbstractSetting] = [
$project, $schemes, $excludeTargets, $excludeTests, $indexExclude, $reportExclude, $reportInclude, $outputFormat,
$retainPublic, $retainFiles, $retainAssignOnlyProperties, $retainAssignOnlyPropertyTypes, $retainObjcAccessible,
$retainObjcAnnotated, $retainUnusedProtocolFuncParams, $retainSwiftUIPreviews, $disableRedundantPublicAnalysis,
$retainObjcAnnotated, $retainUnusedProtocolFuncParams, $retainSwiftUIPreviews, $retainIbaction, $disableRedundantPublicAnalysis,
$disableUnusedImportAnalysis, $retainUnusedImportedModules, $externalEncodableProtocols, $externalCodableProtocols,
$externalTestCaseClasses, $verbose, $quiet, $disableUpdateCheck, $strict, $indexStorePath, $skipBuild,
$skipSchemesValidation, $cleanBuild, $buildArguments, $xcodeListArguments, $relativeResults, $jsonPackageManifestPath,
Expand Down
4 changes: 4 additions & 0 deletions Sources/Frontend/Commands/ScanCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ struct ScanCommand: FrontendCommand {
@Flag(help: "Retain SwiftUI previews")
var retainSwiftUIPreviews: Bool = defaultConfiguration.$retainSwiftUIPreviews.defaultValue

@Flag(help: "Retain declarations annotated with @IBAction or @IBSegueAction")
var retainIbaction: Bool = defaultConfiguration.$retainIbaction.defaultValue

@Flag(help: "Retain properties on Codable types (including Encodable and Decodable)")
var retainCodableProperties: Bool = defaultConfiguration.$retainCodableProperties.defaultValue

Expand Down Expand Up @@ -173,6 +176,7 @@ struct ScanCommand: FrontendCommand {
configuration.apply(\.$retainObjcAnnotated, retainObjcAnnotated)
configuration.apply(\.$retainUnusedProtocolFuncParams, retainUnusedProtocolFuncParams)
configuration.apply(\.$retainSwiftUIPreviews, retainSwiftUIPreviews)
configuration.apply(\.$retainIbaction, retainIbaction)
configuration.apply(\.$disableRedundantPublicAnalysis, disableRedundantPublicAnalysis)
configuration.apply(\.$disableUnusedImportAnalysis, disableUnusedImportAnalysis)
configuration.apply(\.$retainUnusedImportedModules, retainUnusedImportedModules)
Expand Down
26 changes: 26 additions & 0 deletions Sources/SourceGraph/Mutators/InterfaceBuilderActionRetainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Configuration
import Foundation
import Shared

final class InterfaceBuilderActionRetainer: SourceGraphMutator {
private let graph: SourceGraph
private let configuration: Configuration
private static let actionAttributes: Set<String> = ["IBAction", "IBSegueAction"]

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

func mutate() {
guard configuration.retainIbaction else { return }

graph.allDeclarations
.lazy
.filter { !$0.attributes.isDisjoint(with: Self.actionAttributes) }
.forEach { declaration in
graph.markRetained(declaration)
declaration.ancestralDeclarations.forEach { graph.markRetained($0) }
}
}
}
1 change: 1 addition & 0 deletions Sources/SourceGraph/SourceGraphMutatorRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public final class SourceGraphMutatorRunner {
DynamicMemberRetainer.self,
UnusedParameterRetainer.self,
AssetReferenceRetainer.self,
InterfaceBuilderActionRetainer.self,
EntryPointAttributeRetainer.self,
PubliclyAccessibleRetainer.self,
XCTestRetainer.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class RetainIbactionFixture {
@IBAction func tapped(_ sender: Any) {}
}
23 changes: 20 additions & 3 deletions Tests/PeripheryTests/RetentionTest.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Configuration
import SystemPackage
@testable import TestShared
import XCTest
Expand Down Expand Up @@ -984,9 +985,13 @@ final class RetentionTest: FixtureSourceGraphTestCase {
}

func testRetainsFilesOption() {
analyze(retainFiles: [testFixturePath.string]) {
assertReferenced(.class("FixtureClass100"))
}
let retainConfiguration = Configuration()
retainConfiguration.retainFiles = [testFixturePath.string]
retainConfiguration.buildFilenameMatchers()

index(sourceFiles: [testFixturePath], configuration: retainConfiguration)

assertReferenced(.class("FixtureClass100"))

analyze(retainFiles: []) {
assertNotReferenced(.class("FixtureClass100"))
Expand Down Expand Up @@ -1665,6 +1670,18 @@ final class RetentionTest: FixtureSourceGraphTestCase {
}
}

func testRetainsInterfaceBuilderActionsWhenConfigured() {
let configuration = Configuration()
configuration.retainIbaction = true
configuration.buildFilenameMatchers()

index(sourceFiles: [testFixturePath], configuration: configuration)

assertReferenced(.class("RetainIbactionFixture")) {
self.assertReferenced(.functionMethodInstance("tapped(_:)"))
}
}

// MARK: - Known Failures

// https://github.com/apple/swift/issues/56165
Expand Down
2 changes: 1 addition & 1 deletion Tests/Shared/SourceGraphTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ open class SourceGraphTestCase: XCTestCase {
} else {
guard let declaration = materialize(description, file: file, line: line) else { return }

if !Self.graph.usedDeclarations.contains(declaration) {
if !Self.graph.usedDeclarations.contains(declaration) && !Self.graph.isRetained(declaration) {
XCTFail("Expected declaration to be referenced: \(declaration)", file: file, line: line)
}

Expand Down