[Node] 淡如止水 TypeScript (五):語法錯誤

0. 回顧

0.1 第一篇回顧

在第一篇中锯玛,我們克隆了 TypeScript 源碼倉庫,并配置了調(diào)試環(huán)境澳淑,
VSCode 斷點成功停在了 lib/tsc 的第一行秋冰。

我們新建了一個 debug/index.ts 文件仲义,作為編譯的源代碼,內(nèi)容如下剑勾,

const i: number = 1;

0.2 第二篇回顧

第二篇埃撵,我們從 lib/tsc 第一行往下單步調(diào)試,
解決了 require 無法跳轉(zhuǎn)到 .ts 文件的問題虽另。

我們一路跟蹤 TypeScript 從命令行工具 lib/tsc 到 parser 的過程暂刘。

lib/tsc -> ts.executeCommandLine -> executeCommandLineWorker -> performCompilation

performCompilation 包含了 TypeScript 編譯的兩個主要步驟,

createProgram: 創(chuàng)建 Program 對象
emitFilesAndReportErrorsAndGetExitStatus: 寫文件

我們就暫時略過了寫文件的邏輯捂刺,
第二篇文章的后半部分谣拣,后面第三、四篇文章族展,都在介紹 createProgram森缠。

TypeScript 每次編譯會創(chuàng)建一個 Program,但有可能會創(chuàng)建多個 SourceFile仪缸。
用于處理每一個待編譯的源文件

createProgram     // 創(chuàng)建一個 Program 對象
processRootFile   // 處理每個文件贵涵,創(chuàng)建多個 SourceFile 對象
processSourceFile
getSourceFileFromReferenceWorker
getSourceFile
findSourceFile
host.getSourceFile
createSourceFile  // 來到了 parser 中

0.3 第三篇回顧

第三篇,我們從 parser 中的 createSourceFile 開始往下執(zhí)行恰画,

createSourceFile
Parser.parseSourceFile
parseSourceFileWorker
(nextToken)
parseList

parseList 又是一個關鍵函數(shù)宾茂,它是整個語法解析器的入口。
不過語法分析我們放到了第四篇中來介紹锣尉。
第三篇中刻炒,我們跟進了 parseList 之前的 nextToken 執(zhí)行過程。

nextToken()token() 是 TypeScript 詞法分析器最常見的兩個函數(shù)自沧,
詞法分析器內(nèi)部保存了狀態(tài),token() 用來返回當前在處理的 token (的種類 SyntaxKind),
nextToken() 邏輯非常復雜拇厢,覆蓋了詞法分析掃描下一個 token 是所有細節(jié)爱谁。

簡略介紹 nextToken() 的基本原理的話,
它首先在字符流中孝偎,從當前位置访敌,向后掃描一個字符,然后分情況分析衣盾,
比如說遇到了一個英文字符 c寺旺,就認為它可能是一個標識符或關鍵字,
然后就繼續(xù)往后掃描势决,直到積累到的字符串不再構成標識符位置阻塑,比如掃描到了空格。

這樣就會掃描到一個完整的果复,合法的標識符序列陈莽,例如 const,將字符串存為 tokenValue虽抄,
然后走搁,詞法分析器會識別出這是一個關鍵字,并返回該 token 的種類為 SyntaxKind.ConstKeyword迈窟。

這就是一個簡單示例詞法分析的全過程了私植。

0.4 第四篇回顧

第四篇,我們研究了 TypeScript 的語法分析车酣,
從頂層的語法解析函數(shù) parseList 開始往下調(diào)試兵琳。

TypeScript 采用了手工編寫的遞歸下降解析方法,
AST 的創(chuàng)建過程骇径,由大量的互相調(diào)用的 parseXXX 函數(shù)來完成躯肌。

最頂層的 parseXXX 函數(shù)是 parseList,返回了 AST 的根節(jié)點破衔,
AST 的每個子樹(子節(jié)點)都由相應的 parseXXX 函數(shù)返回清女。

在解析的過程中,解析器還可能會調(diào)用 nextToken()token()晰筛,
用以獲取下一個或當前的 token嫡丙,來填充 AST 節(jié)點內(nèi)容。

parseList
  parseDeclaration
    parseVariableStatement
      parseVariableDeclarationList
        parseVariableDeclaration
          parseIdentifierOrPattern
            parseIdentifier
          parseTypeAnnotation
            parseType
          parseInitializer
            parseAssignmentExpressionOrHigher
      parseSemicolon

實際的解析鏈路會非常長读第,以上只是粗略寫了一些關鍵的 parseXXX 函數(shù)曙博。
語法分析器會通過前瞻(look ahead)來決定使用哪一個函數(shù)進行解析。
即怜瞒,通過自頂向下構造 AST 的方式父泳,實現(xiàn)了產(chǎn)生式的最左推導般哼。

