- [Node] 淡如止水 TypeScript (六):类型检查
- [Node] 淡如止水 TypeScript (零):开篇
- [Node] 淡如止水 TypeScript (十):自动补全
- [Node] 淡如止水 TypeScript (一):准备调试
- [Node] 淡如止水 TypeScript (三):词法分析
- [Node] 淡如止水 TypeScript (九):通信过程
- [Node] 淡如止水 TypeScript (七):代码生成
- [Node] 淡如止水 TypeScript (二):开始编译
- [Node] 淡如止水 TypeScript (四):语法分析
- [Node] 淡如止水 TypeScript (八):进程间通信
0. 回顾
上文我们介绍了 TypeScript 处理语法错误的代码逻辑,
是在 parseXXX 函数中,遇到期望之外的情况时,跑到额外的分支来处理错误的。
这个过程发生在 AST 的创建过程,即,发生在 parseList 调用链路上。
我们知道 TypeScript 源码的宏观结构,可简写如下,
performCompilation // 执行编译
createProgram // 创建 Program 对象
Parser.parseSourceFile // 每个文件单独解析,创建 SourceFile 对象
parseList // 返回一个 AST
emitFilesAndReportErrorsAndGetExitStatus
语法错误的处理,仍然发生在 createProgram 中。
本文开始分析类型错误,它发生在了 AST 创建之后的 emitFilesAndReportErrorsAndGetExitStatus 中。
1. 类型检查
与上一篇类似,我们先构造一个类型错误,然后再通过报错信息,找到调用栈。
我们修改 debug/index.ts 文件如下,
const i: number = '1';
把 i 的值从数字 1 改成了字符串 '1'。
编译结果,
$ 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.
错误码为 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
...
getSemanticDiagnostics
emitFilesAndReportErrors
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...
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.getSyntacticDiagnostics
program.getOptionsDiagnostics
program.getGlobalDiagnostics
program.getSemanticDiagnostics
类型检查发生在 program.getSemanticDiagnostics,src/compiler/program.ts#L1665,
后面就不再赘述了,我们只挑选一些关键节点来阅读代码。
沿着调用栈向上查找,我们看到了一个关键函数 checkSourceFile,
它是对 SourceFile 对象进行检查的。
reportRelationError
...
checkSourceFileWorker
checkSourceFile
getDiagnosticsWorker
...
getSemanticDiagnostics
emitFilesAndReportErrors
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...
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.ts
debug/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 // 无法赋值
checkTypeRelatedTo
checkTypeRelatedToAndOptionallyElaborate
checkTypeAssignableToAndOptionallyElaborate
checkVariableLikeDeclaration
checkVariableDeclaration
...
checkSourceElement
...
checkVariableStatement
...
checkSourceElement
...
checkSourceFile
...
在检查是否可将类型为 "1" 的值赋值给类型为 number 的 i 时,报错了。
总结
在本文中,我们在 debug/index.ts 中构造了一个类型错误,
然后顺藤摸瓜,通过调用栈信息,反查了整条链路。
总结如下,TypeScript 在 performCompilation 中做了两件事情,
createProgram 和 emitFilesAndReportErrorsAndGetExitStatus,
createProgram 进行了语法检查,
emitFilesAndReportErrorsAndGetExitStatus 进行了类型检查。
类型检查的整条链路如下,
performCompilation
createProgram
emitFilesAndReportErrorsAndGetExitStatus
getSemanticDiagnostics
checkSourceFile
...
reportRelationError
...
TypeScript 的类型检查器非常的复杂,我们所能看到的只是很小的一部分。
checker.ts 代码已经有 36198 行了,src/compiler/checker.ts#L36198。














网友评论