SwiftSyntax 詳解

涼介· 2019-10-31
本文來自 掘金 ,作者 涼介

SwiftSyntax是基于libSyntax構建的Swift庫,利用它可以分析,生成和轉換Swift代碼。現在已經有一些基于它開源的庫,比如SwiftRewriter針對代碼進行自動格式化(其中包括基于代碼規范進行簡單的代碼優化)。

Swift 編譯器

Swift編譯器分為前端和后端,LLVM架構下的都是如此(Objective-C編譯器的前端是Clang,后端是也是LLVM),下圖是Swift的編譯器結構:

image.png

下面來解釋一下各個階段

image.png

SwiftSyntax

SwiftSyntax 的操作目標是編譯過程第一步所生成的 AST,從上面了解到AST不包含語義和類型信息,本文的關注點也是AST,其實生成AST需要兩步:

第一步,詞法分析,也叫做掃描scanner(或者Lexer)。它讀取我們的代碼,然后把它們按照預定的規則合并成一個個的標識tokens。同時,它會移除空白符,注釋等。最后,整個代碼將被分割進一個tokens列表(或者說一維數組)。

當詞法分析源代碼的時候,它會一個一個字母地讀取代碼,所以很形象地稱之為掃描-scans;當它遇到空格,操作符,或者特殊符號的時候,它會認為一個話已經完成了。

image.png

第二步,語法分析,也解析器。它會將詞法分析出來的數組轉化成樹形的表達形式。當生成樹的時候,解析器會刪除一些沒必要的標識tokens(比如不完整的括號),因此AST不是100%與源碼匹配的,但是已經能讓我們知道如何處理了。

xcrun swiftc -frontend -emit-syntax ./Cat.swift | python -m json.tool

可以在終端使用這個命令,結果為一串 JSON 格式的 AST,我截取了其中一部分,把開頭import相關的移除了。

