知海

架构概述

TypeScript-dev项目相关

架构概述

层次概述

架构概览图
  • 核心 TypeScript 编译器
    • 语法分析器(Parser): 从一系列源文件开始,依据语言语法生成抽象语法树(AST)。
    • 绑定器(Binder): 使用 Symbol 将针对相同结构的声明联合在一起(例如:同一个接口或模块的不同声明,或拥有相同名字的函数和模块)。这有助于类型系统推导出这些具名声明。
    • 类型解析器与检查器(Type resolver / Checker): 解析每种类型的构造,检查读写语义并生成适当的诊断信息。
    • 生成器(Emitter): 从一系列输入文件(.ts.d.ts)生成输出,它们可以是以下形式之一:JavaScript(.js)、声明(.d.ts)或 source maps(.js.map)。
    • 预处理器(Pre-processor): “编译上下文”指某个“程序”中涉及到的所有文件。上下文的创建方式是:检查所有从命令行传入编译器的文件,按顺序排列,再加入这些文件直接引用或通过 import 语句和 /// <reference path=... /> 标签间接引用的其它文件。

沿着引用图遍历,会得到一个有序的源文件列表,它们组成了整个程序。当解析导入(import)时,会优先选择 .ts 文件而不是 .d.ts 文件,以确保处理的是最新文件。编译器会采用与 Node.js 相似的流程来解析导入,沿着目录链查找与待导入模块相匹配的带 .ts.d.ts 扩展名的文件。导入失败不会报 error,因为可能已声明了外部模块。

  • 独立编译器(tsc): 批处理编译命令行界面,主要负责针对不同支持的引擎读写文件(如:Node.js)。
  • 语言服务: “语言服务”在核心编译器管道之上暴露了额外的一层,非常适合类编辑器的应用。

语言服务支持一系列典型的编辑器操作,比如语句自动补全、函数签名提示、代码格式化与高亮、着色等。还包括基本的重构功能(如重命名)、调试接口辅助功能(如验证断点),以及 TypeScript 特有的功能(如支持增量编译,即命令行上的 --watch)。语言服务被设计为能有效处理长期存在的编译上下文中文件随时间变化的情况;在这种场景下,语言服务提供了与其它编译器接口不同的视角来处理程序和源文件。

请参考 [使用语言服务 API] 以了解更多详细内容。

数据结构

  • Node: 抽象语法树(AST)的基本组成块。通常 Node 表示语言语法中的非终结符;一些终结符也会保存在语法树中,比如标识符和字面量。
  • SourceFile: 给定源文件的 AST。SourceFile 本身是一个 Node,它提供了额外的接口来访问文件的源码、文件中的引用、文件中的标识符列表,以及文件中某个位置与对应行号和列号的映射。
  • Program: SourceFile 的集合及一系列编译选项,代表一个编译单元。Program 是类型系统和生成代码的主入口。
  • Symbol: 具名声明。Symbol 是绑定(联合)的结果,它连接了树中的声明节点与其它针对同一实体的声明。Symbol 是语义系统的基本构建块。
  • Type: Type 是语义系统的其它组成部分。Type 可能是命名的(如类和接口),也可能是匿名的(如对象类型)。
  • Signature: 共有三种 Signature 类型:调用签名(call)、构造签名(construct)和索引签名(index)。

编译过程概述

整个过程从预处理开始。预处理器会计算出哪些文件参与编译,它会去查找如下引用:/// <reference path=... /> 标签和 import 语句。

语法分析器(Parser)生成抽象语法树(AST)Node。这些仅为用户输出的抽象表现,以树的形式呈现。一个 SourceFile 对象表示一个给定文件的 AST,并附带一些额外信息,如文件名及源文件内容。

然后,绑定器(Binder)处理 AST 节点,结合并生成 Symbol。一个 Symbol 对应到一个命名实体。这里有一个微妙的差别:几个声明节点可能会对应名字相同的实体。也就是说,有时候不同的 Node 具有相同的 Symbol,并且每个 Symbol 都会跟踪它的声明节点。比如,一个名字相同的 classnamespace 可以合并,并且拥有相同的 Symbol。绑定器还会处理作用域,以确保每个 Symbol 都在正确的封闭作用域中创建。

生成 SourceFile(连同其 Symbol)是通过调用 createSourceFile API 完成的。

到目前为止,Symbol 代表的命名实体可以在单个文件中看到,但有些声明可以从多文件合并,因此下一步就是构建一个包含所有文件的全局视图,即创建一个 Program

一个 ProgramSourceFile 的集合,并带有一系列 CompilerOptions。通过调用 createProgram API 来创建 Program

