TypeScript 的编译流程

最近在做一个 ts 语言编译的项目,了解了一下 ts 库提供的一些 api,以及编译流程。ts 自身提供了强大的 api,可以帮助我们进行 ts 语言编译器的开发。

依赖预处理

进行编译前首先要遍历代码根据 import 找到哪些文件是需要被引入到整个编译流程中的。

对单个文件处理,生成抽象语法树节点

parser 会根据输入的源文件,生成抽象语法树节点。在 ts 编译过程中,一个文件的抽象语法树是使用 SourceFile 结构来定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface SourceFile extends Declaration {
kind: SyntaxKind.SourceFile;
statements: NodeArray<Statement>;
endOfFileToken: Token<SyntaxKind.EndOfFileToken>;
fileName: string;
text: string;
amdDependencies: ReadonlyArray<AmdDependency>;
moduleName?: string;
referencedFiles: ReadonlyArray<FileReference>;
typeReferenceDirectives: ReadonlyArray<FileReference>;
libReferenceDirectives: ReadonlyArray<FileReference>;
languageVariant: LanguageVariant;
isDeclarationFile: boolean;
/**
* lib.d.ts should have a reference comment like
*
* /// <reference no-default-lib="true"/>
*
* If any other file has this comment, it signals not to include lib.d.ts
* because this containing file is intended to act as a default library.
*/
hasNoDefaultLib: boolean;
languageVersion: ScriptTarget;
}

SourceFile 本身是基础抽象语法树结构 Node 的一个扩展,他在抽象语法树的基础上增加了额外的接口,可以用来获取源文件代码,源文件地址,identifier 列表等功能。

在 parser 解析生成语法树后,会有一个 binder 遍历语法树来生成 binds Symbols,每一个 name 都会生成一个 Symbol。同时 binder 也会对命名空间进行处理,确保每个 Symbol 都在正确的命名空间中被创建。

Symbol 表示一个有名字的声明。用来链接定义同一个实体的不同声明节点,比方说一个 class 和它同样名称的 namespace 会有同样的 Symbol。

1
2
3
4
5
6
7
8
9
interface Symbol {
flags: SymbolFlags;
escapedName: __String;
declarations: Declaration[];
valueDeclaration: Declaration;
members?: SymbolTable;
exports?: SymbolTable;
globalExports?: SymbolTable;
}

生成 SourceFile 以及各节点对应的 Symbol,可以通过 createSourceFile API来实现。

多文件合并处理

由于要进行类型分析,分开处理单个文件是不行的,需要将多个文件中的声明合并在一起。因此下一个步骤是在一个整体视角上将所有依赖文件合并分析,构建一个 Program。

Program 指一个编译单元。它是由多个 SourceFile 和一些编译选项组成的。Program 是类型系统和代码生成的总入口。

可以使用 createProgram API 来生成 Program。由于 Program 由多个 SourceFile 组成,事实上,createProgram 也包含了依赖预处理、生成抽象语法树节点过程。我们只需要将入口 ts 代码以及编译选项传给 createProgram 就可以得到入口文件及其依赖文件(包括代码和一些声明文件)生成的 SourceFile

1
2
3
4
5
6
7
8
const program = ts.createProgram([filePath], {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS
});

for (const sourceFile of program.getSourceFiles()) {

}

类型分析

在生成 Program 实例后,可以调用 .getTypeChecker() 方法得到 TypeChecker 实例。

1
const typeChecker = program.getTypeChecker();

TypeChecker 是 ts 类型系统的核心。它负责计算不同文件中 Symbols 的关系,判断 Symbol 的类型,生成语法诊断(也就是语法错误)。

TypeChecker 做的第一件事情是分析不同文件间的 Symbol ,合并相同的 Symbol,得到一个统一的 Symbol Table。在初始化后,TypeChecker 就可以分析得到 Program 的各种状态,例如:

  • 这个 Node 对应的 Symbol 是什么?
  • 这个 Symbol 的类型是什么?
  • 在这部分 AST 中,哪些 Symbol 是可见的?
  • 这个文件有哪些 error ?

TypeChecker是懒惰式进行分析的,在调用时才会计算必要的内容,不会去尝试计算整个整体。

生成代码

通过 Program 实例可以创建 EmitterEmitter 用来生成 SourceFile 对应的输出,例如 .js、.jsx、.d.ts、.js.map 等。

相关文章