Analysis

The goal of Periphery is to report instances of unused declarations. A declaration is a class, struct, protocol, function, property, constructor, enum, typealias or associatedtype. As you'd expect, Periphery is able to identify simple unreferenced declarations, i.e a class that is no longer used anywhere in your codebase.

This document aims to explain in detail the more advanced analysis techniques that Periphery employs.

Function Parameters

Periphery provides two commands for identifying unused function parameters. The scan-syntax command is the fastest, yet only analyses functions by parsing syntax. This means some results - while still technically correct - may not be practicaly useful.

The scan command also identifies unused function parameters, but uses the context of your whole application in order to silence results which are not practicaly useful. The sections below describe the scenarios in which the scan command works to provide more useful results.

Protocols

An unused parameter of a protocol function will only be reported as unused if the parameter is also unused in all implementations.

 
 
......................
'name' is unused
 
 
 
 
 
 
 
......................
'name' is unused
protocol Greeter { func greet(name: String) func farewell(name: String) } class InformalGreeter: Greeter { func greet(name: String) { print("Sup " + name + ".") } func farewell(name: String) { print("Cya.") } }
Tip
You can ignore all unused parameters from protocols and conforming functions with the --retain-unused-protocol-func-params option.

Overrides

Similar to protocols, parameters of overriden functions are only reported as unused if they're also unused in the base function and all overriding functions.

 
 
 
 
 
......................
'name' is unused
 
 
 
 
 
 
 
 
 
...............................
'name' is unused
class BaseGreeter { func greet(name: String) { print("Hello.") } func farewell(name: String) { print("Goodbye.") } } class InformalGreeter: BaseGreeter { override func greet(name: String) { print("Sup " + name + ".") } override func farewell(name: String) { print("Cya.") } }

Foreign Protocols & Classes

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.

fatalError Functions

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")
    }
}

Protocols

A protocol which is conformed to by an object is not truly used unless it's also used in a type cast, as a property type, or to specialize a generic method/class, etc. Periphery is able to identify such protocols whether they are conformed to by one, or even multiple objects.

...................
'MyProtocol' is unused
protocol MyProtocol { func someMethod() } class MyClass1: MyProtocol { func someMethod() { print("Hello from MyClass1!") } } class MyClass2: MyProtocol { 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 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.

 
 
......................
'unusedProperty' is unused
 
 
 
 
......................
'unusedProperty' is unused
protocol MyProtocol { var usedProperty: String { get } var unusedProperty: String { get } } class MyConformingClass: MyProtocol { var usedProperty: String = "used" var unusedProperty: String = "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 thus also unused and can be removed along with the unused implementation of unusedProperty.

Assign-only Properties

Properties which are assigned to but never read from can be identified as unused by Periphery. However, since an unread property may be a valid use-case, e.g to purposefully retain the object, this identification is only enabled with Aggressive Analysis.

 
 
 
 
 
............................
'myDependency' is unused
class MyClass { static func make() -> Self { return self.init(myDependency: inject()) } private let myDependency: MyDependency init(myDependency: MyDependency) { self.myDependency = myDependency } func someMethod() { } }

Note that this analysis only applies to simple properties, i.e properties do not define a custom getter or setter.

Removal of unused dependencies can reduce redundant incremental recompilation. The Swift compiler keeps track of every type that a source file exports, and uses (in .swiftdeps files). When a source file changes, any files that depend upon the changed types must also be recompiled. In this example, if MyDependency is declared in another file, and that file is changed, then the file containing MyClass will be needlessly recompiled.

As with any aggressive analysis technique, you should consider that the property might be needed solely to retain the instance. If the unread property is in fact needed, then this is a friendly reminder that you should add a comment explaining why.

Enumerations

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 thus their identification is restricted to Aggressive Analysis only.

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)
  }
}

Since MyEnum has a raw value type of String, myCase is only identified as unused when using aggressive analysis. There's no direct reference to the 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". Therefore more scrutiny is advised when using aggressive analysis and reviewing unused enum cases.

Objective-C

Since Objective-C can use dynamic types, Periphery cannot reason about it from a static standpoint. Therefore, by default, Periphery will assume that any declaration exposed to Objective-C is in use. If your project is 100% Swift, then you can disable this behavior with the --no-retain-objc-annotated option. For those using Periphery on a mixed project, there are some important implications to be aware of.

As you already know, any declaration that is annotated with @objc or @objcMembers is exposed to the Objective-C runtime, and Periphery will assume they are in use. However, you should also be aware that any class that inherits from NSObject is also implicitly exposed to Objective-C. If you ever come across a situation where Periphery reports that all methods and properties within a class - but not the class itself - are unused, then the class likely inherits from NSObject. It may be worth your time doing a cursory run of Periphery with --no-retain-objc-annotated, you may find a few extra declarations to remove. Though be warned, many declarations reported as unused may still be in use by Objective-C code, so you'll need to take extra care when reviewing them.

Global Equatable Operators

Periphery is currently unable to identify if an Equatable infix operator is in use if it is defined at global scope. For example:

class MyClass: Equatable {}

func == (lhs: MyClass, rhs: MyClass) -> Bool {
    return true
}

Therefore, by default, Periphery will assume all global Equatable infix operators are in use. However, when operating with Aggressive Analysis, such operators will be reported as unused. Clearly, false negative results are unwanted, so you can resolve this by moving the operator within the class, or into an extension.

class MyClass {}

extension MyClass: Equatable {
    static func == (lhs: MyClass, rhs: MyClass) -> Bool {
        return true
    }
}