通过一个 Program 实例可创建 TypeCheckerTypeChecker 是 TypeScript 类型系统的核心。它负责计算不同文件中 Symbol 之间的关系,将 Type 赋值给 Symbol,并生成任何语义 Diagnostic(如 error)。

TypeChecker 首先要做的是将不同 SourceFile 中的 Symbol 合并到一个单独的视图,创建单一的 Symbol 表,并合并所有普通的 Symbol(如不同文件中的 namespace)。

在初始状态初始化完成后,TypeChecker 就可以解决关于这个程序的任何问题了。这些“问题”可以是:

  • 这个 NodeSymbol 是什么?
  • 这个 SymbolType 是什么?
  • 在 AST 的某个部分中有哪些 Symbol 是可见的?
  • 某个函数声明的 Signature 都有哪些?
  • 针对某个文件应该报哪些错误?

TypeChecker 计算所有东西都是“懒惰”的;为了回答一个问题,它只“解决”必要的信息。TypeChecker 仅会检测与这个问题有关的 NodeSymbolType,而不会检测额外的实体。

对于一个 Program 同样会生成一个 EmitterEmitter 负责生成给定 SourceFile 的输出,包括:.js.jsx.d.ts.js.map

术语

完整开始 / 令牌开始(Full Start / Token Start)

令牌本身具有我们称为“完整开始”和“令牌开始”的两个概念。“令牌开始”是更自然的版本,表示令牌在文件中开始的位置。“完整开始”是指从上一个有意义的令牌之后扫描器开始扫描的起始位置。当关心琐碎内容时,我们往往更关注完整开始。

函数 描述
ts.Node.getStart 获取某节点的第一个令牌起始位置。
ts.Node.getFullStart 获取某节点拥有的第一个令牌的完整开始。

琐碎内容(Trivia)

语法中的琐碎内容代表源码里那些对理解代码无关紧要的内容,比如空白、注释,甚至一些冲突的标记。

由于琐碎内容不是语言正常语法的一部分(不包括在 ECMAScript API 规范中),并且可能出现在任意两个令牌之间的任意位置,因此它们不会包含在语法树中。但是,由于它们对于重构和维护高保真源码很重要,所以需要时仍然可以通过我们的 API 访问。

因为 EndOfFileToken 后面可以没有任何内容(令牌和琐碎内容),所有琐碎内容自然地出现在非琐碎内容之前,并存在于那个令牌的“完整开始”和“令牌开始”之间。

虽然这并非一个严格的规则,但可以用一种方便的标记法来说明某个注释“属于”某个 Node。比如,在下面的例子中,可以明显看出 genie 函数拥有两个注释:

typescript 复制代码
var x = 10; // This is x.

/**
 * Postcondition: Grants all three wishes.
 */
function genie([wish1, wish2, wish3]: [Wish, Wish, Wish]) {
  while (true) {}
} // End function

尽管事实上,函数声明的完整开始是在 var x = 10; 之后。

我们依据 Roslyn 的琐碎内容所有权概念 来处理注释所有权。通常来讲,一个令牌拥有同一行上的所有琐碎内容,直到下一个令牌开始。任何出现在这行之后的注释都属于下一个令牌。源文件的第一个令牌拥有所有的初始琐碎内容,并且最后的一系列琐碎内容会添加到 end-of-file 令牌上。

对于大多数普通用户来说,注释是“有趣的”琐碎内容。属于一个节点的注释内容可以通过以下函数获取:

函数 描述
ts.getLeadingCommentRanges 给定源文件和一个指定位置,返回该位置后的第一个换行与下一个令牌之间的注释范围(与 ts.Node.getFullStart 配合使用更有用)。
ts.getTrailingCommentRanges 给定源文件和一个指定位置,返回到该位置后第一个换行为止的注释范围(与 ts.Node.getEnd 配合使用更有用)。

作为例子,假设有下面一部分源代码:

typescript 复制代码
debugger;/*hello*/
    //bye
  /*hi*/    function

function 关键字的完整开始是从 /*hello*/ 注释开始的,但 getLeadingCommentRanges 仅会返回后面两个注释:

text 复制代码
d e b u g g e r ; / * h e l l o * / _ _ _ _ _ [CR] [NL] _ _ _ _ / / b y e [CR] [NL] _ _ / * h i * / _ _ _ _ f u n c t i o n
                  ↑                                     ↑       ↑                       ↑                   ↑
                  完整开始                              查找      第一个注释               第二个注释         令牌开始
                                                        开始注释

相应地,在 debugger 语句之后调用 getTrailingCommentRanges 可以提取出 /*hello*/ 注释。

如果你需要令牌流的更多信息,createScanner 也有一个 skipTrivia 标记,你可以设置为 false,然后使用 setText / setTextPos 来扫描文件中的不同位置。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。