做者: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。