Periphery
Periphery

一个用于识别 Swift 项目中未使用的代码的工具。

现在我成了 Delete,代码的毁灭者。



目录

安装

Homebrew

brew install periphery

Mint

mint install peripheryapp/periphery

Bazel

bazel_dep(name = "periphery", version = "<version>", dev_dependency = True)
use_repo(use_extension("@periphery//bazel:generated.bzl", "generated"), "periphery_generated")

请参阅下面的 Bazel 以获取使用说明。

如何使用

scan 命令

scan 命令是 Periphery 的主要功能。 要开始引导式设置,请更改到您的项目目录并运行

periphery scan --setup

引导式设置将检测您的项目类型并配置一些选项。 在回答几个问题后,Periphery 将打印出完整的扫描命令并执行它。

引导式设置仅用于介绍目的,一旦您熟悉 Periphery,您可以尝试一些更高级的选项,所有这些都可以通过 periphery help scan 查看。

为了最大限度地利用 Periphery,重要的是要了解它的工作原理。 Periphery 首先构建您的项目;这样做是为了生成“索引存储”。 索引存储包含有关项目中声明(类、结构体、函数等)及其对其他声明的引用的详细信息。 使用此存储,Periphery 构建项目关系结构的内存图,并通过解析每个源文件来补充额外的信息。 接下来,对图进行修改,使其更适合检测未使用的代码,例如,标记项目的入口点。 最后,从其根遍历图以识别未引用的声明。

提示

索引存储仅包含构建阶段编译的构建目标中源文件的信息。 如果给定的类仅在未编译的源文件中引用,则 Periphery 会将该类标识为未使用。 务必确保构建所有您期望包含引用的目标。 对于 Xcode 项目,这可以使用 -—schemes 选项进行控制。 对于 Swift 包,将自动构建所有目标。

如果您的项目由一个或多个独立的框架组成,这些框架不包含消耗其接口的某种应用程序,则您需要告诉 Periphery 假设所有 public 声明都已使用,使用 --retain-public 选项。

对于混合使用 Objective-C 和 Swift 的项目,强烈建议您阅读有关这可能对您的结果产生的影响

配置

一旦您确定了适合您项目的选项,您可能希望将它们保存在 YAML 配置文件中。 实现此目的的最简单方法是使用 --verbose 选项运行 Periphery。 在输出的开头附近,您将看到 [configuration:begin] 部分,其中您的配置以 YAML 格式显示在下方。 将配置复制并粘贴到项目文件夹根目录中的 .periphery.yml 中。 您现在只需运行 periphery scan 即可,并且将使用 YAML 配置。

分析

Periphery 的目标是报告未使用的声明的实例。 声明是一个 classstructprotocolfunctionpropertyconstructorenumtypealiasassociatedtype 等。 正如您所期望的那样,Periphery 可以识别简单的未引用声明,例如,不再在您的代码库中任何地方使用的 class

Periphery 还可以识别更高级的未使用代码实例。 以下部分详细解释了这些实例。

函数参数

Periphery 可以识别未使用的函数参数。 未使用参数的实例也可以在协议及其符合声明中识别,以及重写方法中的参数。 这两种情况将在下面进一步解释。

协议

只有当协议函数的未使用参数也在所有实现中未使用时,才会被报告为未使用。

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

提示

您可以使用 --retain-unused-protocol-func-params 选项忽略协议和符合函数中的所有未使用参数。

重写

与协议类似,只有当重写函数的参数也在基本函数和所有重写函数中未使用时,才会被报告为未使用。

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

外部协议和类

始终忽略在外部模块(例如 Foundation)中定义的协议或类的未使用参数,因为您无法访问修改基本函数声明。

fatalError 函数

还会忽略仅调用 fatalError 的函数的未使用参数。 这样的函数通常是子类中未实现的必需初始化程序。

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

协议

除非协议也用作存在类型或用于专门化泛型方法/类,否则对象符合的协议并非真正使用。 Periphery 能够识别此类冗余协议,无论它们是否被一个甚至多个对象符合。

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

在这里我们可以看到,尽管调用了 someMethod 的两个实现,但在任何时候对象都不会采用 MyProtocol 的类型。 因此,协议本身是冗余的,并且 MyClass1MyClass2 符合它没有任何好处。 我们可以删除 MyProtocol 以及每个冗余的一致性,只需保留每个类中的 someMethod 即可。

就像对象的普通方法或属性一样,您的协议声明的各个属性和方法也可以被识别为未使用。

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

