[Node] 淡如止水 TypeScript (六):類型檢查

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)用了 emitFilesAndReportErrorssrc/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.getSemanticDiagnosticssrc/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)用者為 getDiagnosticsWorkersrc/compiler/checker.ts#L33100前翎,

function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {
  ...
  if (sourceFile) {
    ...
    checkSourceFile(sourceFile);
    ...
  }
  ...
}

為了獲取診斷信息勺拣,它調(diào)用了 checkSourceFilesrc/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判莉,
賦值給targetTypenumber 的 type 時(shí)出錯(cuò)了育谬。

message 的值為 Type '{0}' is not assignable to type '{1}'.
sourceTypetargetType 填充后為膛檀,

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" 的值賦值給類型為 numberi 時(shí)磕潮,報(bào)錯(cuò)了。


總結(jié)

在本文中自脯,我們?cè)?debug/index.ts 中構(gòu)造了一個(gè)類型錯(cuò)誤,
然后順藤摸瓜膏潮,通過(guò)調(diào)用棧信息,反查了整條鏈路轻纪。

總結(jié)如下,TypeScript 在 performCompilation 中做了兩件事情刻帚,
createProgramemitFilesAndReportErrorsAndGetExitStatus
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摹芙。

參考

TypeScript v3.7.3

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灼狰,一起剝皮案震驚了整個(gè)濱河市浮禾,隨后出現(xiàn)的幾起案子交胚,更是在濱河造成了極大的恐慌盈电,老刑警劉巖蝴簇,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匆帚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡互拾,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)嚎幸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人嫉晶,你說(shuō)我怎么就攤上這事田篇」棵” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵兽赁,是天一觀的道長(zhǎng)柄瑰。 經(jīng)常有香客問(wèn)我剪况,道長(zhǎng),這世上最難降的妖魔是什么译断? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任孙咪,我火速辦了婚禮,結(jié)果婚禮上翎蹈,老公的妹妹穿的比我還像新娘。我一直安慰自己合陵,他們只是感情好澄阳,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著低剔,像睡著了一般肮塞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上枕赵,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天烁设,我揣著相機(jī)與錄音钓试,去河邊找鬼副瀑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛糠睡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播信认,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼均抽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了潦蝇?” 一聲冷哼從身側(cè)響起深寥,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎则酝,沒(méi)想到半個(gè)月后闰集,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妥泉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年洞坑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刽沾。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡排拷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出布蔗,到底是詐尸還是另有隱情,我是刑警寧澤纵揍,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布泽谨,位于F島的核電站,受9級(jí)特大地震影響吧雹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜搓蚪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一龙亲、第九天 我趴在偏房一處隱蔽的房頂上張望悍抑。 院中可真熱鬧,春花似錦搜骡、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至呀洲,卻和暖如春啼止,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背献烦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吏夯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓舶替,卻偏偏與公主長(zhǎng)得像杠园,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抛蚁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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