现在我成了 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 是一套革命性的产品,旨在增强移动应用程序和构建它们的团队。