[译] TypeScript / 编译器是如何编译的

1,783 阅读3分钟

原文来自: huy.rocks - TypeScript / How the co…

作者:huy


这篇文章的灵感来源于 Orta TheroxHow the TypeScript compiler compiles,如果你想深入了解 TypeScript 的编译过程的话,也许你也会有兴趣看看这个。

从比较高的维度上来看,TypeScript 编译器是一个帮助我们分析和编译 TypeScript 代码到 JavaScript (*.js),同时产出类型定义文件 (*.d.ts) 和 source maps (*.js.map) 的工具。

image.png

如果 TypeScript 代码中有些错误,TypeScript 的编译器也能够帮助我们确定什么地方出了问题,以及如何解决。

一. 编译过程

在 TypeScript 的内部,编译是一个包括了许多不同步骤的复杂过程,下图绘制的就是 TypeScript 的简要编译过程。

tsc-overview.png

编译过程会在 tsc 命令被使用时启动,在启动编译前,TypeScript 需要一个 tsconfig.json 文件,这个文件主要是为了确定 编译配置输入文件 这两部分

{
    "files": [
        "src/*.ts"
    ],
    "compilerOptions": {
        ...
    }
}

在开始编译时,编译上下文将被创建为一个 Program 对象,这个方法被定义在 TypeScript 仓库的src/compiler/program.ts 文件中。创建Program 对象后,它将加载所有的输入文件以及输入文件中所 imports 导入的文件。并调用解析器 Parser(定义在 src/compiler/parser.ts 中),将每个文件解析成 AST(抽象语法树)。

tsc-program-to-ast.png

在底层,解析器 Parser 会创建一个 Scanner 实例,它将扫描源代码并生成代表 token 种类的SyntaxKind 流(注:比如说我们词法分析得到的一个 token 是 const,此 token 对应的种类即为 SyntaxKind.ConstKeyword, SyntaxKind 用来表示 token 的种类)。

解析过程并没有就此停止,在此之后,AST 将被送入绑定器 Binder(定义在 src/compiler/binder.ts 中),以创建 AST 节点和符号 Symbol 之间的映射。

tsc-binder-symbols.png

上图中的符号 Symbol 是用来存储每个 AST 节点 类型信息的元数据。绑定器 Binder 会创建一个 Symbol 表,它将在后面的阶段中被使用,比如类型检查。

在这之后,随着 Program.emit 被调用,Emit Worker 将被创建,并将 AST 转化为 JavaScript 源代码和一些其他的东西。发射器 Emitter 可以分为两类:

当发射器 Emitter 运行时,它将会调用 getDiagnostics() 函数以创建一个类型检查器 Type Checker,这个类型检查器被定义在 src/compiler/checker.ts 文件中。接下来发射器 Emitter 将会遍历 AST 并处理每个 Node 节点

在每个节点上,它将执行代码分析,并使用符号表的类型数据,如果一切顺利,最终的 JavaScript 代码将被生成。

tsc-emitter.png

二. 错误报告

在编译过程中,可能会有不同类型的错误被抛出,这取决于编译器是在哪个阶段发现错误的。

enum BuildResultFlags {
    None = 0,
    Success = 1 << 0,
    DeclarationOutputUnchanged = 1 << 1,
 
    ConfigFileErrors = 1 << 2,
    SyntaxErrors = 1 << 3,
    TypeErrors = 1 << 4,
    DeclarationEmitErrors = 1 << 5,
    EmitErrors = 1 << 6,
 
    AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors
}

例如,如果 tsconfig.json 文件中存在错误,TypeScript 编译器将会抛出 ConfigFileErrors 这个错误。

如果是扫描器 Scanner 发现了一个错误,那么这个错误就属于 SyntaxErrors。有时,代码的语法正确,但语义不正确,在大多数情况下,它们属于 TypeErrors,且可以被解析器 Parser 或 类型检查器 Type Checker 捕获。比如说

let a: number = "hello";

这段代码的语法是正确的,但在语义上是不正确的,因为你不能把一个字符串值分配给一个 number 变量。

三. 总结

在这篇文章中,只简要说明了编译过程中不同部分的概述,以及不同编译部分之间的联系,有了这个,你就能够探索 TypeScript 源代码,看看它实际是如何实现的。

建议阅读 TypeScript Compiler Internals 文档,它比本文更加深入(此文档也深入到了代码部分,以及编译过程中编译器的不同部分是如何相互调用的)。