Project Icon

periphery

Swift项目未使用代码识别工具

Periphery是一款识别Swift项目中未使用代码的开源工具。它能分析项目结构,找出未引用的声明、未使用的函数参数和冗余协议等。支持Xcode和Swift Package Manager项目,可通过Homebrew或Mint安装。Periphery提供命令行界面和Xcode集成,帮助开发者清理代码。其分析范围涵盖函数参数、协议、枚举和属性等多个方面,适用于Swift项目代码优化。

Periphery
Periphery

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

现在我成为了删除者,代码的毁灭者。



目录

安装

Homebrew

brew install peripheryapp/periphery/periphery

Mint

mint install peripheryapp/periphery

使用方法

scan 命令

scan 命令是 Periphery 的主要功能。要开始引导式设置,只需切换到项目目录并运行:

periphery scan --setup

引导式设置仅适用于 Xcode 和 SwiftPM 项目,要在非苹果构建系统(如 Bazel)中使用 Periphery,请参阅构建系统

回答几个问题后,Periphery 将打印出完整的扫描命令并执行它。

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

要从 Periphery 获得一致的结果,理解你选择分析的构建目标的影响至关重要。例如,假设一个项目由三个目标组成:App、Lib 和 Tests。App 目标导入 Lib,Tests 目标导入 App 和 Lib。如果你在 --targets 选项中提供所有三个目标,那么 Periphery 将能够分析你的整个项目。但是,如果你只选择分析 App 和 Lib,而不分析 Tests,Periphery 可能会报告一些未使用的代码实例,这些实例_仅_被 Tests 引用。因此,当你怀疑 Periphery 提供了不正确的结果时,考虑你选择分析的目标很重要。

如果你的项目由一个或多个独立的框架组成,这些框架不包含消费其接口的应用程序,那么你需要告诉 Periphery 假设所有公共声明实际上都被使用,方法是包含 --retain-public 选项。

对于混合了 Objective-C 和 Swift 的项目,强烈建议你阅读相关影响

配置

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

工作原理

Periphery 首先构建你的项目。对于 Xcode 项目,通过 --schemes 选项提供的方案使用 xcodebuild 构建。对于 Swift Package Manager 项目,通过 --targets 选项提供的单个目标使用 swift build 构建。Swift 编译器采用一种称为边构建边索引的技术来填充索引存储,其中包含有关项目源代码结构的信息。 项目构建完成后,Periphery 会执行索引阶段。对于通过 --targets 选项提供的目标中的每个源文件,Periphery 从索引存储中获取其结构信息,并构建项目的内部图形表示。Periphery 还分析每个文件的抽象语法树(AST),以填补索引存储未提供的一些细节。

索引完成后,Periphery 分析图形以识别未使用的代码。这个阶段包括一系列步骤,这些步骤会改变图形,使其更容易识别未使用代码的特定场景。最后一步是从图形的根部遍历,以识别不再被引用的声明。

分析

Periphery 的目标是报告未使用的声明实例。声明包括 classstructprotocolfunctionpropertyconstructorenumtypealiasassociatedtype 等。正如您所预期的,Periphery 能够识别简单的未引用声明,例如在代码库中不再使用的 class

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

函数参数

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

协议

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

protocol Greeter {
    func greet(name: String)
    func farewell(name: String) // 'name' 未使用
}

class InformalGreeter: Greeter {
    func greet(name: String) {
        print("嗨 " + name + "。")
    }

    func farewell(name: String) { // 'name' 未使用
      print("再见。")
    }
}

提示

您可以使用 --retain-unused-protocol-func-params 选项忽略所有来自协议和遵循函数的未使用参数。

重写

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

class BaseGreeter {
    func greet(name: String) {
        print("你好。")
    }

    func farewell(name: String) { // 'name' 未使用
        print("再见。")
    }
}

class InformalGreeter: BaseGreeter {
    override func greet(name: String) {
        print("嗨 " + name + "。")
    }

