0. 回顾

上文我们介绍了 TypeScript 处理语法错误的代码逻辑,
是在 parseXXX 函数中,遇到期望之外的情况时,跑到额外的分支来处理错误的。
这个过程发生在 AST 的创建过程,即,发生在 parseList 调用链路上。

我们知道 TypeScript 源码的宏观结构,可简写如下,

  1. performCompilation // 执行编译
  2. createProgram // 创建 Program 对象
  3. Parser.parseSourceFile // 每个文件单独解析,创建 SourceFile 对象
  4. parseList // 返回一个 AST
  5. emitFilesAndReportErrorsAndGetExitStatus

语法错误的处理,仍然发生在 createProgram 中。
本文开始分析类型错误,它发生在了 AST 创建之后的 emitFilesAndReportErrorsAndGetExitStatus 中。

1. 类型检查

与上一篇类似,我们先构造一个类型错误,然后再通过报错信息,找到调用栈。
我们修改 debug/index.ts 文件如下,

  1. const i: number = '1';

i 的值从数字 1 改成了字符串 '1'

编译结果,

  1. $ node bin/tsc debug/index.ts
  2. debug/index.ts:1:7 - error TS2322: Type '"1"' is not assignable to type 'number'.
  3. 1 const i: number = '1';
  4. ~
  5. Found 1 error.

错误码为 2322,TypeScript src/ 目录搜到的错误 key 为 Type_0_is_not_assignable_to_type_1
src/compiler/diagnosticInformationMap.generated.ts#L299

用到这个 key 的位置在这里 src/compiler/checker.ts#L14486
reportRelationError 函数中,

  1. function reportRelationError(message: DiagnosticMessage | undefined, source: Type, target: Type) {
  2. ...
  3. if (!message) {
  4. if (relation === comparableRelation) {
  5. ...
  6. }
  7. else if (sourceType === targetType) {
  8. ...
  9. }
  10. else {
  11. message = Diagnostics.Type_0_is_not_assignable_to_type_1;
  12. }
  13. }
  14. ...
  15. }

启动调试,程序顺利的停在了断点处,
淡如止水 TypeScript (六):类型检查 - 图1

我们看到左侧的调用栈,非常的陌生,这对我们来说是一个陌生的代码分支。
最下面的一个函数是 getSemanticDiagnosticssrc/compiler/program.ts#L1665

2. 跟踪调用栈

我们往下翻阅,查看调用栈信息,好在没有翻动多少,就看到了我们熟悉的函数了,
淡如止水 TypeScript (六):类型检查 - 图2

以下我们记录了一下调用栈信息,值得注意的是,调用顺序为倒序,
最底层的函数,最先触发,最上层的函数,越晚被调用。

  1. reportRelationError
  2. ...
  3. getSemanticDiagnostics
  4. emitFilesAndReportErrors
  5. emitFilesAndReportErrorsAndGetExitStatus
  6. performCompilation
  7. ...

performCompilationsrc/tsc/executeCommandLine.ts#L493

  1. function performCompilation(
  2. ...
  3. ) {
  4. ...
  5. const program = createProgram(programOptions);
  6. const exitStatus = emitFilesAndReportErrorsAndGetExitStatus(
  7. ...
  8. );
  9. ...
  10. }

先是调用了 emitFilesAndReportErrorsAndGetExitStatussrc/compiler/watch.ts#L200

  1. export function emitFilesAndReportErrorsAndGetExitStatus(
  2. ...
  3. ) {
  4. const { emitResult, diagnostics } = emitFilesAndReportErrors(
  5. ...
  6. );
  7. ...
  8. }

接着又调用了 emitFilesAndReportErrorssrc/compiler/watch.ts#L142

  1. export function emitFilesAndReportErrors(
  2. ...
  3. ) {
  4. ...
  5. addRange(diagnostics, program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken));
  6. ...
  7. if (diagnostics.length === configFileParsingDiagnosticsLength) {
  8. addRange(diagnostics, program.getOptionsDiagnostics(cancellationToken));
  9. if (!isListFilesOnly) {
  10. addRange(diagnostics, program.getGlobalDiagnostics(cancellationToken));
  11. if (diagnostics.length === configFileParsingDiagnosticsLength) {
  12. addRange(diagnostics, program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken));
  13. }
  14. }
  15. }
  16. ...
  17. }

这个函数中进行了多种检查,

  1. program.getSyntacticDiagnostics
  2. program.getOptionsDiagnostics
  3. program.getGlobalDiagnostics
  4. program.getSemanticDiagnostics

类型检查发生在 program.getSemanticDiagnosticssrc/compiler/program.ts#L1665
后面就不再赘述了,我们只挑选一些关键节点来阅读代码。

沿着调用栈向上查找,我们看到了一个关键函数 checkSourceFile
它是对 SourceFile 对象进行检查的。

  1. reportRelationError
  2. ...
  3. checkSourceFileWorker
  4. checkSourceFile
  5. getDiagnosticsWorker
  6. ...
  7. getSemanticDiagnostics
  8. emitFilesAndReportErrors
  9. emitFilesAndReportErrorsAndGetExitStatus
  10. performCompilation
  11. ...

