绑定器

大多数的 JavaScript 转译器(transpiler)都比 TypeScript 简单,因为它们几乎没提供代码分析的方法。典型的 JavaScript 转换器只有以下流程:

  1. 源码 ~~扫描器~~> Tokens ~~解析器~~> AST ~~发射器~~> JavaScript

上述架构确实对于简化 TypeScript 生成 JavaScript 的理解有帮助,但缺失了一个关键功能,即 TypeScript 的语义系统。为了协助(检查器执行)类型检查,绑定器将源码的各部分连接成一个相关的类型系统,供检查器使用。绑定器的主要职责是创建符号(Symbols)。

符号

符号将 AST 中的声明节点与其它声明连接到相同的实体上。符号是语义系统的基本构造块。符号的构造器定义在 core.ts(绑定器实际上通过 objectAllocator.getSymbolConstructor 来获取构造器)。下面是符号构造器:

  1. function Symbol(flags: SymbolFlags, name: string) {
  2. this.flags = flags;
  3. this.name = name;
  4. this.declarations = undefined;
  5. }

SymbolFlags 符号标志是个标志枚举,用于识别额外的符号类别(例如:变量作用域标志 FunctionScopedVariableBlockScopedVariable 等)

检查器对绑定器的使用

实际上,绑定器被检查器在内部调用,而检查器又被程序调用。简化的调用栈如下所示:

  1. program.getTypeChecker ->
  2. ts.createTypeChecker(检查器中)->
  3. initializeTypeChecker(检查器中) ->
  4. for each SourceFile `ts.bindSourceFile`(绑定器中)
  5. // followed by
  6. for each SourceFile `ts.mergeSymbolTable`(检查器中)

SourceFile 是绑定器的工作单元,binder.tschecker.ts 驱动。

绑定器函数

bindSourceFilemergeSymbolTable 是两个关键的绑定器函数,我们来看下:

bindSourceFile

该函数主要是检查 file.locals 是否定义,如果没有则交给(本地函数) bind 来处理。

注意:locals 定义在节点上,其类型为 SymbolTableSourceFile 也是一个节点(事实上是 AST 中的根节点)。

提示:TypeScript 编译器大量使用本地函数。本地函数很可能使用来自父函数的变量(通过闭包捕获)。例如 bindbindSourceFile 中的一个本地函数,它或它调用的函数会设置 symbolCountclassifiableNames 等状态,然后将其存在返回的 SourceFile

bind

bind 能处理任一节点(不只是 SourceFile),它做的第一件事是分配 node.parent(如果 parent 变量已设置,绑定器在 bindChildren 函数的处理中仍会再次设置), 然后交给 bindWorker 做很多活。最后调用 bindChildren(该函数简单地将绑定器的状态(如:parent)存入函数本地变量中,接着在每个子节点上调用 bind,然后再将状态转存回绑定器中)。现在我们看下 bindWorker,一个更有趣的函数。

bindWorker

该函数依据 node.kindSyntaxKind类型)进行切换,并将工作委托给合适的 bindXXX 函数(也定义在binder.ts中)。例如:如果该节点是 SourceFile 则(最终且仅当节点是外部文件模块时)调用 bindAnonymousDeclaration

bindXXX 函数

bindXXX 系函数有一些通用的模式和工具函数。其中最常用的一个是 createSymbol 函数,全部代码展示如下:

  1. function createSymbol(flags: SymbolFlags, name: string): Symbol {
  2. symbolCount++;
  3. return new Symbol(flags, name);
  4. }

如您所见,它简单地更新 symbolCount(一个 bindSourceFile 的本地变量),并使用指定的参数创建符号。

绑定器声明

符号与声明

节点和符号间的链接由几个函数执行。其中一个用于绑定 SourceFile 节点到源文件符号(外部模块的情况下)的函数是 addDeclarationToSymbol