    override func farewell(name: String) { // 'name' 未使用
        print("拜。")
    }
}

外部协议和类

外部模块(如 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:) 尚未实现")
    }
}

协议

一个被对象遵循的协议,只有在作为存在类型使用或用于特化泛型方法/类时才是真正被使用的。Periphery 能够识别这种冗余协议,无论它们被一个还是多个对象遵循。

protocol MyProtocol { // 'MyProtocol' 是冗余的
    func someMethod()
}

class MyClass1: MyProtocol { // 'MyProtocol' 遵循是冗余的
    func someMethod() {
        print("来自 MyClass1 的问候!")
    }
}

class MyClass2: MyProtocol { // 'MyProtocol' 遵循是冗余的
    func someMethod() {
        print("来自 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' 未使用
}

class MyConformingClass: MyProtocol {
    var usedProperty: String = "已使用"
    var unusedProperty: String = "未使用" // 'unusedProperty' 未使用
}

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 案例,所以我们可能会认为它不再需要。但是,如果删除它,我们可以看到当 someFunction 传入 "myCase" 值时,somethingImportant 将永远不会被调用。

仅赋值属性

被赋值但从未使用的属性会被识别为仅赋值属性,例如:

class MyClass {
    var assignOnlyProperty: String // 'assignOnlyProperty' 被赋值,但从未使用

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

在某些情况下,这可能是预期的行为,因此你有几个选项可以消除这类结果:

  • 使用注释命令保留单个属性。
  • 使用 --retain-assign-only-property-types 按类型保留所有仅赋值属性。给定的类型必须与属性声明中的确切用法匹配(不包括可选的问号),例如 String[String]Set<String>。Periphery 无法解析推断的属性类型,因此在某些情况下,你可能需要为属性添加显式类型注解。
  • 使用 --retain-assign-only-properties 完全禁用仅赋值属性分析。

冗余的公共可访问性

被标记为 public 但在其所在模块外部没有被引用的声明会被识别为具有冗余的公共可访问性。在这种情况下,可以从声明中移除 public 注解。移除冗余的公共可访问性有几个好处:

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

未使用的导入

Periphery 可以检测它已扫描的目标(即用 --targets 参数指定的目标)中未使用的导入。它无法检测其他目标的未使用导入,因为无法获取 Swift 源文件,也无法观察 @_exported 的使用情况。@_exported 存在问题,因为它改变了目标的公共接口,使得目标导出的声明不再必须由导入的目标声明。例如,Foundation 目标导出了 Dispatch 等多个目标。如果任何给定的源文件导入 Foundation 并引用了 DispatchQueue,但没有引用 Foundation 中的其他声明,那么就不能移除 Foundation 导入,因为这也会使 DispatchQueue 类型不可用。因此,为了避免误报,Periphery 只检测它已扫描的目标中未使用的导入。

对于混合 Swift 和 Objective-C 的目标,Periphery 可能会产生误报,因为 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 未扫描的外部模块中的协议声明的,你可以使用 --external-codable-protocols "ExternalProtocol" 指示 Periphery 将这些协议识别为 Codable

XCTestCase

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

Interface Builder

如果你的项目包含 Interface Builder 文件(如 storyboard 和 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 命令可以放在源文件的顶部,以忽略文件的全部内容。请注意,该注释必须放在任何代码之上,包括导入语句。

注释命令还支持在连字符后添加尾随注释,这样你可以在同一行中包含解释:

// periphery:ignore - 解释为什么这是必要的
class MyClass {}

Xcode 集成

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

步骤 1:创建聚合目标

在项目导航器中选择你的项目,然后点击目标部分左下角的 + 按钮。选择跨平台并选择聚合。点击下一步。

步骤 1

为新目标选择一个名称,例如 "Periphery" 或 "未使用代码"。

步骤 2

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

构建阶段部分,点击 + 按钮添加新的运行脚本阶段。

步骤 3

在 shell 脚本窗口中输入 Periphery 命令。确保包含 --format xcode 选项。

步骤 4

步骤 3:选择并运行

现在你已准备就绪。你应该能在下拉菜单中看到新的方案。选择它并点击运行。

提示

如果你希望团队中的其他人也能使用该方案,你需要将其标记为_共享_。可以通过选择_管理方案..._并选中新方案旁边的_共享_复选框来完成。现在可以将方案定义提交到源代码控制中。

步骤 5

排除文件

以下两个排除选项都接受 Bash v4 风格的路径通配符,可以是绝对路径或相对于你的项目目录的路径。你可以用空格分隔多个通配符,例如 --option "Sources/Single.swift" "**/Generated/*.swift" "**/*.{xib,storyboard}"

排除结果

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

排除索引文件

要从索引中排除文件,请向 scan 命令传递 --index-exclude <globs> 选项。从索引阶段排除文件意味着 Periphery 将看不到这些文件中包含的任何声明和引用。Periphery 的行为就像这些文件不存在一样。例如,此选项可用于排除包含对非生成代码引用的生成代码,或排除所有包含对代码引用的 .xib.storyboard 文件。

保留文件声明

要保留文件中的所有声明,请向 scan 命令传递 --retain-files <globs> 选项。此选项相当于在每个文件顶部添加 // 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 索引存储的位置。

构建系统

Periphery 可以分析使用第三方构建系统(如 Bazel)的项目,但不能像 SwiftPM 和 xcodebuild 那样自动驱动它们。相反,您需要指定索引存储位置并提供文件-目标映射文件。

文件-目标映射文件包含源文件到构建目标的简单映射。您需要使用构建系统的适当工具自行生成此文件。格式如下:

{
  "file_targets": {
    "path/to/file_a.swift": ["TargetA"],
    "path/to/file_b.swift": ["TargetB", "TargetC"]
  }
}

提示

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

然后您可以如下调用 periphery:

periphery scan --file-targets-path map.json --index-store-path index/store

提示

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

平台

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

故障排除

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

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

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

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

struct BuildInfo {
    let debugName = "debug"
    let releaseName = "release" // 'releaseName' 未使用

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

您有几个选项来解决这个问题:

  • 使用注释命令显式忽略 releaseName
  • 过滤结果以删除已知实例。
  • 为每个构建配置运行一次 Periphery 并合并结果。您可以在 -- 后指定传递给底层构建的参数,例如 periphery scan ... -- -configuration release

Swift 包是平台特定的

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

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

示例:

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

# 2. 使用生成的索引存储进行扫描
periphery scan --skip-build --index-store-path '../dd/Index.noindex/DataStore/'

已知问题

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

ID标题
56559索引存储未关联通过 Self 引用的构造函数
56541索引存储未关联用作下标键的静态属性获取器
56327索引存储未关联子类中实现的 objc 可选协议方法
56189索引存储应关联字符串字面量中的 appendInterpolation
56165索引存储未关联通过字面量表示法的构造函数
49641索引不包含对具有泛型类型的类/结构体构造函数的引用

赞助商 赞助商

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

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

Emerge Tools

Emerge Tools 是一套革命性产品,旨在为移动应用及其开发团队提供超强动力。

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

白日梦AI

白日梦AI提供专注于AI视频生成的多样化功能,包括文生视频、动态画面和形象生成等,帮助用户快速上手,创造专业级内容。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

讯飞绘镜

讯飞绘镜是一个支持从创意到完整视频创作的智能平台,用户可以快速生成视频素材并创作独特的音乐视频和故事。平台提供多样化的主题和精选作品,帮助用户探索创意灵感。

Project Cover

讯飞文书

讯飞文书依托讯飞星火大模型,为文书写作者提供从素材筹备到稿件撰写及审稿的全程支持。通过录音智记和以稿写稿等功能,满足事务性工作的高频需求,帮助撰稿人节省精力,提高效率,优化工作与生活。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号