0. 回顾
上文我们介绍了 TypeScript 处理语法错误的代码逻辑,
是在 parseXXX 函数中,遇到期望之外的情况时,跑到额外的分支来处理错误的。
这个过程发生在 AST 的创建过程,即,发生在 parseList 调用链路上。
我们知道 TypeScript 源码的宏观结构,可简写如下,
performCompilation // 执行编译createProgram // 创建 Program 对象Parser.parseSourceFile // 每个文件单独解析,创建 SourceFile 对象parseList // 返回一个 ASTemitFilesAndReportErrorsAndGetExitStatus
语法错误的处理,仍然发生在 createProgram 中。
本文开始分析类型错误,它发生在了 AST 创建之后的 emitFilesAndReportErrorsAndGetExitStatus 中。
1. 类型检查
与上一篇类似,我们先构造一个类型错误,然后再通过报错信息,找到调用栈。
我们修改 debug/index.ts 文件如下,
const i: number = '1';
把 i 的值从数字 1 改成了字符串 '1'。
编译结果,
$ node bin/tsc debug/index.tsdebug/index.ts:1:7 - error TS2322: Type '"1"' is not assignable to type 'number'.1 const i: number = '1';~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 函数中,
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;}}...}
启动调试,程序顺利的停在了断点处,
我们看到左侧的调用栈,非常的陌生,这对我们来说是一个陌生的代码分支。
最下面的一个函数是 getSemanticDiagnostics,src/compiler/program.ts#L1665。
2. 跟踪调用栈
我们往下翻阅,查看调用栈信息,好在没有翻动多少,就看到了我们熟悉的函数了,
以下我们记录了一下调用栈信息,值得注意的是,调用顺序为倒序,
最底层的函数,最先触发,最上层的函数,越晚被调用。
reportRelationError...getSemanticDiagnosticsemitFilesAndReportErrorsemitFilesAndReportErrorsAndGetExitStatusperformCompilation...
performCompilation,src/tsc/executeCommandLine.ts#L493,
function performCompilation(...) {...const program = createProgram(programOptions);const exitStatus = emitFilesAndReportErrorsAndGetExitStatus(...);...}
先是调用了 emitFilesAndReportErrorsAndGetExitStatus,src/compiler/watch.ts#L200,
export function emitFilesAndReportErrorsAndGetExitStatus(...) {const { emitResult, diagnostics } = emitFilesAndReportErrors(...);...}
接着又调用了 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));}}}...}
这个函数中进行了多种检查,
program.getSyntacticDiagnosticsprogram.getOptionsDiagnosticsprogram.getGlobalDiagnosticsprogram.getSemanticDiagnostics
类型检查发生在 program.getSemanticDiagnostics,src/compiler/program.ts#L1665,
后面就不再赘述了,我们只挑选一些关键节点来阅读代码。
沿着调用栈向上查找,我们看到了一个关键函数 checkSourceFile,
它是对 SourceFile 对象进行检查的。
reportRelationError...checkSourceFileWorkercheckSourceFilegetDiagnosticsWorker...getSemanticDiagnosticsemitFilesAndReportErrorsemitFilesAndReportErrorsAndGetExitStatusperformCompilation...
3. checkSourceFile
首先,我们来看 checkSourceFile,是如何被调用的,
它的调用者为 getDiagnosticsWorker,src/compiler/checker.ts#L33100,
function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {...if (sourceFile) {...checkSourceFile(sourceFile);...}...}
为了获取诊断信息,它调用了 checkSourceFile,src/compiler/checker.ts#L33007,
function checkSourceFile(node: SourceFile) {performance.mark("beforeCheck");checkSourceFileWorker(node);performance.mark("afterCheck");performance.measure("Check", "beforeCheck", "afterCheck");}
这个函数中有 performance.mark 信息,是用来统计编译性能的,
看来我们的感觉没错,checkSourceFile 确实是一个关键函数。
现在我们来看一下 node 中的信息,
发现 fileName 居然是 built/local/lib.es5.d.ts。
这不是我们要编译的 debug/index.ts。
另一个问题是,这种 TypeScript 内置的文件,也会有类型错误?
确实是有的,我们来编译下这个文件,
$ node lib/tsc built/local/lib.es5.d.ts...Found 18 errors.
限于篇幅,中间的出错信息就不写了,至少我们知道,这个文件确实是有类型错误。
4. 条件断点
为了能拿到 debug/index.ts 文件的类型检查错误,
我们需要使用 VSCode 的条件断点功能。
在 checkSourceFileWorker 被调用所在的行,原来打断点的位置,右键,
选择 Add Conditional Breakpoint,
然后 VSCode 会弹出一个框,我们来输入条件,然后按回车,
node.fileName === 'debug/index.ts'
行首就会出现一个与普通断点不一样的断点了,
鼠标移动上去,会展示触发条件,
现在我们只保留这个断点,启动调试。
我们顺利停在了 check debug/index.ts 的情况下了。
5. reportRelationError
现在已经在处理 debug/index.ts 了,我们也确定对它进行类型检查一定会报错,
$ node bin/tsc debug/index.tsdebug/index.ts:1:7 - error TS2322: Type '"1"' is not assignable to type 'number'.1 const i: number = '1';~Found 1 error.
因此,我们保持程序在调试状态下,再到 reportRelationError 打个断点,
位于 src/compiler/checker.ts#L14486,
然后按 F5 继续运行。
我们看到,这是将 sourceType 为 "1" 的 type,
赋值给targetType 为 number 的 type 时出错了。
message 的值为 Type '{0}' is not assignable to type '{1}'.。
将 sourceType 和 targetType 填充后为,
Type '"1"' is not assignable to type 'number'.
正是上文的类型检查报错信息。
6. 真实调用栈

至此我们才拿到了 debug/index.ts 类型检查出错的,真实调用栈信息,
我们看到在 checkSourceFile 中,进行了一系列检查,
reportRelationError // 报错isRelatedTo // 无法赋值checkTypeRelatedTocheckTypeRelatedToAndOptionallyElaboratecheckTypeAssignableToAndOptionallyElaboratecheckVariableLikeDeclarationcheckVariableDeclaration...checkSourceElement...checkVariableStatement...checkSourceElement...checkSourceFile...
在检查是否可将类型为 "1" 的值赋值给类型为 number 的 i 时,报错了。
总结
在本文中,我们在 debug/index.ts 中构造了一个类型错误,
然后顺藤摸瓜,通过调用栈信息,反查了整条链路。
总结如下,TypeScript 在 performCompilation 中做了两件事情,createProgram 和 emitFilesAndReportErrorsAndGetExitStatus,createProgram 进行了语法检查,emitFilesAndReportErrorsAndGetExitStatus 进行了类型检查。
类型检查的整条链路如下,
performCompilationcreateProgramemitFilesAndReportErrorsAndGetExitStatusgetSemanticDiagnosticscheckSourceFile...reportRelationError...
TypeScript 的类型检查器非常的复杂,我们所能看到的只是很小的一部分。
checker.ts 代码已经有 36198 行了,src/compiler/checker.ts#L36198。