{
    "id": 28,
    "kind": "SourceFile",
    "layout": [
        {
            "id": 27,
            "kind": "CodeBlockItemList",
            "layout": [
                {
                    "id": 25,
                    "kind": "CodeBlockItem",
                    "layout": [
                        {
                            "id": 24,
                            "kind": "StructDecl",
                            "layout": [
                                null,
                                null,
                                {
                                    "id": 7,
                                    "leadingTrivia": [
                                        {
                                            "kind": "Newline",
                                            "value": 2
                                        }
                                    ],
                                    "presence": "Present",
                                    "tokenKind": {
                                        "kind": "kw_struct"
                                    },
                                    "trailingTrivia": [
                                        {
                                            "kind": "Space",
                                            "value": 1
                                        }
                                    ]
                                },
                                {
                                    "id": 8,
                                    "leadingTrivia": [],
                                    "presence": "Present",
                                    "tokenKind": {
                                        "kind": "identifier",
                                        "text": "Cat"
                                    },
                                    "trailingTrivia": [
                                        {
                                            "kind": "Space",
                                            "value": 1
                                        }
                                    ]
                                },
                                null,
                                null,
                                null,
                                {
                                    "id": 23,
                                    "kind": "MemberDeclBlock",
                                    "layout": [
                                        {
                                            "id": 9,
                                            "leadingTrivia": [],
                                            "presence": "Present",
                                            "tokenKind": {
                                                "kind": "l_brace"
                                            },
                                            "trailingTrivia": []
                                        }

SwiftSyntax內部構造

RawSyntax

RawSyntax是所有Syntax的原始不可變后備存儲,表示語法樹基礎的原始樹結構。這些節點沒有身份的概念,僅提供樹的結構。它們是不可變的,可以在語法節點之間自由共享,因此它們不維護任何父母關系。最終,RawSyntax在以TokenSyntax類表示的Token中達到最低點,也就是葉子節點。

  • RawSyntax 是所有語法的不可變后備存儲。

  • RawSyntax 是不可變的。

  • RawSyntax 建立語法的樹結構。

  • RawSyntax 不存儲任何父母關系,因此如果語法節點具有相同的內容,則可以在語法節點之間共享它們。

final class RawSyntax: ManagedBuffer<RawSyntaxBase, RawSyntaxDataElement> {
let data: RawSyntaxData
var presence: SourcePresence
}

/// 特定樹或者Token節點的數據
fileprivate enum RawSyntaxData {
  /// 一個token,包含tokenKind,leading trivia, and trailing trivia
  case token(TokenData)
  /// 一個樹節點,包含syntaxKind和一個子節點數組
  case layout(LayoutData)
}

Trivia

Trivia與程序的語義無關,以下是一些Trivia的“原子”例子:

  • 空格

  • 標簽

  • 換行符

  • // 注釋

  • /* ... */ 注釋

  • /// 注釋

  • /** ... */ 注釋

  • ` ` 反引號

解析或構造新的語法節點時,應遵循以下兩個Trivia規則:

  1. Trailing trivia: 一個Token擁有它之后的所有Trivia,直到遇到下一個換行符,并且不包含這個換行符。

  2. Leading trivia: 一個Token擁有它之前的所有Trivia,直到遇到第一個換行符,并且包含這個換行符。

例子

func foo() {
  var x = 2
}

我們來逐個Token分解

  • func

  • Leading trivia: 無

  • Trailing trivia: 占有之后的一個空格(根據規則1)

// Equivalent to:
Trivia::spaces(1)
  • foo

  • Leading trivia: 無,前一個func占有了這個空格

  • Trailing trivia: 無

  • (

  • Leading trivia: 無

  • Trailing trivia: 無

  • )

  • Leading trivia: 無

  • Trailing trivia: 占有之后的一個空格(根據規則1)

  • {

  • Leading trivia: 無,前一個(占有了這個空格

  • Trailing trivia: 無,不占用下一個換行符(根據規則1)

  • var

  • Leading trivia: 一個換行符和兩個空格(根據規則2)

      
       // Equivalent to:
      Trivia::newlines(1) + Trivia::spaces(2)


    • Trailing trivia: 占有之后的一個空格(根據規則1)

    • x

    • Leading trivia: 無,前一個var占有了這個空格

    • Trailing trivia: 占有之后的一個空格(根據規則1)

    • =

      • Leading trivia: 無,前一個x占有了這個空格

      • Trailing trivia: 占有之后的一個空格(根據規則1)

      • 2

      • Leading trivia: 無,前一個=占有了這個空格

      • Trailing trivia: 無,不占用下一個換行符(根據規則1)

      • }

      • Leading trivia: 一個換行符(根據規則2)

      • Trailing trivia: 無

      • EOF

      • Leading trivia: 無

      • Trailing trivia: 無

      SyntaxData

      它用一些附加信息包裝RawSyntax節點:指向父節點的指針,該節點在其父節點中的位置以及緩存的子節點。可以將SyntaxData視為“具體“或“已實現”語法節點。它們代表特定的源代碼片段,具有絕對的位置,行和列號等。SyntaxData是每個Syntax節點的基礎存儲,私有的,不對外暴露。

      Syntax

      Syntax表示在葉子上帶有Token的節點樹,每個節點都有其已知子節點的訪問器,并允許通過其children屬性對子節點進行有效的迭代。

      抽象語法樹節點的類別有三個類:與聲明有關、與表達式有關、與語句有關。Swift也是一樣,只不過在實現的時候劃分更加細

      public protocol DeclSyntax: Syntax {}
      
      public protocol ExprSyntax: Syntax {}
      
      public protocol StmtSyntax: Syntax {}
      
      public protocol TypeSyntax: Syntax {}
      
      public protocol PatternSyntax: Syntax {}
      • DeclSyntax:與聲明有關,比如TypealiasDeclSyntax、ClassDeclSyntax、StructDeclSyntax、ProtocolDeclSyntax、ExtensionDeclSyntax、FunctionDeclSyntax、DeinitializerDeclSyntax、ImportDeclSyntax、VariableDeclSyntax、EnumCaseDeclSyntax等等。

      • StmtSyntax:與語句有關,比如GuardStmtSyntax、ForInStmtSyntax、SwitchStmtSyntax、DoStmtSyntax、BreakStmtSyntax、ReturnStmtSyntax等等。

      • ExprSyntax:與表達式有關,比如StringLiteralExprSyntax、IntegerLiteralExprSyntax、TryExprSyntax、FloatLiteralExprSyntax、TupleExprSyntax、DictionaryExprSyntax等等。

      • TypeSyntax:與聲明有關,表示類型,TupleTypeSyntax、FunctionTypeSyntax、DictionaryTypeSyntax、ArrayTypeSyntax、ClassRestrictionTypeSyntax、AttributedTypeSyntax等

      • PatternSyntax:與模式匹配有關

      swift中模式有以下幾種:

      • 通配符模式(WildcardPatternSyntax)

      • 標識符模式(IdentifierPatternSyntax)

      • 值綁定模式(ValueBindingPatternSyntax)

      • 元組模式(TuplePatternSyntax)

      • 枚舉用例模式(EnumCasePatternSyntax)

      • 可選模式(OptionalPatternSyntax)

      • 類型轉換模式(AsTypePatternSyntax)

      • 表達式模式(ExpressionPatternSyntax)

      • 未知模式(UnknownPatternSyntax)

      除了以上幾種大類型的Syntax,還有其他的Syntax:

      • SourceFileSyntax

      • FunctionParameterSyntax

      • InitializerClauseSyntax

      • MemberDeclListItemSyntax

      • MemberDeclBlockSyntax

      • TypeInheritanceClauseSyntax

      • InheritedTypeSyntax

      • ......

      SyntaxNode

      表示語法樹中的節點。這是比Syntax更有效的表示形式,因為它避免了對表示父層次結構的Syntax的強制轉換。它提供一般信息,例如節點的位置,范圍和uniqueIdentifier,同時在必要時仍允許獲取關聯的Syntax對象。SyntaxParser使用SyntaxNode來有效地報告在增量重新解析期間重新使用了哪些語法節點。

      示例:{return 1}

      這是{return 1}示例圖的樣子。

      16de98aca165d8b4.webp.jpg

      • 綠色:RawSyntax類型(TokenSyntax也是RawSyntax),這個圖圖是從Syntax拿來的,圖中RawTokenSyntax在SwiftSyntax是TokenSyntax。

      • 紅色:SyntaxData類型

      • 藍色:Syntax類型

      • 灰色:Trivia

      • 實心箭頭:強引用

      • 虛線箭頭:弱引用

      SwiftSyntax API

      Make APIs

      let returnKeyword = SyntaxFactory.makeReturnKeyword(trailingTrivia: .spaces(1))
      let three = SyntaxFactory.makeIntegerLiteralExpr(digits: SyntaxFactory.makeIntegerLiteral(String(3)))
      let returnStmt = SyntaxFactory.makeReturnStmt(returnKeyword: returnKeyword, expression: three)

      輸出

      return 3

      With APIs

      with API用于將節點轉換為其他節點。 假設我們不返回3,而是希望語句返回“hello”。我們將使用expression方法來調用它,然后傳入字符串。

      let returnHello = returnStmt.withExpression(SyntaxFactory.makeStringLiteralExpr("Hello"))

      Syntax Builders

      對于每種語法,都有一個對應的構建器結構。這些提供了一種構建語法節點的增量方法。如果我們想從頭開始構建該cat結構,只需要四個Token,struct關鍵字,cat標識符和兩個大括號。

      let structKeyword = SyntaxFactory.makeStructKeyword(trailingTrivia: .spaces(1))
      let identifier = SyntaxFactory.makeIdentifier("Cat", 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)
      }

      SyntaxVisitors

      使用SyntaxVisitor,我們可以遍歷語法樹。當我們想要提取一些信息以對源代碼進行分析時,這很有用。

      class FindPublicExtensionDeclVisitor: SyntaxVisitor {
      
          func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
              if node.modifiers?.contains(where: { $0.name.tokenKind == .publicKeyword }) == true {
                  // Do something if you find a `public extension` declaration.
              }
              return .skipChildren
          }
      }

      返回值是一種延續類型,指示是繼續并訪問語法樹上的子節點(.visitChildren)還是跳過它(.skipChildren)

      public enum SyntaxVisitorContinueKind {
      
        /// The visitor should visit the descendents of the current node.
        case visitChildren
      
        /// The visitor should avoid visiting the descendents of the current node.
        case skipChildren
      }

      SyntaxRewriters

      SyntaxRewriter使我們可以通過僅重寫visit方法并基于規則返回新節點來修改樹的結構。
      注意:所有節點都是不可變的,因此我們不修改節點,而是創建另一個節點并將其返回以替換當前節點。

      class PigRewriter: SyntaxRewriter {
      
          override func visit(_ token: TokenSyntax) -> Syntax {
              guard case .stringLiteral = token.tokenKind else { return token }
              return token.withKind(.stringLiteral("\"豬\""))
          }
      }

      在這個例子中,我們將代碼中的所有字符串替換成表情,官方示例是將所有數字加一,感興趣可以去github上看看。

      SwiftSyntax的使用

      生成代碼

      通常我們不會用SwiftSyntax來大量生成代碼,因為這需要寫大量代碼,工作量巨大,簡直讓人崩潰。我們可以用GYBGYB(模板生成)是一個 Swift 內部使用的工具,可以用模板生成源文件。Swift標準庫中的源碼的很多代碼就是用GYB生成的,其實SwiftSyntax很多代碼也是用GYB生成的,包括SyntaxBuilders、SyntaxFactory、SyntaxRewriter等等。開源社區中另一個很棒的工具是 Sourcery,它允許你在Swift(通過Stencil)而不是Python中編寫模板,SwiftGen也是使用Stencil生成Swift代碼的。

      分析和轉換代碼

      現在有兩個不錯的庫在使用SwiftSyntax,一個是periphery,檢測未使用的Swift代碼,比如未使用的Protocol和類,以及他們的方法和方法參數等等。另一個是SwiftRewriter,Swift代碼格式化工具。你也可以寫一個Swift語法高亮工具,SwiftGG有這么個例子。

      參考:

      Improving Swift Tools with libSyntax
      An overview of SwiftSyntax
      Swift編譯器結構分析
      libSyntax 編程語言的實現,從AST(抽象語法樹)開始

      广东26选5开奖结果查 微帮平台靠什么赚钱6 第一次做生意的人做什么赚钱 河南22选5 问道手游怎么小号赚钱吗 68彩票游戏 20版本酷划赚钱吗 电竞比分网666 人工挖孔桩成本赚钱 澳门华体足球指数 体球七星彩开奖结果 什么是足球指数 湖南棋牌捕鱼平台 旅游景点卖点什么好赚钱 竞彩篮球大小分 抖音点赞赚钱业务 58彩票首页