最近在做一个 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
实例可以创建 Emitter
,Emitter
用来生成 SourceFile
对应的输出,例如 .js、.jsx、.d.ts、.js.map 等。