Node.js

[Node] 淡如止水 TypeScript (六):类型检查

2020-01-01  本文已影响0人  何幻

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;
    }
  }

  ...
}

启动调试,程序顺利的停在了断点处,


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

2. 跟踪调用栈

我们往下翻阅,查看调用栈信息,好在没有翻动多少,就看到了我们熟悉的函数了,


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

reportRelationError
...
getSemanticDiagnostics
emitFilesAndReportErrors
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...

performCompilationsrc/tsc/executeCommandLine.ts#L493

function performCompilation(
  ...
) {
  ...
  const program = createProgram(programOptions);
  const exitStatus = emitFilesAndReportErrorsAndGetExitStatus(
    ...
  );
  ...
}

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

export function emitFilesAndReportErrorsAndGetExitStatus(
  ...
) {
  const { emitResult, diagnostics } = emitFilesAndReportErrors(
    ...
  );

  ...
}

接着又调用了 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));
      }
    }
  }

  ...
}

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

program.getSyntacticDiagnostics
program.getOptionsDiagnostics
program.getGlobalDiagnostics
program.getSemanticDiagnostics

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

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

reportRelationError
...
checkSourceFileWorker
checkSourceFile
getDiagnosticsWorker
...
getSemanticDiagnostics
emitFilesAndReportErrors
emitFilesAndReportErrorsAndGetExitStatus
performCompilation
...

3. checkSourceFile

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

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

为了获取诊断信息,它调用了 checkSourceFilesrc/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,
赋值给targetTypenumber 的 type 时出错了。

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

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" 的值赋值给类型为 numberi 时,报错了。


总结

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

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

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

performCompilation
  createProgram
  emitFilesAndReportErrorsAndGetExitStatus
    getSemanticDiagnostics
      checkSourceFile
        ...
          reportRelationError
  ...

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

参考

TypeScript v3.7.3

上一篇 下一篇

猜你喜欢

热点阅读