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ù)中來。
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 處理語法錯誤的代碼邏輯贰锁,實際處理過程會更加的復雜,
包括這些錯誤信息如何最終展示到控制臺中滤蝠,
以及錯誤消息的格式化展示問題豌熄。
但我覺得目前來看,這件事不是特別的緊急物咳,因此先放下锣险,等有機會再詳細了解它。