3. checkSourceFile

首先,我们来看 checkSourceFile,是如何被调用的,
它的调用者为 getDiagnosticsWorkersrc/compiler/checker.ts#L33100

  1. function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {
  2. ...
  3. if (sourceFile) {
  4. ...
  5. checkSourceFile(sourceFile);
  6. ...
  7. }
  8. ...
  9. }

为了获取诊断信息,它调用了 checkSourceFilesrc/compiler/checker.ts#L33007

  1. function checkSourceFile(node: SourceFile) {
  2. performance.mark("beforeCheck");
  3. checkSourceFileWorker(node);
  4. performance.mark("afterCheck");
  5. performance.measure("Check", "beforeCheck", "afterCheck");
  6. }

这个函数中有 performance.mark 信息,是用来统计编译性能的,
看来我们的感觉没错,checkSourceFile 确实是一个关键函数。

现在我们来看一下 node 中的信息,
淡如止水 TypeScript (六):类型检查 - 图3

发现 fileName 居然是 built/local/lib.es5.d.ts
这不是我们要编译的 debug/index.ts
另一个问题是,这种 TypeScript 内置的文件,也会有类型错误?

确实是有的,我们来编译下这个文件,

  1. $ node lib/tsc built/local/lib.es5.d.ts
  2. ...
  3. Found 18 errors.

限于篇幅,中间的出错信息就不写了,至少我们知道,这个文件确实是有类型错误。

4. 条件断点

为了能拿到 debug/index.ts 文件的类型检查错误,
我们需要使用 VSCode 的条件断点功能。

checkSourceFileWorker 被调用所在的行,原来打断点的位置,右键,
选择 Add Conditional Breakpoint
淡如止水 TypeScript (六):类型检查 - 图4

然后 VSCode 会弹出一个框,我们来输入条件,然后按回车,
淡如止水 TypeScript (六):类型检查 - 图5

  1. node.fileName === 'debug/index.ts'

行首就会出现一个与普通断点不一样的断点了,
淡如止水 TypeScript (六):类型检查 - 图6

鼠标移动上去,会展示触发条件,
淡如止水 TypeScript (六):类型检查 - 图7

现在我们只保留这个断点,启动调试。
淡如止水 TypeScript (六):类型检查 - 图8

我们顺利停在了 check debug/index.ts 的情况下了。

5. reportRelationError

现在已经在处理 debug/index.ts 了,我们也确定对它进行类型检查一定会报错,

  1. $ node bin/tsc debug/index.ts
  2. debug/index.ts:1:7 - error TS2322: Type '"1"' is not assignable to type 'number'.
  3. 1 const i: number = '1';
  4. ~
  5. Found 1 error.

因此,我们保持程序在调试状态下,再到 reportRelationError 打个断点,
位于 src/compiler/checker.ts#L14486
淡如止水 TypeScript (六):类型检查 - 图9

然后按 F5 继续运行。
淡如止水 TypeScript (六):类型检查 - 图10
我们看到,这是将 sourceType"1" 的 type,
赋值给targetTypenumber 的 type 时出错了。

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

  1. Type '"1"' is not assignable to type 'number'.

正是上文的类型检查报错信息。

6. 真实调用栈

淡如止水 TypeScript (六):类型检查 - 图11

至此我们才拿到了 debug/index.ts 类型检查出错的,真实调用栈信息,
我们看到在 checkSourceFile 中,进行了一系列检查,

  1. reportRelationError // 报错
  2. isRelatedTo // 无法赋值
  3. checkTypeRelatedTo
  4. checkTypeRelatedToAndOptionallyElaborate
  5. checkTypeAssignableToAndOptionallyElaborate
  6. checkVariableLikeDeclaration
  7. checkVariableDeclaration
  8. ...
  9. checkSourceElement
  10. ...
  11. checkVariableStatement
  12. ...
  13. checkSourceElement
  14. ...
  15. checkSourceFile
  16. ...

在检查是否可将类型为 "1" 的值赋值给类型为 numberi 时,报错了。


总结

在本文中,我们在 debug/index.ts 中构造了一个类型错误,
然后顺藤摸瓜,通过调用栈信息,反查了整条链路。

总结如下,TypeScript 在 performCompilation 中做了两件事情,
createProgramemitFilesAndReportErrorsAndGetExitStatus
createProgram 进行了语法检查,
emitFilesAndReportErrorsAndGetExitStatus 进行了类型检查。

类型检查的整条链路如下,

  1. performCompilation
  2. createProgram
  3. emitFilesAndReportErrorsAndGetExitStatus
  4. getSemanticDiagnostics
  5. checkSourceFile
  6. ...
  7. reportRelationError
  8. ...

TypeScript 的类型检查器非常的复杂,我们所能看到的只是很小的一部分。
checker.ts 代码已经有 36198 行了,src/compiler/checker.ts#L36198

参考

TypeScript v3.7.3