在这里我们可以看到 MyProtocol 本身已被使用,无法删除。 但是,由于 unusedProperty 从未在 MyConformingClass 上调用,因此 Periphery 可以识别 MyProtocolunusedProperty 的声明也未使用,并且可以与 unusedProperty 的未使用实现一起删除。

枚举

除了能够识别未使用的枚举之外,Periphery 还可以识别各个未使用的枚举案例。 可以可靠地识别非原始可表示的普通枚举,即 具有 StringCharacterInt 或浮点值类型的枚举。 但是,确实 具有原始值类型的枚举可能是动态的,因此必须假定为已使用。

让我们用一个简单的例子来澄清这一点

enum MyEnum: String {
    case myCase
}

func someFunction(value: String) {
    if let myEnum = MyEnum(rawValue: value) {
        somethingImportant(myEnum)
    }
}

没有对 myCase 案例的直接引用,因此可以合理地期望它可能不再需要,但是,如果将其删除,我们可以看到如果将 "myCase" 的值传递给 someFunction,则永远不会调用 somethingImportant

仅赋值属性

已分配但从未使用过的属性被标识为此类,例如

class MyClass {
    var assignOnlyProperty: String // 'assignOnlyProperty' is assigned, but never used

    init(value: String) {
        self.assignOnlyProperty = value
    }
}

在某些情况下,这可能是预期行为,因此您有几个选项可以使此类结果静音

冗余的 Public 访问权限

标记为 public 但未从其主模块外部引用的声明,被标识为具有冗余的 public 访问权限。 在这种情况下,可以从声明中删除 public 注释。 删除冗余的 public 访问权限有几个好处

可以使用 --disable-redundant-public-analysis 禁用此分析。

未使用的 Imports

Periphery 可以检测它扫描的目标的未使用的导入,即使用 --targets 参数指定的目标。 它无法检测其他目标的未使用导入,因为 Swift 源文件不可用,并且无法观察到 @_exported 的使用。 @_exported 是有问题的,因为它更改了目标的 public 接口,因此目标导出的声明不再一定是导入目标声明的声明。 例如,Foundation 目标导出 Dispatch,以及其他目标。 如果任何给定的源文件导入 Foundation 并引用 DispatchQueue,但不引用 Foundation 的任何其他声明,则不能删除 Foundation 导入,因为它也会使 DispatchQueue 类型不可用。 因此,为了避免误报,Periphery 只检测它扫描的目标的未使用导入。

Periphery 可能会为混合使用 Swift 和 Objective-C 的目标产生误报,因为 Periphery 无法扫描 Objective-C 文件。 因此,建议禁用包含大量 Objective-C 的项目的未使用导入检测,或者手动从结果中排除混合语言目标。

Objective-C

Periphery 无法分析 Objective-C 代码,因为类型可能是动态类型的。

默认情况下,Periphery 不会假设 Objective-C 运行时可访问的声明正在使用中。 如果您的项目是 Swift 和 Objective-C 的混合,您可以使用 --retain-objc-accessible 选项启用此行为。 可由 Objective-C 运行时访问的 Swift 声明是那些使用 @objc@objcMembers 显式注释的声明,以及直接或间接通过另一个类继承 NSObject 的类。

或者,可以使用 --retain-objc-annotated 仅保留使用 @objc@objcMembers 显式注释的声明。 除非它们有显式注释,否则不保留继承 NSObject 的类型。 此选项可能会发现更多未使用的代码,但需要注意的是,如果声明在 Objective-C 代码中使用,则某些结果可能不正确。 要解决这些不正确的结果,您必须向声明添加 @objc 注释。

Codable

Swift 为 Codable 类型合成了额外的代码,这些代码对 Periphery 不可见,并且可能导致未从非合成代码直接引用的属性出现误报。 如果您的项目包含许多此类类型,您可以使用 --retain-codable-properties 保留 Codable 类型的所有属性。 或者,您可以使用 --retain-encodable-properties 仅保留 Encodable 类型的属性。

如果 Codable 一致性由 Periphery 未扫描的外部模块中的协议声明,您可以指示 Periphery 使用 --external-codable-protocols "ExternalProtocol" 将协议识别为 Codable

XCTestCase

继承 XCTestCase 的任何类都会自动保留及其测试方法。 但是,当一个类通过另一个类(例如 UnitTestCase)间接继承 XCTestCase,并且该类位于 Periphery 未扫描的目标中时,您需要使用 --external-test-case-classes UnitTestCase 选项来指示 Periphery 将 UnitTestCase 视为 XCTestCase 子类。