解析是根據(jù)給定的文法,結構化一段線性表示的過程惠窄。
TypeScript 語法分析蒸眠,最終目的是創(chuàng)建一棵 AST。

小結

以上我們回顧了前四篇文章的內(nèi)容杆融,有幾個關鍵點需要一覽楞卡,

performCompilation          // 執(zhí)行編譯
  createProgram             // 創(chuàng)建 Program 對象
    Parser.parseSourceFile  // 每個文件單獨解析,創(chuàng)建 SourceFile 對象
      parseList             // 返回一個 AST
  emitFilesAndReportErrorsAndGetExitStatus

前四篇中脾歇,我們已經(jīng)對 createProgram 的流程打探清楚了蒋腮,
從本文開始,我們來 emitFilesAndReportErrorsAndGetExitStatus藕各。

其中包含了語法檢查池摧,語義檢查,寫文件等等業(yè)務邏輯座韵。

1. 回溯到 performCompilation

書接上文险绘,第四篇中我們已經(jīng)了解了 parseList
它執(zhí)行完之后返回到了誉碴,parseSourceFileWorker 函數(shù)中宦棺,位于 src/compiler/parser.ts#L858

function parseSourceFileWorker(...): SourceFile {
  ...

  sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);
  Debug.assert(token() === SyntaxKind.EndOfFileToken);
  
  ...
  return sourceFile;
}

看到 parseList 返回后黔帕,后面一句的判斷代咸,當前 token 就已經(jīng)是文件結尾了。

完整的調(diào)用鏈路是這樣的成黄,

performCompilation
  createProgram
    forEach processRootFile
      processSourceFile
        getSourceFileFromReferenceWorker
          getSourceFile
            findSourceFile
              host.getSourceFile
                createSourceFile
                  Parser.parseSourceFile
                    parseSourceFileWorker
                      parseList
  emitFilesAndReportErrorsAndGetExitStatus

就這樣我們一路回到了 performCompilation呐芥。
在回溯過程中,forEach processRootFile 還會處理另外一些 TypeScript 內(nèi)置的 .d.ts 文件奋岁,
這里就暫且略過不寫了思瘟。

createProgram 完了之后,TypeScript 就開始執(zhí)行 emitFilesAndReportErrorsAndGetExitStatus 了闻伶,
調(diào)用位置位于 src/tsc/executeCommandLine.ts#L515

function performCompilation(
  ...
) {
  ...
  const program = createProgram(programOptions);
  const exitStatus = emitFilesAndReportErrorsAndGetExitStatus(
      ...
  );
  ...
}

2. 語法錯誤

2.1 全局搜索報錯位置

我們知道 TypeScript 在編譯的時候滨攻,會提示各種可能的錯誤,例如語法錯誤蓝翰、類型錯誤光绕。
從頭跟蹤編譯過程,然后找到哪里出錯畜份,是一件很繁瑣的事情诞帐。

為此,我們可以先構造一個錯誤爆雹,然后在 TypeScript 報錯的位置打個斷點停蕉,
再通過 VSCode 反查調(diào)用棧愕鼓,得到出錯的鏈路信息。

修改 debug/index.ts 文件的內(nèi)容如下谷徙,

const 0

然后命令行調(diào)用 lib/tsc 編譯一下拒啰,

$ node bin/tsc debug/index.ts
debug/index.ts:1:7 - error TS1134: Variable declaration expected.

1 const 0
        ~


Found 1 error.

得到了以上報錯信息驯绎。

現(xiàn)在完慧,我們就可以在 TypeScript src/ 文件夾下搜索錯誤碼 1134 了。

搜到了 src/compiler/diagnosticInformationMap.generated.ts#L110 位置的剩失,
Variable_declaration_expected屈尼,它是錯誤信息的 key 值。
根據(jù)這個錯誤信息拴孤,我們就可以找出脾歧,代碼中哪里拋出了這個錯誤。


我們在這個位置打個斷點演熟。

src/compiler/parser.ts#L2095鞭执,

function parsingContextErrors(context: ParsingContext): DiagnosticMessage {
  switch (context) {
    ...
    case ParsingContext.VariableDeclarations: return Diagnostics.Variable_declaration_expected;
    ...
  }
}

啟動調(diào)試,程序會停在斷點處芒粹,


2.2 調(diào)用棧

我們可以看到 VSCode 左側(cè)的調(diào)用棧信息兄纺,


有一些步驟似曾相識,我們點開來看化漆,
parseDelimitedList 之前都是前幾篇文章中估脆,我們已經(jīng)分析過的內(nèi)容。

