0. 回顧
上文我們介紹了 TypeScript 處理語(yǔ)法錯(cuò)誤的代碼邏輯早抠,
是在 parseXXX
函數(shù)中,遇到期望之外的情況時(shí)撬讽,跑到額外的分支來(lái)處理錯(cuò)誤的蕊连。
這個(gè)過(guò)程發(fā)生在 AST 的創(chuàng)建過(guò)程,即锐秦,發(fā)生在 parseList
調(diào)用鏈路上咪奖。
我們知道 TypeScript 源碼的宏觀結(jié)構(gòu)盗忱,可簡(jiǎn)寫(xiě)如下酱床,
performCompilation // 執(zhí)行編譯
createProgram // 創(chuàng)建 Program 對(duì)象
Parser.parseSourceFile // 每個(gè)文件單獨(dú)解析,創(chuàng)建 SourceFile 對(duì)象
parseList // 返回一個(gè) AST
emitFilesAndReportErrorsAndGetExitStatus
語(yǔ)法錯(cuò)誤的處理趟佃,仍然發(fā)生在 createProgram
中扇谣。
本文開(kāi)始分析類型錯(cuò)誤昧捷,它發(fā)生在了 AST 創(chuàng)建之后的 emitFilesAndReportErrorsAndGetExitStatus
中。
1. 類型檢查
與上一篇類似罐寨,我們先構(gòu)造一個(gè)類型錯(cuò)誤靡挥,然后再通過(guò)報(bào)錯(cuò)信息,找到調(diào)用棧鸯绿。
我們修改 debug/index.ts
文件如下跋破,
const i: number = '1';
把 i
的值從數(shù)字 1
改成了字符串 '1'
。
編譯結(jié)果瓶蝴,
$ node bin/tsc debug/index.ts
debug/index.ts:1:7 - error TS2322: Type '"1"' is not assignable to type 'number'.
1 const i: number = '1';
~
Found 1 error.
錯(cuò)誤碼為 2322
毒返,TypeScript src/
目錄搜到的錯(cuò)誤 key 為 Type_0_is_not_assignable_to_type_1
,
src/compiler/diagnosticInformationMap.generated.ts#L299
用到這個(gè) key 的位置在這里 src/compiler/checker.ts#L14486舷手,
reportRelationError
函數(shù)中拧簸,
function reportRelationError(message: DiagnosticMessage | undefined, source: Type, target: Type) {
...
if (!message) {
if (relation === comparableRelation) {
...
}
else if (sourceType === targetType) {
...
}
else {
message = Diagnostics.Type_0_is_not_assignable_to_type_1;
}
}
...
}
啟動(dòng)調(diào)試,程序順利的停在了斷點(diǎn)處男窟,
我們看到左側(cè)的調(diào)用棧盆赤,非常的陌生,這對(duì)我們來(lái)說(shuō)是一個(gè)陌生的代碼分支歉眷。
最下面的一個(gè)函數(shù)是 getSemanticDiagnostics
牺六,src/compiler/program.ts#L1665。
2. 跟蹤調(diào)用棧
我們往下翻閱姥芥,查看調(diào)用棧信息兔乞,好在沒(méi)有翻動(dòng)多少,就看到了我們熟悉的函數(shù)了凉唐,
以下我們記錄了一下調(diào)用棧信息庸追,值得注意的是,調(diào)用順序?yàn)榈剐颍?br> 最底層的函數(shù)台囱,最先觸發(fā)淡溯,最上層的函數(shù),越晚被調(diào)用簿训。
reportRelationError
...
getSemanticDiagnostics
emitFilesAndReportErrors
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...
performCompilation
咱娶,src/tsc/executeCommandLine.ts#L493,
function performCompilation(
...
) {
...
const program = createProgram(programOptions);
const exitStatus = emitFilesAndReportErrorsAndGetExitStatus(
...
);
...
}
先是調(diào)用了 emitFilesAndReportErrorsAndGetExitStatus
强品,src/compiler/watch.ts#L200膘侮,
export function emitFilesAndReportErrorsAndGetExitStatus(
...
) {
const { emitResult, diagnostics } = emitFilesAndReportErrors(
...
);
...
}
接著又調(diào)用了 emitFilesAndReportErrors
,src/compiler/watch.ts#L142的榛,
export function emitFilesAndReportErrors(
...
) {
...
addRange(diagnostics, program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken));
...
if (diagnostics.length === configFileParsingDiagnosticsLength) {
addRange(diagnostics, program.getOptionsDiagnostics(cancellationToken));
if (!isListFilesOnly) {
addRange(diagnostics, program.getGlobalDiagnostics(cancellationToken));
if (diagnostics.length === configFileParsingDiagnosticsLength) {
addRange(diagnostics, program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken));
}
}
}
...
}
這個(gè)函數(shù)中進(jìn)行了多種檢查琼了,
program.getSyntacticDiagnostics
program.getOptionsDiagnostics
program.getGlobalDiagnostics
program.getSemanticDiagnostics
類型檢查發(fā)生在 program.getSemanticDiagnostics
,src/compiler/program.ts#L1665,
后面就不再贅述了雕薪,我們只挑選一些關(guān)鍵節(jié)點(diǎn)來(lái)閱讀代碼昧诱。
沿著調(diào)用棧向上查找,我們看到了一個(gè)關(guān)鍵函數(shù) checkSourceFile
所袁,
它是對(duì) SourceFile
對(duì)象進(jìn)行檢查的盏档。
reportRelationError
...
checkSourceFileWorker
checkSourceFile
getDiagnosticsWorker
...
getSemanticDiagnostics
emitFilesAndReportErrors
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...
3. checkSourceFile
首先,我們來(lái)看 checkSourceFile
燥爷,是如何被調(diào)用的蜈亩,
它的調(diào)用者為 getDiagnosticsWorker
,src/compiler/checker.ts#L33100前翎,
function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {
...
if (sourceFile) {
...
checkSourceFile(sourceFile);
...
}
...
}
為了獲取診斷信息勺拣,它調(diào)用了 checkSourceFile
,src/compiler/checker.ts#L33007鱼填,
function checkSourceFile(node: SourceFile) {
performance.mark("beforeCheck");
checkSourceFileWorker(node);
performance.mark("afterCheck");
performance.measure("Check", "beforeCheck", "afterCheck");
}
這個(gè)函數(shù)中有 performance.mark
信息药有,是用來(lái)統(tǒng)計(jì)編譯性能的,
看來(lái)我們的感覺(jué)沒(méi)錯(cuò)苹丸,checkSourceFile
確實(shí)是一個(gè)關(guān)鍵函數(shù)愤惰。
現(xiàn)在我們來(lái)看一下 node
中的信息,
發(fā)現(xiàn) fileName
居然是 built/local/lib.es5.d.ts
赘理。
這不是我們要編譯的 debug/index.ts
宦言。
另一個(gè)問(wèn)題是,這種 TypeScript 內(nèi)置的文件商模,也會(huì)有類型錯(cuò)誤奠旺?
確實(shí)是有的,我們來(lái)編譯下這個(gè)文件施流,
$ node lib/tsc built/local/lib.es5.d.ts
...
Found 18 errors.
限于篇幅响疚,中間的出錯(cuò)信息就不寫(xiě)了,至少我們知道瞪醋,這個(gè)文件確實(shí)是有類型錯(cuò)誤忿晕。
4. 條件斷點(diǎn)
為了能拿到 debug/index.ts
文件的類型檢查錯(cuò)誤,
我們需要使用 VSCode 的條件斷點(diǎn)功能银受。
在 checkSourceFileWorker
被調(diào)用所在的行践盼,原來(lái)打斷點(diǎn)的位置,右鍵宾巍,
選擇 Add Conditional Breakpoint
咕幻,
然后 VSCode 會(huì)彈出一個(gè)框,我們來(lái)輸入條件顶霞,然后按回車肄程,
node.fileName === 'debug/index.ts'
行首就會(huì)出現(xiàn)一個(gè)與普通斷點(diǎn)不一樣的斷點(diǎn)了,
鼠標(biāo)移動(dòng)上去,會(huì)展示觸發(fā)條件绷耍,
現(xiàn)在我們只保留這個(gè)斷點(diǎn),啟動(dòng)調(diào)試鲜侥。
我們順利停在了 check debug/index.ts
的情況下了褂始。
5. reportRelationError
現(xiàn)在已經(jīng)在處理 debug/index.ts
了,我們也確定對(duì)它進(jìn)行類型檢查一定會(huì)報(bào)錯(cuò)描函,
$ node bin/tsc debug/index.ts
debug/index.ts:1:7 - error TS2322: Type '"1"' is not assignable to type 'number'.
1 const i: number = '1';
~
Found 1 error.
因此,我們保持程序在調(diào)試狀態(tài)下舀寓,再到 reportRelationError
打個(gè)斷點(diǎn),
位于 src/compiler/checker.ts#L14486互墓,
然后按 F5
繼續(xù)運(yùn)行。
我們看到篡撵,這是將
sourceType
為 "1"
的 type判莉,賦值給
targetType
為 number
的 type 時(shí)出錯(cuò)了育谬。
message
的值為 Type '{0}' is not assignable to type '{1}'.
。
將 sourceType
和 targetType
填充后為膛檀,
Type '"1"' is not assignable to type 'number'.
正是上文的類型檢查報(bào)錯(cuò)信息锰镀。
6. 真實(shí)調(diào)用棧
至此我們才拿到了 debug/index.ts
類型檢查出錯(cuò)的,真實(shí)調(diào)用棧信息咖刃,
我們看到在 checkSourceFile
中,進(jìn)行了一系列檢查胡桃,
reportRelationError // 報(bào)錯(cuò)
isRelatedTo // 無(wú)法賦值
checkTypeRelatedTo
checkTypeRelatedToAndOptionallyElaborate
checkTypeAssignableToAndOptionallyElaborate
checkVariableLikeDeclaration
checkVariableDeclaration
...
checkSourceElement
...
checkVariableStatement
...
checkSourceElement
...
checkSourceFile
...
在檢查是否可將類型為 "1"
的值賦值給類型為 number
的 i
時(shí)磕潮,報(bào)錯(cuò)了。
總結(jié)
在本文中自脯,我們?cè)?debug/index.ts
中構(gòu)造了一個(gè)類型錯(cuò)誤,
然后順藤摸瓜膏潮,通過(guò)調(diào)用棧信息,反查了整條鏈路轻纪。
總結(jié)如下,TypeScript 在 performCompilation
中做了兩件事情刻帚,
createProgram
和 emitFilesAndReportErrorsAndGetExitStatus
,
createProgram
進(jìn)行了語(yǔ)法檢查掂僵,
emitFilesAndReportErrorsAndGetExitStatus
進(jìn)行了類型檢查顷歌。
類型檢查的整條鏈路如下,
performCompilation
createProgram
emitFilesAndReportErrorsAndGetExitStatus
getSemanticDiagnostics
checkSourceFile
...
reportRelationError
...
TypeScript 的類型檢查器非常的復(fù)雜眯漩,我們所能看到的只是很小的一部分。
checker.ts 代碼已經(jīng)有 36198
行了冯勉,src/compiler/checker.ts#L36198摹芙。