美文网首页Node.js
[Node] 淡如止水 TypeScript (六):类型检查

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

作者: 何幻 | 来源:发表于2020-01-01 12:03 被阅读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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/acvboctx.html