注意:外部模块源文件的符号设置为 flags : SymbolFlags.ValueModulename: '"' + removeFileExtension(file.fileName) + '"'.

  1. function addDeclarationToSymbol(symbol: Symbol, node: Declaration, symbolFlags: SymbolFlags) {
  2. symbol.flags |= symbolFlags;
  3. // 创建 AST 节点到 symbol 的连接
  4. node.symbol = symbol;
  5. if (!symbol.declarations) {
  6. symbol.declarations = [];
  7. }
  8. // 将该节点添加为该符号的一个声明
  9. symbol.declarations.push(node);
  10. if (symbolFlags & SymbolFlags.HasExports && !symbol.exports) {
  11. symbol.exports = {};
  12. }
  13. if (symbolFlags & SymbolFlags.HasMembers && !symbol.members) {
  14. symbol.members = {};
  15. }
  16. if (symbolFlags & SymbolFlags.Value && !symbol.valueDeclaration) {
  17. symbol.valueDeclaration = node;
  18. }
  19. }

重要的部分(译注:相关注释已添加到上面的代码中):

  • 创建一个从 AST 节点到符号的链接(node.symbol
  • 将节点添加为该符号的一个声明

声明

声明就是一个有可选的名字的节点。下面是 types.ts 中的定义:

  1. interface Declaration extends Node {
  2. _declarationBrand: any;
  3. name?: DeclarationName;
  4. }

绑定器容器

AST 的节点可以作容器。这决定了的节点及相关符号的 SymbolTables 的类别。容器是个抽象概念(没有相关的数据结构)。该概念由一些东西决定,ContainerFlags 枚举是其中之一。函数 getContainerFlags(位于 binder.ts) 驱动此标志,如下所示:

  1. function getContainerFlags(node: Node): ContainerFlags {
  2. switch (node.kind) {
  3. case SyntaxKind.ClassExpression:
  4. case SyntaxKind.ClassDeclaration:
  5. case SyntaxKind.InterfaceDeclaration:
  6. case SyntaxKind.EnumDeclaration:
  7. case SyntaxKind.TypeLiteral:
  8. case SyntaxKind.ObjectLiteralExpression:
  9. return ContainerFlags.IsContainer;
  10. case SyntaxKind.CallSignature:
  11. case SyntaxKind.ConstructSignature:
  12. case SyntaxKind.IndexSignature:
  13. case SyntaxKind.MethodDeclaration:
  14. case SyntaxKind.MethodSignature:
  15. case SyntaxKind.FunctionDeclaration:
  16. case SyntaxKind.Constructor:
  17. case SyntaxKind.GetAccessor:
  18. case SyntaxKind.SetAccessor:
  19. case SyntaxKind.FunctionType:
  20. case SyntaxKind.ConstructorType:
  21. case SyntaxKind.FunctionExpression:
  22. case SyntaxKind.ArrowFunction:
  23. case SyntaxKind.ModuleDeclaration:
  24. case SyntaxKind.SourceFile:
  25. case SyntaxKind.TypeAliasDeclaration:
  26. return ContainerFlags.IsContainerWithLocals;
  27. case SyntaxKind.CatchClause:
  28. case SyntaxKind.ForStatement:
  29. case SyntaxKind.ForInStatement:
  30. case SyntaxKind.ForOfStatement:
  31. case SyntaxKind.CaseBlock:
  32. return ContainerFlags.IsBlockScopedContainer;
  33. case SyntaxKind.Block:
  34. // 不要将函数内部的块直接当做块作用域的容器。
  35. // 本块中的本地变量应当置于函数中,否则下例中的 'x' 不会重新声明为一个块作用域的本地变量:
  36. //
  37. // function foo() {
  38. // var x;
  39. // let x;
  40. // }
  41. //
  42. // 如果将 'var x' 留在函数中,而将 'let x' 放到本块中(函数外),就不会有冲突了。
  43. //
  44. // 如果不在这里创建一个新的块作用域容器,'var x' 和 'let x' 都会进入函数容器本地中,这样就会有碰撞冲突。
  45. return isFunctionLike(node.parent) ? ContainerFlags.None : ContainerFlags.IsBlockScopedContainer;
  46. }
  47. return ContainerFlags.None;
  48. }

该函数在绑定器函数 bindChildren 中调用,会根据 getContainerFlags 的运行结果将节点设为 container 和(或) blockScopedContainer。函数 bindChildren 如下所示:

  1. // 所有容器节点都以声明顺序保存在一个链表中。
  2. // 类型检查器中的 getLocalNameOfContainer 函数会使用该链表对容器使用的本地名称的唯一性做验证。
  3. function bindChildren(node: Node) {
  4. // 在递归到子节点之前,我们先要保存父节点,容器和块容器。处理完弹出的子节点后,再将这些值存回原处。
  5. let saveParent = parent;
  6. let saveContainer = container;
  7. let savedBlockScopeContainer = blockScopeContainer;
  8. // 现在要将这个节点设为父节点,我们要递归它的子节点。
  9. parent = node;
  10. // 根据节点的类型,需要对当前容器或块容器进行调整。 如果当前节点是个容器,则自动将其视为当前的块容器。
  11. // 由于我们知道容器可能包含本地变量,因此提前初始化 .locals 字段。
  12. // 这样做是因为很可能需要将一些子(节点)置入 .locals 中(例如:函数参数或变量声明)。
  13. //
  14. // 但是,我们不会主动为块容器创建 .locals,因为通常块容器中不会有块作用域变量。
  15. // 我们不想为遇到的每个块都分配一个对象,大多数情况没有必要。
  16. //
  17. // 最后,如果是个块容器,我们就清理该容器中可能存在的 .locals 对象。这种情况常在增量编译场景中发生。
  18. // 由于我们可以重用上次编译的节点,而该节点可能已经创建了 locals 对象。
  19. // 因此必须清理,以免意外地从上次的编译中移动了过时的数据。
  20. let containerFlags = getContainerFlags(node);
  21. if (containerFlags & ContainerFlags.IsContainer) {
  22. container = blockScopeContainer = node;
  23. if (containerFlags & ContainerFlags.HasLocals) {
  24. container.locals = {};
  25. }
  26. addToContainerChain(container);
  27. } else if (containerFlags & ContainerFlags.IsBlockScopedContainer) {
  28. blockScopeContainer = node;
  29. blockScopeContainer.locals = undefined;
  30. }
  31. forEachChild(node, bind);
  32. container = saveContainer;
  33. parent = saveParent;
  34. blockScopeContainer = savedBlockScopeContainer;
  35. }

您可能还记得绑定器函数中的这部分:bindChildrenbind 函数调用。我们得到这样的递归绑定:bind 调用 bindChildren,而 bindChildren 又为其每个子节点调用 bind

绑定器符号表

符号表(SymbolTable)实现为一个简单的 HashMap,下面是其接口(types.ts):

  1. interface SymbolTable {
  2. [index: string]: Symbol;
  3. }

符号表通过绑定进行初始化,这里是编译器使用的一些符号表:

节点上:

  1. locals?: SymbolTable; // 节点相关的本地变量

符号上:

  1. members?: SymbolTable; // 类,接口或字面量实例成员
  2. exports?: SymbolTable; // 模块导出

请注意:bindChildren 基于 ContainerFlags 初始化 locals(为 {}

符号表填充

符号表使用符号来填充,主要是通过调用 declareSymbol 来进行,如下所示的是该函数的全部代码:

  1. /**
  2. * 为指定的节点声明一个符号并加入 symbols。标识名冲突时报告错误。
  3. * @param symbolTable - 要将节点加入进的符号表
  4. * @param parent - 指定节点的父节点的声明
  5. * @param node - 要添加到符号表的(节点)声明
  6. * @param includes - SymbolFlags,指定节点额外的声明类型(例如:export, ambient 等)
  7. * @param excludes - 不能在符号表中声明的标志,用于报告禁止的声明
  8. */
  9. function declareSymbol(
  10. symbolTable: SymbolTable,
  11. parent: Symbol,
  12. node: Declaration,
  13. includes: SymbolFlags,
  14. excludes: SymbolFlags
  15. ): Symbol {
  16. Debug.assert(!hasDynamicName(node));
  17. // 默认导出的函数节点或类节点的符号总是"default"
  18. let name = node.flags & NodeFlags.Default && parent ? 'default' : getDeclarationName(node);
  19. let symbol: Symbol;
  20. if (name !== undefined) {
  21. // 检查符号表中是否已有同名的符号。若没有,创建此名称的新符号并加入表中。
  22. // 注意,我们尚未给新符号指定任何标志。这可以确保不会和传入的 excludes 标志起冲突。
  23. //
  24. // 如果已存在的一个符号,查看是否与要创建的新符号冲突。
  25. // 例如:同一符号表中,'var' 符号和 'class' 符号会冲突。
  26. // 如果有冲突,报告该问题给该符号的每个声明,然后为该声明创建一个新符号
  27. //
  28. // 如果我们创建的新符号既没在符号表中重名也没和现有符号冲突,就将该节点添加为新符号的唯一声明。
  29. //
  30. // 否则,就要(将新符号)合并进兼容的现有符号中(例如同一容器中有多个同名的 'var' 时)。这种情况下要把该节点添加到符号的声明列表中。
  31. symbol = hasProperty(symbolTable, name)
  32. ? symbolTable[name]
  33. : (symbolTable[name] = createSymbol(SymbolFlags.None, name));
  34. if (name && includes & SymbolFlags.Classifiable) {
  35. classifiableNames[name] = name;
  36. }
  37. if (symbol.flags & excludes) {
  38. if (node.name) {
  39. node.name.parent = node;
  40. }
  41. // 报告每个重复声明的错误位置
  42. // 报告之前遇到的声明错误
  43. let message =
  44. symbol.flags & SymbolFlags.BlockScopedVariable
  45. ? Diagnostics.Cannot_redeclare_block_scoped_variable_0
  46. : Diagnostics.Duplicate_identifier_0;
  47. forEach(symbol.declarations, declaration => {
  48. file.bindDiagnostics.push(
  49. createDiagnosticForNode(declaration.name || declaration, message, getDisplayName(declaration))
  50. );
  51. });
  52. file.bindDiagnostics.push(createDiagnosticForNode(node.name || node, message, getDisplayName(node)));
  53. symbol = createSymbol(SymbolFlags.None, name);
  54. }
  55. } else {
  56. symbol = createSymbol(SymbolFlags.None, '__missing');
  57. }
  58. addDeclarationToSymbol(symbol, node, includes);
  59. symbol.parent = parent;
  60. return symbol;
  61. }

填充哪个符号表,由此函数的第一个参数决定。例如:添加声明到类型为 SyntaxKind.ClassDeclarationSyntaxKind.ClassExpression容器时,将会调用下面的函数 declareClassMember:

  1. function declareClassMember(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) {
  2. return node.flags & NodeFlags.Static
  3. ? declareSymbol(container.symbol.exports, container.symbol, node, symbolFlags, symbolExcludes)
  4. : declareSymbol(container.symbol.members, container.symbol, node, symbolFlags, symbolExcludes);
  5. }

绑定器错误报告

绑定错误被添加到源文件的 bindDiagnostics 列表中

一个绑定时错误检测的例子是在严格模式下使用 evalarguments 作为变量名。下面展示了相关的全部代码(多个位置都会调用checkStrictModeEvalOrArguments,调用栈发自 bindWorker,该函数对不同节点的 SyntaxKind 调用不同的检查函数):

  1. function checkStrictModeEvalOrArguments(contextNode: Node, name: Node) {
  2. if (name && name.kind === SyntaxKind.Identifier) {
  3. let identifier = <Identifier>name;
  4. if (isEvalOrArgumentsIdentifier(identifier)) {
  5. // 首先检查名字是否在类声明或者类表达式中,如果是则给出明确消息,否则报告一般性错误
  6. let span = getErrorSpanForNode(file, name);
  7. file.bindDiagnostics.push(
  8. createFileDiagnostic(
  9. file,
  10. span.start,
  11. span.length,
  12. getStrictModeEvalOrArgumentsMessage(contextNode),
  13. identifier.text
  14. )
  15. );
  16. }
  17. }
  18. }
  19. function isEvalOrArgumentsIdentifier(node: Node): boolean {
  20. return (
  21. node.kind === SyntaxKind.Identifier &&
  22. ((<Identifier>node).text === 'eval' || (<Identifier>node).text === 'arguments')
  23. );
  24. }
  25. function getStrictModeEvalOrArgumentsMessage(node: Node) {
  26. // 向用户提供特定消息,有助他们理解为何会处于严格模式。
  27. if (getContainingClass(node)) {
  28. return Diagnostics.Invalid_use_of_0_Class_definitions_are_automatically_in_strict_mode;
  29. }
  30. if (file.externalModuleIndicator) {
  31. return Diagnostics.Invalid_use_of_0_Modules_are_automatically_in_strict_mode;
  32. }
  33. return Diagnostics.Invalid_use_of_0_in_strict_mode;
  34. }

原文: https://jkchao.github.io/typescript-book-chinese/compiler/binder.html