SwiftSyntax

做者:Mattt,原文連接,原文日期:2018-10-22 譯者:jojotov;校對:numbbbbbYousanflicspmst;定稿:Forelaxhtml

SwiftSyntax 是一個能夠分析、生成以及轉換 Swift 源代碼的 Swift 庫。它是基於 libSyntax 庫開發的,並於 2017 年 8 月 從 Swift 語言的主倉庫中分離出來,單獨創建了一個倉庫。python

總的來講,這些庫都是爲了給結構化編輯(structured editing)提供安全、正確且直觀的工具。關於結構化編輯,在 thusly 中有具體的描述:git

什麼是結構化編輯?結構化編輯是一種編輯的策略,它對源代碼的結構更加敏感,而源代碼的表示(例如字符或者字節)則沒那麼重要。這能夠細化爲如下幾個部分:替換標識符,將對全局方法的調用轉爲對方法的調用,或者根據已定的規則識別並格式化整個源文件。github

在寫這篇文章時,SwiftSyntax 仍處於在開發中並進行 API 調整的階段。不過目前你已經可使用它對 Swift 代碼進行一些編程工做。正則表達式

目前,Swift Migrator 已經在使用 SwiftSyntax 了,而且在對內和對外層面上,對 SwiftSyntax 的接入也在不斷地努力着。shell

SwiftSyntax 如何工做?

爲了明白 SwiftSyntax 如何工做,咱們首先要回頭看看 Swift 編譯器的架構:編程

Swift 編譯器的主要職責是把 Swift 代碼轉換爲可執行的機器代碼。整個過程能夠劃分爲幾個離散的步驟,一開始,語法分析器 會生成一個抽象語法樹(AST)。以後,語義分析器會進行工做並生成一個經過類型檢查的 AST。至此步驟,代碼會降級到 Swift 中間層語言;隨後 SIL 會繼續轉換並優化自身,降級爲 LLVM IR,並最終編譯爲機器代碼。json

對於咱們的討論來講,最重要的關鍵點是 SwiftSyntax 的操做目標是編譯過程第一步所生成的 AST。但也因爲這樣,SwiftSyntax 沒法告知你任何關於代碼的語義或類型信息。swift

與 SwiftSyntax 相反,一些如 SourceKit 之類的工具,操做的目標爲更容易理解的 Swift 代碼。這能夠幫助此類工具實現一些編輯器相關的特性,例如代碼補全或者文件之間的跳轉。雖然 SwiftSyntax 不能像 SourceKit 同樣實現跳轉或者補全的功能,但在語法層面上也有不少應用場景,例如代碼格式化和語法高亮。安全

揭祕 AST

抽象語法樹在抽象層面上比較難以理解。所以咱們先生成一個示例來一睹其貌。

留意一下以下的一行 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 用例就是讓編寫 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 來用於天天的代碼生成。(事實上,libSyntaxSwiftSyntax 都使用了 gyb 來生成接口。

但這個接口在某些特殊的問題上卻格外有用。例如,你或許會使用 SwiftSyntax 來實現一個 Swift 編譯器的 模糊測試,使用它能夠隨機生成一個表面有效卻實際上很是複雜的程序,以此來進行壓力測試。

重寫 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!")
複製代碼

鬼魅通常,對吧?

高亮 Swift 代碼

讓咱們用一個真正實用的東西來總結咱們對 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

相關文章
相關標籤/搜索