TypeScript 的编译流程

1,367 阅读3分钟

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

依赖预处理

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

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

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

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。

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

const program = ts.createProgram([filePath], {
    target: ts.ScriptTarget.ES5,
    module: ts.ModuleKind.CommonJS
});

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

}

类型分析

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

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 等。

相关文章