TypeScript 原始碼詳細解讀(4)語法1-語法樹
在上一節介紹了標記的解析,就相當於識別了一句話裡有哪些詞語,接下來就是把這些詞語組成完整的句子,即拼裝標記為語法樹。
樹(tree)
樹是計算機資料結構裡的專業術語。就像一個學校有很多年級,每個年級下面有很多班,每個班級下面有很多學生,這種組織結構就叫樹。
- 組成樹的每個部分稱為節點(Node);
- 最頂層的節點(即例子中的學校)稱為根節點(Root Node);
- 和每個節點的下級節點稱為這個節點的子節點(Child Node,注意不叫 Subnode)(班級是年級的子節點);
- 反過來,每個節點的上級節點稱為這個節點的父節點(Parent node)(年級是班級的父節點);
- 一個節點的子節點以及子節點的子節點統稱為這個節點的後代節點(Descendant node);
- 一個節點的父節點以及父節點的父節點統稱為這個節點的祖父節點(Ancestor node)。
很多人一提到樹就想起二叉樹,說明你壓根不懂什麼是樹。二叉樹只是樹的一種。二叉樹被用的最多的地方在試卷,請忘掉這個詞。
從樹中的任一個節點開始,都可以遍歷這個節點的所有後代節點。因為節點不會出現迴圈關係,所以遍歷樹也不會出現死迴圈。
遍歷節點的順序有有很多,沒特別說明的話,是按照先父節點、再子節點,同級節點則從左到右的順序(圖中編號順序)。
語法樹(Syntax Tree)
語法樹用於表示解析之後的程式碼結構的一種樹。
比如以下程式碼解析後的語法樹如圖:
var x = ['l', [100]] if (x) { foo(x) }
其中,原始檔(Source File)是語法樹的根節點。
語法樹中有很多種類的節點,根據種類的不同,這些節點的子節點種類也會變化。比如:
- “if 語句”節點,只有“條件表示式”、“則部分”和“否則部分”(可能為空)三個子節點。
- “雙目表示式(x + y)”節點,只有“左值表示式”和“右值表示式”兩個子節點。
TypeScript 中,節點有約 100 種,它們都繼承 “Node” 介面:
export interface Node extends TextRange { kind: SyntaxKind; flags: NodeFlags; parent: Node; // ...(略) }
Node 介面中 kind 列舉用於標記這個節點的種類。
TypeScript 將表示標記種類的列舉和表示節點種類的列舉合併成一個了(這可能也是導致很多人讀不懂程式碼的原因之一):
export const enum SyntaxKind { // ...(略) TemplateSpan, SemicolonClassElement, // Element Block, EmptyStatement, VariableStatement, ExpressionStatement, IfStatement, DoStatement, WhileStatement, ForStatement, ForInStatement, ForOfStatement, ContinueStatement, BreakStatement, // ...(略) }
如果你深知“學而不思則罔”的道理,現在應該會思考這樣一個問題:那到底有哪 100 種語法節點呢?
這裡先推薦一個工具:https://astexplorer.net/
這個工具可以在左側輸入程式碼,右側檢視實時生成的語法樹(以 JSON 方式展示)。讀者可以在這個工具頂部選擇“JavaScript”語言和“typescript”編譯器,檢視 TypeScript 生成的語法樹結構。
為了幫助英文文盲們更好地理解語法型別,讀者可參考:https://github.com/meriyah/meriyah/wiki/ESTree-Node-Types-Table
語法節點分類
雖然語法節點種類很多,但其實只有四類:
- 型別節點(Type Node):一般出現在“:”後面(var a: 型別節點),可以解析為一個型別。
- 表示式節點(Expression):可以計算得到一個值的節點,表示式節點只能依附於一個語句節點,不能獨立使用。
- 語句節點(Statement):可以直接在最外層使用的節點,俗稱的幾行程式碼就是指幾個語句節點。
- 其它節點:其它內嵌在表示式或語句節點的特定節點,比如 case 節點。
在 TypeScript 中,節點命名比較規範,一般型別節點以 TypeNode 結尾;表示式節點以 Expression 結尾;語句節點以 Statement 結尾。
比如 if 語句節點:
export interface IfStatement extends Statement { kind: SyntaxKind.IfStatement; expression: Expression; thenStatement: Statement; elseStatement?: Statement; }
鑑於有些讀者對部分語法比較陌生,這裡可以說明一些可能未正確理解的節點型別
表示式語句(ExpressionStatement)
export interface ExpressionStatement extends Statement, JSDocContainer { kind: SyntaxKind.ExpressionStatement; expression: Expression; }
表示式是不能直接出現在最外層的,但以下程式碼是允許的:
var x = 1; 1 + 1; // 這是表示式
因為 1 + 1 是表示式,它們同時又是一個表示式語句。所以以上程式碼的語法樹如圖:
常見的賦值、函式呼叫語句都其實是一個表示式語句。
塊語句(BlockStatement)
一對“{}”本身也是一個語句,稱為塊語句。一個塊語句可以包含若干個語句。
export interface Block extends Statement { kind: SyntaxKind.Block; statements: NodeArray<Statement>; /*@internal*/ multiLine?: boolean; }
比如 while 語句的主體只能是一條語句:
export interface IterationStatement extends Statement { statement: Statement; } export interface WhileStatement extends IterationStatement { kind: SyntaxKind.WhileStatement; expression: Expression; }
但 while 裡面明明是可以寫很多語句的:
while(x_d) { var a = 120; var b = 100; }
本質上,當我們使用 {} 時,就已經使用了一個塊語句,while 的主體仍然是一個語句:塊語句。其它語句都是塊語句的子節點。
標籤語句(LabeledStatement)
export interface LabeledStatement extends Statement, JSDocContainer { kind: SyntaxKind.LabeledStatement; label: Identifier; statement: Statement; }
通過標籤語句可以為語句命名,比如:
label: var x = 120;
命名後有啥用?可以在 break 或 continue 中引用該名稱,以此實現跨級 break 和 continue 的效果:
export interface BreakStatement extends Statement { kind: SyntaxKind.BreakStatement; label?: Identifier; // 跳轉的標籤名 } export interface ContinueStatement extends Statement { kind: SyntaxKind.ContinueStatement; label?: Identifier; // 跳轉的標籤名 }
運算子的優先順序
比如 x + y * z 中,需要先算乘號。生成的語法樹節點如下:
通過節點的層次關係,實現了這種優先順序的效果(因為永遠不會把圖裡的 x 和 y 先處理)。
因此建立語法樹的同時,也就處理了優先順序的問題,括號完全可以從語法樹中刪除。
類
一個複雜的類,也能用語法樹表示?
當然,任何語法最後都是用語法樹表達的,只不過類確實複雜一些:
export interface Declaration extends Node { _declarationBrand: any; } export interface NamedDeclaration extends Declaration { name?: DeclarationName; } export interface ClassLikeDeclarationBase extends NamedDeclaration, JSDocContainer { kind: SyntaxKind.ClassDeclaration | SyntaxKind.ClassExpression; name?: Identifier; typeParameters?: NodeArray<TypeParameterDeclaration>; heritageClauses?: NodeArray<HeritageClause>; members: NodeArray<ClassElement>; } export interface ClassDeclaration extends ClassLikeDeclarationBase, DeclarationStatement { kind: SyntaxKind.ClassDeclaration; /** May be undefined in `export default class { ... }`. */ name?: Identifier; }
類、函式、變數、匯入宣告嚴格意義上是獨立的一種語法分類,但鑑於它和其它語句用法一致,為了便於理解,這裡把宣告作語句的一種看待。
節點位置
當原始碼被解析成語法樹後,原始碼就不再需要了。如果後續流程發現一個錯誤,編譯器需要向用戶報告,並指出錯誤位置。
為了可以得到這個位置,需要將節點在原始檔種的位置儲存下來:
export interface TextRange { pos: number; end: number; }
export interface Node extends TextRange { kind: SyntaxKind; flags: NodeFlags; parent: Node; // ...(略) }
通過節點的 parent 可以找到節點的根節點,即所在的檔案;通過節點的 pos 和 end 可以確定節點在原始檔的行列號(具體已經在第二節:標記位置 中介紹)。
遍歷節點
為了方便程式中遍歷任意節點,TypeScript 提供了一個工具函式:
/** * Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes * stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; otherwise, * embedded arrays are flattened and the 'cbNode' callback is invoked for each element. If a callback returns * a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned. * * @param node a given node to visit its children * @param cbNode a callback to be invoked for all child nodes * @param cbNodes a callback to be invoked for embedded array * * @remarks `forEachChild` must visit the children of a node in the order * that they appear in the source code. The language service depends on this property to locate nodes by position. */ export function forEachChild<T>(node: Node, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined { if (!node || node.kind <= SyntaxKind.LastToken) { return; } switch (node.kind) { case SyntaxKind.QualifiedName: return visitNode(cbNode, (<QualifiedName>node).left) || visitNode(cbNode, (<QualifiedName>node).right); case SyntaxKind.TypeParameter: return visitNode(cbNode, (<TypeParameterDeclaration>node).name) || visitNode(cbNode, (<TypeParameterDeclaration>node).constraint) || visitNode(cbNode, (<TypeParameterDeclaration>node).default) || visitNode(cbNode, (<TypeParameterDeclaration>node).expression); case SyntaxKind.ShorthandPropertyAssignment: return visitNodes(cbNode, cbNodes, node.decorators) || visitNodes(cbNode, cbNodes, node.modifiers) || visitNode(cbNode, (<ShorthandPropertyAssignment>node).name) || visitNode(cbNode, (<ShorthandPropertyAssignment>node).questionToken) || // ...(略) } }
forEachChild 函式只會遍歷節點的直接子節點,如果使用者需要遞迴遍歷所有子節點,需要遞迴呼叫 forEachChild。forEachChild 接收一個函式用於遍歷,並允許使用者返回一個 true 型的值並終止迴圈。
小結
掌握語法樹是掌握整個編譯系統的基礎。你應該可以深刻地知道語法樹的大概樣子,並清楚每種語法的語法樹結構。如果還沒有徹底掌握,可以使用上文推薦的工具。
下一節將介紹生成語法樹的全過程。【不定時更新】<