brew install peripheryapp/periphery/periphery
mint install peripheryapp/periphery
scan
CommandThe scan command is Periphery's primary function. To begin a guided setup, simply change to your project directory and run:
periphery scan --setup
Guided setup only works for Xcode and SwiftPM projects, to use Periphery with non-Apple build systems such as Bazel, see Build Systems.
After answering a few questions, Periphery will print out the full scan command and execute it.
The guided setup is only intended for introductory purposes, once you are familiar with Periphery you can try some more advanced options, all of which can be seen with periphery help scan
.
To get coherent results from Periphery, it's crucial to understand the implications of the build targets you choose to analyze. For example, imagine a project consisting of three targets: App, Lib and Tests. The App target imports Lib, and the Tests targets imports both App and Lib. If you were to provide all three to the --targets
option then Periphery will be able to analyze your project as a whole. However, if you only choose to analyze App and Lib, but not Tests, Periphery may report some instances of unused code that are only referenced by Tests. Therefore when you suspect Periphery has provided an incorrect result, it's important to consider the targets that you have chosen to analyze.
If your project consists of one or more standalone frameworks that do not also contain some kind of application that consume their interfaces, then you'll need to tell Periphery to assume that all public declarations are in fact used by including the --retain-public
option.
For projects that are mixed Objective-C & Swift, it's highly recommend you read about the implications this can have on your results.
Once you've settled upon the appropriate options for your project, you may wish to persist them in a YAML configuration file. The simplest way to achieve this is to run Periphery with the --verbose
option. Near the beginning of the output you will see the [configuration:begin]
section with your configuration formatted as YAML below. Copy & paste the configuration into .periphery.yml
in the root of your project folder. You can now simply run periphery scan
and the YAML configuration will be used.
Periphery first builds your project. For Xcode projects the schemes provided via the --schemes
option are built using xcodebuild
. For Swift Package Manager projects, the individual targets provided via the --targets
option are built using swift build
. The Swift compiler employs a technique called index-while-building to populate an index store that contains information about the structure of your project's source code.
After your project is built, Periphery performs an indexing phase. For every source file that is a member of the targets provided via the --targets
option, Periphery obtains its structural information from the index store and builds its own internal graph representation of your project. Periphery also analyzes each file's abstract syntax tree (AST) to fill in some details not provided by the index store.
Once indexing is complete, Periphery analyzes the graph to identify unused code. This phase consists of a number of steps that mutate the graph to make it easier to identify specific scenarios of unused code. The final step walks the graph from its roots to identify declarations that are no longer referenced.
The goal of Periphery is to report instances of unused declarations. A declaration is a class
, struct
, protocol
, function
, property
, constructor
, enum
, typealias
, associatedtype
, etc. As you'd expect, Periphery is able to identify simple unreferenced declarations, e.g a class
that is no longer used anywhere in your codebase.
Periphery can also identify more advanced instances of unused code. The following section explains these in detail.
Periphery can identify unused function parameters. Instances of unused parameters can also be identified in protocols and their conforming declarations, as well as parameters in overridden methods. Both of these scenarios are explained further below.
An unused parameter of a protocol function will only be reported as unused if the parameter is also unused in all implementations.
protocol Greeter {
func greet(name: String)
func farewell(name: String) // 'name' is unused
}
class InformalGreeter: Greeter {
func greet(name: String) {
print("Sup " + name + ".")
}
func farewell(name: String) { // 'name' is unused
print("Cya.")
}
}
Tip
You can ignore all unused parameters from protocols and conforming functions with the
--retain-unused-protocol-func-params
option.
Similar to protocols, parameters of overridden functions are only reported as unused if they're also unused in the base function and all overriding functions.
class BaseGreeter {
func greet(name: String) {
print("Hello.")
}
func farewell(name: String) { // 'name' is unused
print("Goodbye.")
}
}
class InformalGreeter: BaseGreeter {
override func greet(name: String) {
print("Sup " + name + ".")
}
override func farewell(name: String) { // 'name' is unused
print("Cya.")
}
}
Unused parameters of protocols or classes defined in foreign modules (e.g Foundation) are always ignored, since you do not have access to modify the base function declaration.
Unused parameters of functions that simply call fatalError
are also ignored. Such functions are often unimplemented required initializers in subclasses.
class Base {
let param: String
required init(param: String) {
self.param = param
}
}
class Subclass: Base {
init(custom: String) {
super.init(param: custom)
}
required init(param: String) {
fatalError("init(param:) has not been implemented")
}
}
A protocol which is conformed to by an object is not truly used unless it's also used as an existential type, or to specialize a generic method/class. Periphery is able to identify such redundant protocols whether they are conformed to by one, or even multiple objects.
protocol MyProtocol { // 'MyProtocol' is redundant
func someMethod()
}
class MyClass1: MyProtocol { // 'MyProtocol' conformance is redundant
func someMethod() {
print("Hello from MyClass1!")
}
}
class MyClass2: MyProtocol { // 'MyProtocol' conformance is redundant
func someMethod() {
print("Hello from MyClass2!")
}
}
let myClass1 = MyClass1()
myClass1.someMethod()
let myClass2 = MyClass2()
myClass2.someMethod()
Here we can see that despite both implementations of someMethod
are called, at no point does an object take on the type of MyProtocol
. Therefore the protocol itself is redundant, and there's no benefit from MyClass1
or MyClass2
conforming to it. We can remove MyProtocol
along with each redundant conformance, and just keep someMethod
in each class.
Just like a normal method or property of a object, individual properties and methods declared by your protocol can also be identified as unused.
protocol MyProtocol {
var usedProperty: String { get }
var unusedProperty: String { get } // 'unusedProperty' is unused
}
class MyConformingClass: MyProtocol {
var usedProperty: String = "used"
var unusedProperty: String = "unused" // 'unusedProperty' is unused
}
class MyClass {
let conformingClass: MyProtocol
init() {
conformingClass = MyConformingClass()
}
func perform() {
print(conformingClass.usedProperty)
}
}
let myClass = MyClass()
myClass.perform()
Here we can see that MyProtocol
is itself used, and cannot be removed. However, since unusedProperty
is never called on MyConformingClass
, Periphery is able to identify that the declaration of unusedProperty
in MyProtocol
is also unused and can be removed along with the unused implementation of unusedProperty
.
Along with being able to identify unused enumerations, Periphery can also identify individual unused enum cases. Plain enums that are not raw representable, i.e that don't have a String
, Character
, Int
or floating-point value type can be reliably identified. However, enumerations that do have a raw value type can be dynamic in nature, and therefore must be assumed to be used.
Let's clear this up with a quick example:
enum MyEnum: String {
case myCase
}
func someFunction(value: String) {
if let myEnum = MyEnum(rawValue: value) {
somethingImportant(myEnum)
}
}
There's no direct reference to the myCase
case, so it's reasonable to expect it might no longer be needed, however if it were removed we can see that somethingImportant
would never be called if someFunction
were passed the value of "myCase"
.
Properties that are assigned but never used are identified as such, e.g:
class MyClass {
var assignOnlyProperty: String // 'assignOnlyProperty' is assigned, but never used
init(value: String) {
self.assignOnlyProperty = value
}
}
In some cases this may be the intended behavior, therefore you have a few options available to silence such results:
--retain-assign-only-property-types
. Given types must match their exact usage in the property declaration (sans optional question mark), e.g String
, [String]
, Set
. Periphery is unable to resolve inferred property types, therefore in some instances you may need to add explicit type annotations to your properties.--retain-assign-only-properties
.Declarations that are marked public
yet are not referenced from outside their home module, are identified as having redundant public accessibility. In this scenario, the public
annotation can be removed from the declaration. Removing redundant public accessibility has a couple of benefits:
final
by automatically discovering all potentially overriding declarations. final
classes are better optimized by the compiler.This analysis can be disabled with --disable-redundant-public-analysis
.
Periphery can detect unused imports of targets it has scanned, i.e. those specified with the --targets
argument. It cannot detect unused imports of other targets because the Swift source files are unavailable and uses of @_exported
cannot be observed. @_exported
is problematic because it changes the public interface of a target such that the declarations exported by the target are no longer necessarily declared by the imported target. For example, the Foundation
target exports Dispatch
, among other targets. If any given source file imports Foundation
and references DispatchQueue
but no other declarations from Foundation
, then the Foundation
import cannot be removed as it would also make the DispatchQueue
type unavailable. To avoid false positives, therefore, Periphery only detects unused imports of targets it has scanned.
Periphery will likely produce false positives for targets with mixed Swift and Objective-C, as Periphery cannot scan the Objective-C files. It is recommended therefore to disable unused import detection for projects with a significant amount of Objective-C, or manually exclude the mixed language targets from the results.
Periphery cannot analyze Objective-C code since types may be dynamically typed.
By default Periphery does not assume that declarations accessible by the Objective-C runtime are in use. If your project is a mix of Swift & Objective-C, you can enable this behavior with the --retain-objc-accessible
option. Swift declarations that are accessible by the Objective-C runtime are those that are explicitly annotated with @objc
or @objcMembers
, and classes that inherit NSObject
either directly or indirectly via another class.
Alternatively, the --retain-objc-annotated
can be used to only retain declarations that are explicitly annotated with @objc
or @objcMembers
. Types that inherit NSObject
are not retained unless they have the explicit annotations. This option may uncover more unused code, but with the caveat that some of the results may be incorrect if the declaration is in fact used in Objective-C code. To resolve these incorrect results you must add an @objc
annotation to the declaration.
Swift synthesizes additional code for Codable
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 Codable
types with --retain-codable-properties
. Alternatively, you can retain properties only on Encodable
types with --retain-encodable-properties
.
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"
.
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.
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). 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.
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.
An ignore comment command can be placed directly on the line above any declaration to ignore it, and all descendent declarations:
// periphery:ignore
class MyClass {}
You can also ignore specific unused function parameters:
// periphery:ignore:parameters unusedOne,unusedTwo
func someFunc(used: String, unusedOne: String, unusedTwo: String) {
print(used)
}
The // periphery:ignore:all
command can be placed at the top of the source file to ignore the entire contents of the file. Note that the comment must be placed above any code, including import statements.
Comment commands also support trailing comments following a hyphen so that you can include an explanation on the same line:
// periphery:ignore - explanation of why this is necessary
class MyClass {}
Before setting up Xcode integration, we highly recommend you first get Periphery working in a terminal, as you will be using the exact same command via Xcode.
Select your project in the Project Navigator and click the + button at the bottom left of the Targets section. Select Cross-platform and choose Aggregate. Hit Next.
Periphery is passion project that takes a huge amount of effort to maintain and develop. If you find Periphery useful, please consider sponsoring through GitHub Sponsors.
Special thanks goes to the following generous sponsors:
SaGa Corp develops unique technology for financial players and their customers.
Emerge Tools is a suite of revolutionary products designed to supercharge mobile apps and the teams that build them.