swift-foundations/swift-linter
GitHub: swift-foundations/swift-linter
一个基于 SwiftSyntax 的 Swift AST 代码检查工具,通过可配置的规则集发现类型系统和所有权层面的深层代码问题。
Stars: 0 | Forks: 0
# swift-linter
 
基于 SwiftSyntax 的 Swift 包 AST linter。它承载的规则
断言需要抽象语法树 —— 类型系统逃逸模式、
所有权规范违规、规约镜像一致性 ——
这些无法通过对源码文本的正则表达式来表达。
## 快速开始
对任意 Swift 包目录运行 linter:
```
swift run swift-linter /path/to/your-package
```
引擎开箱即用时与规则包无关 —— 如果没有显式配置,
将不会触发任何规则。要激活一套规则集,请在您的包根目录下放置一个 `Lint/` 嵌套 SwiftPM
包(参见下文的*采用 `Lint/` 结构*)。
默认输出为兼容 SwiftLint 的文本行;`--format sarif`
将输出适合作为 CI 制品上传的 SARIF 2.1.0 JSON。
## 安装
```
dependencies: [
.package(url: "https://github.com/swift-foundations/swift-linter.git", branch: "main"),
]
```
```
.target(
name: "YourTarget",
dependencies: [
.product(name: "Linter", package: "swift-linter"),
]
)
```
`swift-linter` 可执行文件作为同一
包的独立产品发布。要进行临时调用,
`swift run --package-path swift-linter ` 即可
开箱即用。
## 它与 SwiftLint 和 swift-format 的关系
`swift-linter` 并不能替代这两种工具中的任何一种;这三者的区别在于
定位,而不在于能力上限。这三者都处理 Swift 源码,
并且都能实现 AST 型规则 —— 问题在于 AST
路径是*主要*调用方式,还是与其他方式并列的可选模式。
| 工具 | 定位 | 主要机制 |
|------|---------|-------------------|
| [swift-format](https://github.com/swiftlang/swift-format) | 带有 `lint` 子命令的格式化工具 | 基于 SwiftSyntax 的格式化 + 风格规则;内置约 43 条规则,涵盖缩进、大括号位置、成员排序、文档注释形状以及结构坏味 |
| [SwiftLint](https://github.com/realm/SwiftLint) | 风格/约定 linter;规则主要基于 SwiftSyntax,带有可选的 `analyze` 命令,可添加由 SourceKit 支持的类型信息规则 | 普通情况下使用 SwiftSyntax;当需要类型信息时,通过 `swiftlint analyze` 使用 SourceKit-LSP |
| **swift-linter**(本包) | 构造上仅限 AST;AST 断言即为其表面接口 | SwiftSyntax + SwiftParser;链条中无 SourceKit-LSP 依赖 |
这三者可以结合使用:`swift-format` 用于风格规范化,SwiftLint
用于广泛覆盖的风格/约定包(使用 `swiftlint analyze` 处理
类型信息规则),而 swift-linter 用于规则的
本质就是 AST 断言本身的场景 —— 类型系统逃逸模式、
所有权规范检查、规约镜像一致性。
## 两种消费者结构
`swift-linter` 会在消费者的
包根目录下检测两种配置结构。**大多数消费者应该采用 `Lint.swift`**;`Lint/` 是
高级结构,专为单文件结构无法
表达的情况保留。
1. **`Lint.swift` 单文件**(推荐大多数消费者使用)—— 位于包根目录的单一 Swift 文件,声明 tools-version、
rule-pack 依赖项,并通过
`Lint.run(dependencies:) { ... }` 激活规则集。涵盖了激活机构发布的
bundle
(`Lint.Rule.Bundle.universal` / `.institute` / `.primitives`)的常见情况,
可选择通过 `.excluding(rules:)` 为品牌所有者
进行缩减。无需嵌套 SwiftPM 解析;一个文件,一次解析。
2. **`Lint/` 嵌套 SwiftPM 包**(高级)—— 一个
`Lint/Package.swift` + `Lint/Sources/Lint/main.swift` 对,导入规则包并通过 result-builder DSL 直接实例化
`Lint.Configuration`。当消费者
需要内部自定义规则(定义新
`Lint.Rule` 实例的任意 Swift 代码)、未在任何
机构 bundle 中声明的第三方 rule pack,或者需要带有接受消费者侧域值的构造函数调用的逐规则编程式配置时,这是必需的。
这两种结构在运行时都会生成相同的 `Lint.Configuration` —— 参见
[*内部模型*](#internal-model)。
## 采用 `Lint.swift`(推荐)
在您的包根目录下创建一个 `Lint.swift` 文件,与 `Package.swift` 同级:
```
your-package/
├── Package.swift
├── Lint.swift ← here
└── Sources/...
```
该文件声明了 tools-version 指令,导入已激活的 bundle
所引入的 rule pack,并在尾随闭包中
调用 `Lint.run(dependencies:) { ... }` 以激活该 bundle:
```
// swift-linter-tools-version: 0.1
// (Apache-2.0 license header)
import Linter
import Linter_Institute_Rules
Lint.run(dependencies: [
.package(
url: "https://github.com/swift-foundations/swift-institute-linter-rules.git",
branch: "main",
products: ["Linter Institute Rules"]
),
]) {
Lint.Rule.Bundle.institute
}
```
**Tools-version 指令** (`// swift-linter-tools-version: 0.1`) 必须
是第一行 —— 它告知引擎该文件目标使用的 DSL 版本,这映射了 SwiftPM 的 `swift-tools-version` 规范。
**Bundle 选择**:在五层架构中,激活与您的包所在的层相匹配的 bundle —— 通用 Swift 代码对应 `Lint.Rule.Bundle.universal`,L2/L3 标准和基础对应 `.institute`,L1 原语对应 `.primitives`。这些 bundle 是可累加组合的
(`institute = universal + institute-pack`;`primitives = institute +
primitives-pack`),因此激活更高层级的 bundle 会传递性地
激活较低层级的规则。
**品牌所有者排除项**:品牌所有者包(其主要导出的是类型化原语,且其规则旨在约束外部消费者对该品牌的访问)通过 `.excluding(rules:)` 缩减 bundle:
```
Lint.Rule.Bundle.primitives.excluding(rules: [
Lint.Rule.`raw value access`.id,
Lint.Rule.`unchecked call site`.id,
// ...
])
```
在 Swift 6.3+ 的 `MemberImportVisibility` (SE-0444) 下,每个通过 `.id` 引用的规则都要求直接导入其声明模块。每个
排除项都应该附带一条文件内注释,指明证明该排除合理的品牌边界站点。
**调用**:从您的包根目录运行 `swift run swift-linter .`。
## 采用 `Lint/`(高级)
仅当满足以下触发条件之一时,才采用嵌套包结构:
| 触发条件 | 为什么 `Lint.swift` 无法表达 |
|---|---|
| 内部自定义规则(定义新 `Lint.Rule` 实例的 Swift 代码) | 自定义规则需要一个 SwiftPM 编译单元;单文件 `Lint.swift` 会解析但不会编译任意的规则代码 |
| 未在任何机构 bundle 中声明的第三方 rule pack | 激活非机构 rule pack 需要将其声明为 SwiftPM 依赖项并导入其模块 —— 需要 `Package.swift` |
| 带有构造函数调用的逐规则编程式配置 | `Lint.Configuration { Lint.Rule.Configuration.enable(...) }` DSL 接受带有消费者侧域值的规则构造函数调用;而 bundle DSL 是由元类型驱动的 |
如果这些触发条件都不适用,请改用 `Lint.swift`。
在您的包根目录下创建一个 `Lint/` 目录,其结构如下:
```
your-package/
├── Package.swift
├── Sources/...
└── Lint/
├── Package.swift
└── Sources/Lint/main.swift
```
`Lint/Package.swift` 依赖于您想要激活的 rule pack:
```
// Lint/Package.swift
let package = Package(
name: "Lint",
products: [.executable(name: "Lint", targets: ["Lint"])],
dependencies: [
.package(url: "https://github.com/swift-foundations/swift-linter.git", branch: "main"),
.package(url: "https://github.com/swift-foundations/swift-linter-rules.git", branch: "main"),
],
targets: [
.executableTarget(
name: "Lint",
dependencies: [
.product(name: "Linter", package: "swift-linter"),
.product(name: "Linter Rule Unchecked", package: "swift-linter-rules"),
.product(name: "Linter Rule Cardinal", package: "swift-linter-rules"),
]
),
]
)
```
`Lint/Sources/Lint/main.swift` 通过
`Lint.Configuration` result-builder 激活导入的规则,然后针对
消费者的源码树运行 linter:
```
// Lint/Sources/Lint/main.swift
import File_System
import Linter
import Linter_Reporter_Text
import Linter_Rule_Unchecked
import Linter_Rule_Cardinal
import Terminal_Primitives
let configuration = Lint.Configuration {
Lint.Rule.Configuration.enable(Lint.Rule.Unchecked.self)
Lint.Rule.Configuration.enable(Lint.Rule.Cardinal.Count.self)
}
let arguments = Swift.CommandLine.arguments
let pathStrings: [Swift.String] = arguments.count >= 2
? [Swift.String](arguments.dropFirst())
: ["."]
do {
let consumerPaths: [File.Path] = try pathStrings.map { try File.Path($0) }
let findings = try Lint.Run.run(paths: consumerPaths, configuration: configuration)
Lint.Reporter.Text.emit(findings: findings, to: Terminal.Stream.stdout.write)
} catch {
print("[Lint] error: \(error)")
}
```
规则通过元类型引用(`Lint.Rule.Unchecked.self`)激活,
而不是通过字符串标识符 —— 引擎通过 `.self`
解析身份,并将类型化的元类型传递整个配置。result-builder 的顶层位置要求使用完全限定的
`Lint.Rule.Configuration.enable(...)` 形式(builder 声明了多个 `buildExpression` 重载,因此在不受约束的位置上前导点推断是
有歧义的;在 `if`/`for` 主体内部,上下文类型会变窄,前导点形式在此处有效)。
`swift run swift-linter ` 会检测到消费者的 `Lint/`
嵌套包,构建它,并将 lint 运行分发给
消费者的 `Lint` 可执行文件,该文件链接了引擎 + rule pack,并针对消费者的源码树运行 `Lint.Run.run(paths:configuration:)`。
## 内部模型
**`Lint/` 是规范的内部实现;`Lint.swift` 是
在此基础之上构建的。** 引擎的心智模型是由 result-builder DSL 生成的类型化
`Lint.Configuration` —— 这正是
`Lint/Sources/Lint/main.swift` 显式构造的内容。`Lint.swift` 是一个
单文件前端,其源码由
`Lint.File.Single.Extractor` 解析,通过
`Lint.Configuration.lift` 提升为 `Lint.Configuration`,然后由嵌套包结构直接调用的同一个
`Lint.Run.run(paths:configuration:)` 入口执行。从引擎的角度来看,这两种结构汇聚于
相同的内部类型;消费者的选择纯粹是为了人体工程学。
这种不对称性 —— 推荐单文件,以嵌套包为规范 ——
是刻意为之的:
- 对于激活带有可选品牌所有者排除项的机构 bundle 这种常见情况,单文件结构将消费者侧的设置成本降至最低(一个文件,
一次解析,无需嵌套 SwiftPM 解析)。
- 对于单文件结构无法表达的情况,嵌套包结构暴露了类型化 DSL 的全部威力
(自定义规则类型、第三方 rule pack、编程式的逐规则
配置)。
- 内部以嵌套包为规范保持了引擎
契约的单一事实来源 —— 每个前端都会生成相同的
下游 `Lint.Configuration`。未来仅声明式的前端
(例如 YAML 模式)将作为生成相同
内部类型的新解析器发布,而不是作为并行的执行路径。
## 通过 `// parent:` 指令继承
在托管于
URL 上的规范配置之上分层您的配置。将该指令放置在 `Lint.swift`(或
`Lint/Sources/Lint/main.swift`)的前 30 行内:
```
// swift-linter-tools-version: 0.1
// parent: https://raw.githubusercontent.com//.github/main/Lint.swift
import Linter
import Linter_Institute_Rules
Lint.run(dependencies: [
.package(
url: "https://github.com/swift-foundations/swift-institute-linter-rules.git",
branch: "main",
products: ["Linter Institute Rules"]
),
]) {
Lint.Rule.Bundle.institute
}
```
接受的协议:`http://`, `https://`, `file://`。驱动程序通过 `curl` 获取
每个 parent(每个进程进行记忆化缓存),并带有循环检测和
16 层深度后备。发生任何获取失败时,驱动程序会发出警告并回退
到仅含消费者的配置 —— 该链条是尽力而为的。
任何层级的逐规则覆盖都会在“后发层优先(later layer wins)”语义下覆盖更深层的配置。
组织规范配置的常规公开指针是
其 `.github` 仓库的原始 URL:`/.github/main/Lint.swift`。
这在文件层映射了 SwiftLint 的 `parent_config:` 级联。
## 架构
### 稳定性与 SemVer
`swift-linter` 处于 1.0 之前的阶段。次要版本边界(0.1.x → 0.2.0)
允许引入破坏性源码更新;锁定 `from: "0.1.0"` 的消费者应
计划在每次次要版本升级时审查迁移说明。1.0 的转折点将
标志着 API 表面在标准 SemVer 契约下趋于稳定
(次要版本中不包含破坏性源码更新;移除前会先进行废弃处理)。
已知的 0.1.x → 0.2 候选更改已在 API 表面
本身记录;最突出的是 `Lint.Manifest`
结构体字段的裸形式重命名(参见下文的“Wire-key 稳定性”)。
### Wire-key 稳定性
`Lint.Manifest` 带有三个数组形状的字段 ——
`enabledRuleIDs`、`disabledRuleIDs`、`excludedPaths` —— 引擎在跨越 consumer-driver-shim 子进程
边界时将它们序列化为 JSON。这些字段的 JSON wire-key(`"enabledRuleIDs"`、
`"disabledRuleIDs"`、`"excludedPaths"`)在 0.x 版本中是稳定的;为了去除命名空间隐式前缀的
冗余,Swift 属性名可能会在 0.2 版本中重命名为裸形式(`enabled`、
`disabled`、`excluded`),同时通过序列化器侧的映射保持 wire 兼容性。基于 0.1.x 构建的 JSON
消费者在 0.2 Swift 侧
重命名后依然能够正常工作;Swift API 消费者每个字段只需进行一行代码的迁移。
### 五包队列
该实现划分为五个同级包:
| 包 | 层级 | 角色 |
|---------|-------|------|
| **swift-linter**(本包) | L3 (Foundations) | 引擎、CLI、报告器 |
| swift-linter-rules | L3 | 默认 rule pack |
| swift-manifests | L3 | Manifest 加载器 + parent-chain 解析器 |
| swift-manifest-primitives | L1 (Primitives) | `Manifest.Dependency`, `Manifest.NestedPackage` 类型 |
| swift-linter-primitives | L1 | `Lint.Configuration`, `Lint.Rule.Protocol`, `Lint.Filter`,类型化 DSL 表面 |
这种划分反映了该机构的五层架构:L1
原语提供原子(类型化 DSL 表面、依赖形状类型);
L3 foundations 将它们组合成可运行的工具。单包合并会将 L1 类型化 DSL 表面(面向消费者的类型词汇表)与 L3 引擎(运行编排)混淆从而破坏分层结构。消费者通过 URL 形式的 `.package(url:from:)` 声明依赖 `swift-linter`;队列的原语将被传递性地引入。
## 文档
涵盖规则目录、配置 schema 和 CI
集成方案的 DocC 目录将推迟至单独的周期进行。
## 状态与维护者
本包处于公开 Alpha 阶段(1.0 之前):接口正在稳定中,在首次打标签发布之前 API 可能会发生变化。
由 [Coen ten Thije Boonkkamp](https://github.com/coenttb) 维护 —— 提供 Swift 基础设施和文档系统咨询:coen@coenttb.com。
## 许可证
Apache 2.0。参见 [LICENSE.md](LICENSE.md)。
标签:Apache Flink, SARIF, SOC Prime, Swift, 云安全监控, 代码规范检查, 开发工具, 静态分析