Interface Builder

如果您的项目包含 Interface Builder 文件(如情节提要和 XIB),Periphery 在识别未使用的声明时会考虑到这些文件。 但是,Periphery 目前只识别未使用的类。 存在此限制是因为 Periphery 尚未完全解析 Interface Builder 文件(请参阅 issue #212)。 由于 Periphery 避免误报的设计原则,假设如果一个类在 Interface Builder 文件中被引用,则它的所有 IBOutletsIBActions 都会被使用,即使它们实际上可能不是。 一旦 Periphery 获得解析 Interface Builder 文件的能力,将修改此方法以准确识别未使用的 IBActionsIBOutlets

注释命令

出于某些原因,您可能希望保留一些未使用的代码。可以使用源代码注释命令来忽略特定声明,并将其从结果中排除。

忽略注释命令可以直接放置在任何声明的上一行,以忽略该声明及其所有子声明

// periphery:ignore
class MyClass {}

您还可以忽略特定的未使用函数参数

// periphery:ignore:parameters unusedOne,unusedTwo
func someFunc(used: String, unusedOne: String, unusedTwo: String) {
    print(used)
}

// periphery:ignore:all 命令可以放置在源文件的顶部,以忽略文件的全部内容。请注意,该注释必须放置在任何代码(包括 import 语句)之上。

注释命令还支持在连字符后添加尾随注释,以便您可以在同一行中包含说明

// periphery:ignore - explanation of why this is necessary
class MyClass {}

Xcode 集成

在设置 Xcode 集成之前,我们强烈建议您首先在终端中运行 Periphery,因为您将通过 Xcode 使用相同的命令。

步骤 1:创建聚合目标

在项目导航器中选择您的项目,然后单击“Targets”部分左下角的 + 按钮。选择 Cross-platform,然后选择 Aggregate。单击“Next”。

Step 1

为新目标选择一个名称,例如“Periphery”或“Unused Code”。

Step 2

步骤 2:添加运行脚本构建阶段

Build Phases 部分,单击 + 按钮以添加新的“Run Script”阶段。

Step 3

在 shell 脚本窗口中,输入 Periphery 命令。请务必包含 --format xcode 选项。

Step 4

步骤 3:选择并运行

您已准备就绪。您现在应该在下拉菜单中看到新的 scheme。选择它并单击运行。

提示

如果您希望团队中的其他人能够使用该 scheme,则需要将其标记为Shared。这可以通过选择Manage Schemes...并选中新 scheme 旁边的Shared复选框来完成。现在可以将 scheme 定义检入到源代码控制中。

Step 5

排除文件

下面描述的两个排除选项都接受 Bash v4 样式的路径 glob,可以是绝对路径,也可以是相对于您的项目目录的路径。您可以使用空格分隔多个 glob,例如 --option "Sources/Single.swift" "**/Generated/*.swift" "**/*.{xib,storyboard}"

排除结果

要从某些文件中排除结果,请将 --report-exclude <globs> 选项传递给 scan 命令。

排除索引文件

从索引阶段排除文件意味着 Periphery 将看不到文件中包含的任何声明和引用。Periphery 的行为就好像这些文件不存在一样。

要排除要索引的文件,有以下几个选项

  1. 使用 --exclude-targets "TargetA" "TargetB" 排除所选目标中的所有源文件。
  2. 使用 --exclude-tests 排除所有测试目标。
  3. 使用 --index-exclude "file.swift" "path/*.swift" 排除单个源文件。

保留文件声明

要保留文件中所有声明,请将 --retain-files <globs> 选项传递给 scan 命令。此选项等效于在每个文件的顶部添加 // periphery:ignore:all 注释命令。

持续集成

当将 Periphery 集成到 CI 管道中时,如果您的管道已经完成了构建阶段(例如,要运行测试),您可以选择跳过构建阶段。可以使用 --skip-build 选项来实现。但是,您还需要使用 --index-store-path 告诉 Periphery 索引存储的位置。此位置取决于您的项目类型。

请注意,当使用 --skip-build--index-store-path 时,至关重要的是索引存储包含您通过 --targets 指定的所有目标的数据。例如,如果您的管道之前构建了目标“App”和“Lib”,则索引存储将仅包含这些目标中文件的数据。然后,您不能指示 Periphery 扫描其他目标,例如“Extension”或“UnitTests”。

Xcode

xcodebuild 生成的索引存储位于 DerivedData 中,其位置取决于您的项目,例如 ~/Library/Developer/Xcode/DerivedData/YourProject-abc123/Index/DataStore。对于 Xcode 14 及更高版本,Index 目录可以作为 Index.noindex 找到,这会禁止 Spotlight 索引。

SwiftPM

默认情况下,Periphery 在 .build/debug/index/store 中查找索引存储。因此,如果您打算在调用 swift test 后立即运行 Periphery,则可以省略 --index-store-path 选项,Periphery 将使用在构建项目以进行测试时创建的索引存储。但是,如果情况并非如此,则必须使用 --index-store-path 为 Periphery 提供索引存储的位置。

构建系统

Bazel

bazel run @periphery -- scan --bazel

--bazel 选项启用 Bazel 模式,该模式可与您的项目无缝集成。它的工作原理是查询您的项目以识别所有顶级目标,生成 scan 规则的隐藏实现,然后调用 bazel run。您可以使用 -—bazel-filter <value> 选项过滤顶级目标,其中 <value> 将作为 Bazel 的 filter 运算符的第一个参数传递。可以使用 -—verbose 选项在控制台中查看生成的查询。

其他

Periphery 可以分析使用其他构建系统的项目,但它不能像 SPM、Xcode 和 Bazel 那样自动驱动它们。相反,您需要创建一个配置文件,该文件指定 indexstore 和其他资源文件的位置。格式如下

{
    "indexstores": [
        "path/to/file.indexstore"
    ],
    "test_targets": [
        "MyTests"
    ],
    "plists": [
        "path/to/file.plist"
    ],
    "xibs": [
        "path/to/file.xib",
        "path/to/file.storyboard"
    ],
    "xcdatamodels": [
        "path/to/file.xcdatamodel"
    ],
    "xcmappingmodels": [
        "path/to/file.xcmappingmodel"
    ]
}

提示

相对路径被假定为相对于当前目录。

然后,您可以按如下方式调用 Periphery

periphery scan --generic-project-config config.json

提示

两个选项都支持多个路径。

平台

Periphery 支持 macOS 和 Linux。macOS 同时支持 Xcode 和 Swift Package Manager (SPM) 项目,而 Linux 上仅支持 SPM 项目。

问题排查

一个或多个文件中的错误结果,例如误报和不正确的源文件位置

索引存储可能会损坏,或与源文件失去同步。例如,如果您强制终止 (^C) 扫描,则可能会发生这种情况。要纠正此问题,您可以将 --clean-build 标志传递给 scan 命令,以强制删除现有的构建产物。

预处理器宏条件分支中引用的代码未使用

当 Periphery 构建您的项目时,它使用默认的构建配置,通常是“debug”。如果您使用预处理器宏来有条件地编译代码,Periphery 将只能看到已编译的分支。在下面的示例中,releaseName 将报告为未使用,因为它仅在宏的非调试分支中引用。

struct BuildInfo {
    let debugName = "debug"
    let releaseName = "release" // 'releaseName' is unused

    var name: String {
        #if DEBUG
        debugName
        #else
        releaseName
        #endif
    }
}

您有以下几种解决方法

Swift 包是特定于平台的

Periphery 使用 swift build 来编译 Swift 包,如果 Swift 包是特定于平台的(例如,iOS),则编译将失败。

作为一种解决方法,您可以使用 xcodebuild 手动构建 Swift 包,然后使用 --skip-build--index-store-path 选项来定位之前由 xcodebuild 生成的索引存储。

示例

# 1. Use xcodebuild
xcodebuild -scheme MyScheme -destination 'platform=iOS Simulator,OS=16.2,name=iPhone 14' -derivedDataPath '../dd' clean build

# 2. Use produced index store for scanning
periphery scan --skip-build --index-store-path '../dd/Index.noindex/DataStore/'

已知 Bug

由于 Swift 中的一些底层错误,Periphery 在某些情况下可能会报告不正确的结果。

ID 标题
56541 索引存储不关联用作下标键的静态属性 getter
56327 索引存储不关联在子类中实现的 objc 可选协议方法
56189 索引存储应关联来自字符串文字的 appendInterpolation
56165 索引存储不关联通过文字符号的构造函数

赞助商 赞助商

Periphery 是一个充满激情的项目,需要大量的努力来维护和开发。如果您觉得 Periphery 有用,请考虑通过 GitHub Sponsors 进行赞助。

特别感谢以下慷慨的赞助商

SaGa Corp

SaGa Corp 为金融参与者及其客户开发独特的技术。

Emerge Tools

Emerge Tools 是一套革命性的产品,旨在增强移动应用程序和构建它们的团队。