做者:Mattt,原文连接,原文日期:2018-10-22 译者:jojotov;校对:numbbbbb,Yousanflics,pmst;定稿:Forelaxhtml
SwiftSyntax 是一个能够分析、生成以及转换 Swift 源代码的 Swift 库。它是基于 libSyntax 库开发的,并于 2017 年 8 月 从 Swift 语言的主仓库中分离出来,单独创建了一个仓库。python
总的来讲,这些库都是为了给结构化编辑(structured editing)提供安全、正确且直观的工具。关于结构化编辑,在 thusly 中有具体的描述:git
什么是结构化编辑?结构化编辑是一种编辑的策略,它对源代码的结构更加敏感,而源代码的表示(例如字符或者字节)则没那么重要。这能够细化为如下几个部分:替换标识符,将对全局方法的调用转为对方法的调用,或者根据已定的规则识别并格式化整个源文件。github
在写这篇文章时,SwiftSyntax 仍处于在开发中并进行 API 调整的阶段。不过目前你已经可使用它对 Swift 代码进行一些编程工做。正则表达式
目前,Swift Migrator 已经在使用 SwiftSyntax 了,而且在对内和对外层面上,对 SwiftSyntax 的接入也在不断地努力着。shell
为了明白 SwiftSyntax 如何工做,咱们首先要回头看看 Swift 编译器的架构:编程
Swift 编译器的主要职责是把 Swift 代码转换为可执行的机器代码。整个过程能够划分为几个离散的步骤,一开始,语法分析器 会生成一个抽象语法树(AST)。以后,语义分析器会进行工做并生成一个经过类型检查的 AST。至此步骤,代码会降级到 Swift 中间层语言;随后 SIL 会继续转换并优化自身,降级为 LLVM IR,并最终编译为机器代码。json
对于咱们的讨论来讲,最重要的关键点是 SwiftSyntax 的操做目标是编译过程第一步所生成的 AST。但也因为这样,SwiftSyntax 没法告知你任何关于代码的语义或类型信息。swift
与 SwiftSyntax 相反,一些如 SourceKit 之类的工具,操做的目标为更容易理解的 Swift 代码。这能够帮助此类工具实现一些编辑器相关的特性,例如代码补全或者文件之间的跳转。虽然 SwiftSyntax 不能像 SourceKit 同样实现跳转或者补全的功能,但在语法层面上也有不少应用场景,例如代码格式化和语法高亮。安全
抽象语法树在抽象层面上比较难以理解。所以咱们先生成一个示例来一睹其貌。
留意一下以下的一行 Swift 代码,它声明了一个名为 one()
的函数,函数返回值为 1
:
func one() -> Int { return 1 }
复制代码
在命令行中对此文件运行 swiftc
命令并传入 -frontend -emit-syntax
参数:
$ xcrun swiftc -frontend -emit-syntax ./One.swift
复制代码
运行的结果为一串 JSON 格式的 AST。当你用 JSON 格式来展现时,AST 的结构会表现的更加清晰:
{
"kind": "SourceFile",
"layout": [{
"kind": "CodeBlockItemList",
"layout": [{
"kind": "CodeBlockItem",
"layout": [{
"kind": "FunctionDecl",
"layout": [null, null, {
"tokenKind": {
"kind": "kw_func"
},
"leadingTrivia": [],
"trailingTrivia": [{
"kind": "Space",
"value": 1
}],
"presence": "Present"
}, {
"tokenKind": {
"kind": "identifier",
"text": "one"
},
"leadingTrivia": [],
"trailingTrivia": [],
"presence": "Present"
}, ...
复制代码
Python 中的 json.tool
模块提供了便捷地格式化 JSON 的能力。且几乎全部的 macOS 系统都已经集成了此模块,所以每一个人均可以使用它。举个例子,你可使用以下的命令对编译的输出结果使用 json.tool
格式化:
$ xcrun swiftc -frontend -emit-syntax ./One.swift | python -m json.tool
复制代码
在最外层,能够看到 SourceFile
,它由 CodeBlockItemList
以及 CodeBlockItemList
内部的 CodeBlockItem
这几个部分组成。对于这个示例来讲,仅有一个 CodeBlockItem
对应函数的定义(FunctionDecl
),其自身包含了几个子组件如函数签名、参数闭包和返回闭包。
术语 trivia 用于描述任何没有实际语法意义的东西,例如空格。每一个标记符(Token)能够有一个或多个行前和行尾的 trivia。例如,在返回的闭包(-> Int
)中的 Int
后的空格能够用以下的行尾 trivia 表示:
{
"kind": "Space",
"value": 1
}
复制代码
SwiftSyntax 经过代理系统的 swiftc
调用来生成抽象语法树。可是,这也限制了代码必须放在某个文件才能进行处理,而咱们却常常须要对以字符串表示的代码进行处理。
为了解决这个限制,其中一种办法是把代码写入一个临时文件并传入到编译器中。
咱们曾经尝试过写入临时文件,但目前,有更好的 API 能够帮助咱们完成这项工做,它由 Swift Package Manager 自己提供。在你的 Package.swift
文件中,添加以下的包依赖关系,并把 Utility
依赖添加到正确的 target 中:
.package(url: "https://github.com/apple/swift-package-manager.git", from: "0.3.0"),
复制代码
如今,你能够像下面这样引入 Basic
模块并使用 TemporaryFile
API:
import Basic
import Foundation
let code: String
let tempfile = try TemporaryFile(deleteOnClose: true)
defer { tempfile.fileHandle.closeFile() }
tempfile.fileHandle.write(code.data(using: .utf8)!)
let url = URL(fileURLWithPath: tempfile.path.asString)
let sourceFile = try SyntaxTreeParser.parse(url)
复制代码
如今咱们对 SwiftSyntax 如何工做已经有了足够的理解,是时候讨论一下几个使用它的方式了!
咱们第一个想到,但倒是最没有实际意义的 SwiftSyntax 用例就是让编写 Swift 代码的难度提高几个数量级。
利用 SwiftSyntax 中的 SyntaxFactory
APIs,咱们能够生成完整的 Swift 代码。不幸的是,编写这样的代码并不像闲庭散步般轻松。
留意一下以下的示例代码:
import SwiftSyntax
let structKeyword = SyntaxFactory.makeStructKeyword(trailingTrivia: .spaces(1))
let identifier = SyntaxFactory.makeIdentifier("Example", trailingTrivia: .spaces(1))
let leftBrace = SyntaxFactory.makeLeftBraceToken()
let rightBrace = SyntaxFactory.makeRightBraceToken(leadingTrivia: .newlines(1))
let members = MemberDeclBlockSyntax { builder in
builder.useLeftBrace(leftBrace)
builder.useRightBrace(rightBrace)
}
let structureDeclaration = StructDeclSyntax { builder in
builder.useStructKeyword(structKeyword)
builder.useIdentifier(identifier)
builder.useMembers(members)
}
print(structureDeclaration)
复制代码
*唷。*那最后这段代码让咱们获得了什么呢?
struct Example {
}
复制代码
使人窒息的操做。
这毫不是为了取代 GYB 来用于天天的代码生成。(事实上,libSyntax 和 SwiftSyntax 都使用了 gyb
来生成接口。
但这个接口在某些特殊的问题上却格外有用。例如,你或许会使用 SwiftSyntax 来实现一个 Swift 编译器的 模糊测试,使用它能够随机生成一个表面有效却实际上很是复杂的程序,以此来进行压力测试。
在 SwiftSyntax 的 README 中有一个示例 展现了如何编写一个程序来遍历源文件中的整型并把他们的值加 1。
经过这个,你应该已经推断得出如何使用它来建立一个典型的 swift-format
工具。
但如今,咱们先考虑一个至关没有效率——而且可能在万圣节(🎃)这种须要捣蛋的场景才合适的用例,源代码重写:
import SwiftSyntax
public class ZalgoRewriter: SyntaxRewriter {
public override func visit(_ token: TokenSyntax) -> Syntax {
guard case let .stringLiteral(text) = token.tokenKind else {
return token
}
return token.withKind(.stringLiteral(zalgo(text)))
}
}
复制代码
zalgo
函数是用来作什么的?可能不知道会更好……
无论怎样,在你的源代码中运行这个重写器,能够把全部的文本字符串转换为像下面同样的效果:
// Before 👋😄
print("Hello, world!")
// After 🦑😵
print("H͞͏̟̂ͩel̵ͬ͆͜ĺ͎̪̣͠ơ̡̼͓̋͝, w͎̽̇ͪ͢ǒ̩͔̲̕͝r̷̡̠͓̉͂l̘̳̆ͯ̊d!")
复制代码
鬼魅通常,对吧?
让咱们用一个真正实用的东西来总结咱们对 SwiftSyntax 的探究:一个 Swift 语法高亮工具。
从语法高亮工具的意义上来讲,它能够把源代码按某种方式格式化为显示更为友好的 HTML。
NSHipster 经过 Jekyll 搭建,并使用了 Ruby 的库 Rouge 来渲染你在每篇文章中看到的示例代码。尽管如此,因为 Swift 的复杂语法和过快迭代,渲染出来的 HTML 并非 100% 正确。
不一样于 处理一堆麻烦的正则表达式,咱们能够构造一个 语法高亮器 来放大 SwiftSyntax 对语言的理解的优点。
根据这个核心目的,实现的方法能够很直接:实现一个 SyntaxRewriter
的子类并重写 visit(_:)
方法,这个方法会在遍历源文件的每一个标识符时被调用。经过判断每种不一样的标识符类型,你能够把相应的可高亮标识符映射为 HTML 标记。
例如,数字文本能够用类名是 m
开头的 <span>
元素来表示(mf
表示浮点型,mi
表示整型)。以下是对应的在 SyntaxRewriter
子类中的代码:
import SwiftSyntax
class SwiftSyntaxHighlighter: SyntaxRewriter {
var html: String = ""
override func visit(_ token: TokenSyntax) -> Syntax {
switch token.tokenKind {
// ...
case .floatingLiteral(let string):
html += "<span class=\"mf\">\(string)</span>"
case .integerLiteral(let string):
if string.hasPrefix("0b") {
html += "<span class=\"mb\">\(string)</span>"
} else if string.hasPrefix("0o") {
html += "<span class=\"mo\">\(string)</span>"
} else if string.hasPrefix("0x") {
html += "<span class=\"mh\">\(string)</span>"
} else {
html += "<span class=\"mi\">\(string)</span>"
}
// ...
default:
break
}
return token
}
}
复制代码
尽管 SyntaxRewritere
针对每一种不一样类型的语法元素,都已经实现了 visit(:)
方法,但我发现使用一个 switch
语句能够更简单地处理全部工做。(在 default
分支中打印出没法处理的标记符,能够更好地帮助咱们找到那些没有处理的状况)。这不是最优雅的实现,但鉴于我对 SwiftSyntax 不足的理解,这是个较好的开端。
无论怎样,在几个小时的开发工做后,我已经能够在 Swift 大量的语法特性中,生成出比较理想的渲染过的输出。
这个项目须要一个库和命令行工具的支持。快去 尝试一下 而后让我知道你的想法吧!
本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 swift.gg。