parseVariableDeclarationList座云,src/compiler/parser.ts#L5763疙赠,
識別出了 const 關鍵字,正要開始解析后面的 node.declarations 部分朦拖。

function parseVariableDeclarationList(inForStatementInitializer: boolean): VariableDeclarationList {
  const node = <VariableDeclarationList>createNode(SyntaxKind.VariableDeclarationList);

  switch (token()) {
    ...
    case SyntaxKind.ConstKeyword:
      node.flags |= NodeFlags.Const;
      break;
    ...
  }

  nextToken();

  ...
  if (token() === SyntaxKind.OfKeyword && lookAhead(canFollowContextualOfKeyword)) {
    ...
  }
  else {
    ...
    node.declarations = parseDelimitedList(ParsingContext.VariableDeclarations,
      inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation);
    ...
  }

  ...
}

為此調(diào)用了 parseDelimitedList圃阳,src/compiler/parser.ts#L2115

function parseDelimitedList<T extends Node>(...): NodeArray<T> {
  ...

  while (true) {
    if (isListElement(kind, /*inErrorRecovery*/ false)) {
      ...
      list.push(parseListElement(kind, parseElement));
      ...
    }

    ...

    if (abortParsingListOrMoveToNextToken(kind)) {
      break;
    }
  }

  ...
  const result = createNodeArray(list, listPos);
  ...
  return result;
}

正常的 parseDelimitedList 會調(diào)用 parseListElement 完成后續(xù)的解析璧帝。
但此時 isListElement 的判斷失敗了捍岳,因此走到了 abortParsingListOrMoveToNextToken 函數(shù)中來。

src/compiler/parser.ts#L2074

function abortParsingListOrMoveToNextToken(kind: ParsingContext) {
  parseErrorAtCurrentToken(parsingContextErrors(kind));
  ...
}

接著調(diào)用了 parsingContextErrors裸弦,src/compiler/parser.ts#L2084祟同,

function parsingContextErrors(context: ParsingContext): DiagnosticMessage {
  switch (context) {
    ...
    case ParsingContext.VariableDeclarations: return Diagnostics.Variable_declaration_expected;
    ...
  }
}

這就是上文搜索到包含錯誤 key Variable_declaration_expected 的函數(shù)了。

因此理疙,TypeScript 在解析過程中晕城,如果遇到了預期之外的 token,
就會跑到錯誤處理分支窖贤,根據(jù)錯誤 key 來記錄錯誤消息砖顷。


總結

本文簡單展示了 TypeScript 處理語法錯誤的代碼邏輯贰锁,實際處理過程會更加的復雜,
包括這些錯誤信息如何最終展示到控制臺中滤蝠,
以及錯誤消息的格式化展示問題豌熄。

但我覺得目前來看,這件事不是特別的緊急物咳,因此先放下锣险,等有機會再詳細了解它。

參考

TypeScript v3.7.3

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末览闰,一起剝皮案震驚了整個濱河市芯肤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌压鉴,老刑警劉巖崖咨,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異油吭,居然都是意外死亡击蹲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門婉宰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來歌豺,“玉大人,你說我怎么就攤上這事芍阎∈涝” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵谴咸,是天一觀的道長轮听。 經(jīng)常有香客問我,道長岭佳,這世上最難降的妖魔是什么血巍? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮珊随,結果婚禮上述寡,老公的妹妹穿的比我還像新娘。我一直安慰自己叶洞,他們只是感情好鲫凶,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著衩辟,像睡著了一般螟炫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艺晴,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天昼钻,我揣著相機與錄音掸屡,去河邊找鬼。 笑死然评,一個胖子當著我的面吹牛仅财,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碗淌,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼盏求,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贯莺?” 一聲冷哼從身側(cè)響起风喇,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤宁改,失蹤者是張志新(化名)和其女友劉穎缕探,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體还蹲,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡爹耗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谜喊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片潭兽。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖斗遏,靈堂內(nèi)的尸體忽然破棺而出山卦,到底是詐尸還是另有隱情,我是刑警寧澤诵次,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布账蓉,位于F島的核電站,受9級特大地震影響逾一,放射性物質(zhì)發(fā)生泄漏铸本。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一遵堵、第九天 我趴在偏房一處隱蔽的房頂上張望箱玷。 院中可真熱鬧,春花似錦陌宿、人聲如沸锡足。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舶得。三九已至,卻和暖如春弥虐,著一層夾襖步出監(jiān)牢的瞬間扩灯,已是汗流浹背媚赖。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留珠插,地道東北人惧磺。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像捻撑,于是被迫代替她去往敵國和親磨隘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容