现在我成了 Delete,代码的毁灭者。
brew install periphery
mint install peripheryapp/periphery
bazel_dep(name = "periphery", version = "<version>", dev_dependency = True)
use_repo(use_extension("@periphery//bazel:generated.bzl", "generated"), "periphery_generated")
请参阅下面的 Bazel 以获取使用说明。
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 的目标是报告未使用的声明的实例。 声明是一个 class、struct、protocol、function、property、constructor、enum、typealias、associatedtype 等。 正如您所期望的那样,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 的函数的未使用参数。 这样的函数通常是子类中未实现的必需初始化程序。
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 的类型。 因此,协议本身是冗余的,并且 MyClass1 或 MyClass2 符合它没有任何好处。 我们可以删除 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 可以识别 MyProtocol 中 unusedProperty 的声明也未使用,并且可以与 unusedProperty 的未使用实现一起删除。
除了能够识别未使用的枚举之外,Periphery 还可以识别各个未使用的枚举案例。 可以可靠地识别非原始可表示的普通枚举,即 不 具有 String、Character、Int 或浮点值类型的枚举。 但是,确实 具有原始值类型的枚举可能是动态的,因此必须假定为已使用。
让我们用一个简单的例子来澄清这一点
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
}
}
在某些情况下,这可能是预期行为,因此您有几个选项可以使此类结果静音
--retain-assign-only-property-types 按类型保留所有仅赋值属性。 给定类型必须与其在属性声明中的确切用法匹配(sans optional question mark),例如 String、[String]、Set<String>。 Periphery 无法解析推断的属性类型,因此在某些情况下,您可能需要向属性添加显式类型注释。--retain-assign-only-properties 完全禁用仅赋值属性分析。标记为 public 但未从其主模块外部引用的声明,被标识为具有冗余的 public 访问权限。 在这种情况下,可以从声明中删除 public 注释。 删除冗余的 public 访问权限有几个好处
final。 final 类可以被编译器更好地优化。可以使用 --disable-redundant-public-analysis 禁用此分析。
Periphery 可以检测它扫描的目标的未使用的导入,即使用 --targets 参数指定的目标。 它无法检测其他目标的未使用导入,因为 Swift 源文件不可用,并且无法观察到 @_exported 的使用。 @_exported 是有问题的,因为它更改了目标的 public 接口,因此目标导出的声明不再一定是导入目标声明的声明。 例如,Foundation 目标导出 Dispatch,以及其他目标。 如果任何给定的源文件导入 Foundation 并引用 DispatchQueue,但不引用 Foundation 的任何其他声明,则不能删除 Foundation 导入,因为它也会使 DispatchQueue 类型不可用。 因此,为了避免误报,Periphery 只检测它扫描的目标的未使用导入。
Periphery 可能会为混合使用 Swift 和 Objective-C 的目标产生误报,因为 Periphery 无法扫描 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 注释。
Swift 为 Codable 类型合成了额外的代码,这些代码对 Periphery 不可见,并且可能导致未从非合成代码直接引用的属性出现误报。 如果您的项目包含许多此类类型,您可以使用 --retain-codable-properties 保留 Codable 类型的所有属性。 或者,您可以使用 --retain-encodable-properties 仅保留 Encodable 类型的属性。
如果 Codable 一致性由 Periphery 未扫描的外部模块中的协议声明,您可以指示 Periphery 使用 --external-codable-protocols "ExternalProtocol" 将协议识别为 Codable。
继承 XCTestCase 的任何类都会自动保留及其测试方法。 但是,当一个类通过另一个类(例如 UnitTestCase)间接继承 XCTestCase,并且该类位于 Periphery 未扫描的目标中时,您需要使用 --external-test-case-classes UnitTestCase 选项来指示 Periphery 将 UnitTestCase 视为 XCTestCase 子类。
如果您的项目包含 Interface Builder 文件(如情节提要和 XIB),Periphery 在识别未使用的声明时会考虑到这些文件。 但是,Periphery 目前只识别未使用的类。 存在此限制是因为 Periphery 尚未完全解析 Interface Builder 文件(请参阅 issue #212)。 由于 Periphery 避免误报的设计原则,假设如果一个类在 Interface Builder 文件中被引用,则它的所有 IBOutlets 和 IBActions 都会被使用,即使它们实际上可能不是。 一旦 Periphery 获得解析 Interface Builder 文件的能力,将修改此方法以准确识别未使用的 IBActions 和 IBOutlets。
出于某些原因,您可能希望保留一些未使用的代码。可以使用源代码注释命令来忽略特定声明,并将其从结果中排除。
忽略注释命令可以直接放置在任何声明的上一行,以忽略该声明及其所有子声明
// 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 集成之前,我们强烈建议您首先在终端中运行 Periphery,因为您将通过 Xcode 使用相同的命令。
在项目导航器中选择您的项目,然后单击“Targets”部分左下角的 + 按钮。选择 Cross-platform,然后选择 Aggregate。单击“Next”。
为新目标选择一个名称,例如“Periphery”或“Unused Code”。
在 Build Phases 部分,单击 + 按钮以添加新的“Run Script”阶段。
在 shell 脚本窗口中,输入 Periphery 命令。请务必包含 --format xcode 选项。
您已准备就绪。您现在应该在下拉菜单中看到新的 scheme。选择它并单击运行。
提示
如果您希望团队中的其他人能够使用该 scheme,则需要将其标记为Shared。这可以通过选择Manage Schemes...并选中新 scheme 旁边的Shared复选框来完成。现在可以将 scheme 定义检入到源代码控制中。
下面描述的两个排除选项都接受 Bash v4 样式的路径 glob,可以是绝对路径,也可以是相对于您的项目目录的路径。您可以使用空格分隔多个 glob,例如 --option "Sources/Single.swift" "**/Generated/*.swift" "**/*.{xib,storyboard}"。
要从某些文件中排除结果,请将 --report-exclude <globs> 选项传递给 scan 命令。
从索引阶段排除文件意味着 Periphery 将看不到文件中包含的任何声明和引用。Periphery 的行为就好像这些文件不存在一样。
要排除要索引的文件,有以下几个选项
--exclude-targets "TargetA" "TargetB" 排除所选目标中的所有源文件。--exclude-tests 排除所有测试目标。--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”。
由 xcodebuild 生成的索引存储位于 DerivedData 中,其位置取决于您的项目,例如 ~/Library/Developer/Xcode/DerivedData/YourProject-abc123/Index/DataStore。对于 Xcode 14 及更高版本,Index 目录可以作为 Index.noindex 找到,这会禁止 Spotlight 索引。
默认情况下,Periphery 在 .build/debug/index/store 中查找索引存储。因此,如果您打算在调用 swift test 后立即运行 Periphery,则可以省略 --index-store-path 选项,Periphery 将使用在构建项目以进行测试时创建的索引存储。但是,如果情况并非如此,则必须使用 --index-store-path 为 Periphery 提供索引存储的位置。
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
}
}
您有以下几种解决方法
releaseName。-- 之后指定参数将参数传递给底层构建,例如 periphery scan ... -- -configuration release。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/'
由于 Swift 中的一些底层错误,Periphery 在某些情况下可能会报告不正确的结果。
| ID | 标题 |
|---|---|
| 56541 | 索引存储不关联用作下标键的静态属性 getter |
| 56327 | 索引存储不关联在子类中实现的 objc 可选协议方法 |
| 56189 | 索引存储应关联来自字符串文字的 appendInterpolation |
| 56165 | 索引存储不关联通过文字符号的构造函数 |
Periphery 是一个充满激情的项目,需要大量的努力来维护和开发。如果您觉得 Periphery 有用,请考虑通过 GitHub Sponsors 进行赞助。
特别感谢以下慷慨的赞助商
SaGa Corp 为金融参与者及其客户开发独特的技术。
Emerge Tools 是一套革命性的产品,旨在增强移动应用程序和构建它们的团队。