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。
网友评论