TypeScript编译原理学习笔记

2,018 阅读24分钟

TypeScript的编译器的源文件位于TypeScript的源码的src/compiler目录下。

一、TypeScript编译器

1、关键组成部分

TypeScript编译器分为如下五个关键部分:

image.png

  • 扫描器(Scanner),在scanner.ts中
  • 解析器(Parser),在parser.ts中
  • 绑定器(Binder),在binder.ts中
  • 检查器(Checker),在checker.ts中
  • 发射器(Emitter),在emitter.ts中

这几个关键部分之间的协作关系如下图所示:

image.png

1)源代码经过扫描器,得到token流;

2)token流经过解析器,得到AST(Abstract Syntax Tree,抽象语法树)

3)AST经过绑定器,得到Symbol(符号)

符号(Symbol)是绑定的结果,它是TypeScript语义系统的主要构造块。符号将AST中的声明节点与相同实体的其他声明连接起来。符号和AST是检查器用来验证源代码语义的。

4)AST + Symbol(符号)输入检查器,得以进行类型验证

5)当需要输出JavaScript时,AST经过发射器,并调用检查器进行类型检查,输出JavaScript代码

2、核心工具——core.ts

core.ts文件是TypeScript编译器使用的核心工具集,其中let objectAllocator:ObjectAllocator是一个定义为全局单例的变量,它提供以下定义:

  • getNodeConstructor
  • getSymbolConstructor
  • getTypeConstructor
  • getSignatureConstructor(签名用于索引、调用和构造)

3、关键数据结构——types.ts

types.ts包含整个编译器所使用的关键数据结构和接口,这里列出一些关键部分:

  • SyntaxKind AST:节点类型通过SyntaxKind枚举进行识别
  • TypeChecker:类型检查器提供的接口
  • CompilerHost:用于程序(Program)和系统(System)之间的交互
  • Node AST:节点

4、系统文件——system.ts

TypeScript编译器与操作系统的所有交互均通过System接口进行。

二、程序——program.ts

编译上下文在TypeScript编译器中被视为一个Program,它包含SourceFile和编译选项。

1、CompilerHost的使用

CompilerHost是与操作环境进行交互的机制。Program使用==>CompilerHost使用==>System

用CompilerHost做中间层的原因是,这样可以让接口对Program的需求进行细粒度的调整,而无需考虑操作环境的需求。例如,Program无需关心System的fileExists函数。

2、SourceFile

Program提供了一个API,用于获取SourceFile:getSourceFiles():SourceFile[];。其得到的每个元素均是一个抽象语法树的根节点(被称作SourceFile)。

SourceFile包含两部分,即SyntaxKind.SourceFile和interface SourceFile。

每个SourceFile都是一个AST的根节点,它们包含在Program中。

三、抽象语法树

1、Node(节点)

节点是抽象语法树(Abstract Syntax Tree,简称AST)的基本构造块。

AST节点文档由两个关键部分构成。一个是节点的SyntaxKind枚举,用于标识AST中的类型;另一个是其接口,即在实例化AST时,节点所提供的API。

下面是interface Node的一些关键成员:

  • TextRange:标识该节点在源文件中的起止位置。
  • parent?:Node:指当前节点(在AST中)的父节点。

Node还有一些其他的成员,如标记(flag)和修饰符(modifier)等。你可以在源代码中搜索interface Node进行查看,对节点的遍历是非常重要的。

2、访问节点的子节点

访问子节点有个工具函数ts.forEachChild,可以用来访问AST任意节点的所有子节点。

下面是其简化后的代码片段,用于演示如何工作:

export function forEachChild<T>(node: Node, cbNode: (node: Node) => T, cbNodeArray?: (nodes: Node[]) => T): T {
    if (!node) return;

    switch (node.kind) {
        case SyntaxKind.BinaryExpression:
            return visitNode(cbNode, (<BinaryExpression>node).left) ||
                visitNode(cbNode, (<BinaryExpression>node).operatorToken) ||
                visitNode(cbNode, (<BinaryExpression>node).right);
        case SyntaxKind.IfStatement:
            return visitNode(cbNode, (<IfStatement>node).expression) ||
                visitNode(cbNode, (<IfStatement>node).thenStatement) ||
                visitNode(cbNode, (<IfStatement>node).elseStatement);        
    }
}

该函数主要检查node.kind并据此判断Node的接口,然后在其子节点上调用cbNode。但是,要注意该函数不会为所有子节点调用visitNode(如SyntaxKind.SemicolonToken)。如果想获得某AST点的所有子节点,只要调用该节点的成员函数.getChildren即可。

下面的函数会输出AST节点的详细信息:

function printAllChildren(node: ts.Node, depth = 0) {
    console.log(new Array(depth + 1).join('----'), ts.syntaxKindToName(node.kind), node.pos, node.end);
    depth++;
    node.getChildren().forEach(c => printAllChildren(c, depth));
}

3、SyntaxKind枚举

SyntaxKind被定义为一个常量枚举,如下所示:

export const enum SyntaxKind {
    Unkonwn,
    EndOfFileToken,
    SingleLineCommentTrivia,
    // ...
}

这是一个常量枚举,方便内联(例如,ts.SyntaxKind.EndOfFileToken会变为1),这样在使用AST时就不会有处理引用的额外开销了。但编译时需要使用--preserve ConstEnums来编译标记,以便枚举在运行时仍然可用。在JavaScript中,你也可以根据需要使用ts.SyntaxKind.EndOfFileToken。另外,可以用下面的函数,将枚举成员转化为可读的字符串:

export function syntaxKindToName(kind: ts.Syntaxkind) {
    return (<any>ts).SyntaxKind[kind];
}

4、AST杂项杂项(Trivia)

AST杂项杂项(Trivia)是指在源代码中对于正常理解代码来说不太重要的部分。例如,空白、注释、冲突标记等。为了保持轻量,杂项不会存储在AST中,但是可以视需要使用一些ts API来获取它们。

在展示这些API之前,你需要理解以下内容:

1)杂项的所有权

在通常情况下,token拥有它后面同一行到下一个token之前的所有杂项;该行之后的注释都与下一个token相关。

对于文件中的前导(leading)和结束(ending)注释来说。源文件中的第1个token拥有所有开始的杂项;而文件最后的一些杂项则附加到文件结束符上,该token长度为0。

2)杂项API在大多数基本的使用中,注释都是让人关注的杂项。节点的注释可以通过下面的函数来获取。

函数描述
ts.getLeadingCommentRanges给定源文本及其位置,返回给定位置后第一个换行符到token本身之间的注释范围(科恩那个需要结合ts.Node.getFullStart使用)
ts.getTrailingCommentRanges给定源文本及其位置,返回给定位置后第一个换行符之前的注释范围(科恩那个需要结合ts.Node.getEnd使用)

假设下面是某个源文件的一部分:

debugger;/*hello*/
    //bye
  /*hi*/  function

那么,对于function而言,getLeadingCommentRanges只返回最后的两个注释//bye/*hi*/。另外,在debugger语句结束的位置,调用getTrailingCommentRanges会得到注释/*hello*/

3)token start和full start的位置

节点有所谓的token start和full start的位置。

token start:比较自然的版本,即一个token文本在文件中开始的位置。

full start:是指扫描器从上一个重要token开始扫描的位置。

AST节点有getStart API和getFullStart API,用于获取以上两种位置,还是刚才的例子,对于function而言,token start即function的位置,而full start是/*hello*/的位置。

要注意,full start甚至会包含前一节点拥有的杂项。

四、扫描器——scanner.ts

由解析器控制扫描器将源代码转化为抽象语法树(AST)。即源代码经过扫描器变成token流,token流经过解析器,变成AST。

1、解析器对扫描器的调用

为避免重复创建扫描器造成的开销,在parser.ts中创建了一个扫描器的单例。解析器使用initializeState函数根据需要启动该扫描器。

下面是解析器中代码的简化版,你可以运行它演示以上概念:

import * as ts from 'typescript';

// 单例扫描器
const scanner = ts.createScanner(ts.ScriptTarget.Latest, /* 忽略杂项 */ true);

// 此函数与初始化使用的initializeState函数相似
function initializeState(text: string) {
    scanner.setText(text);
    scanner.setOnError((message: ts.DiagnosticMessage, length: number) => {
        console.error(message);
    });
    scanner.setScriptTarget(ts.ScriptTarget.ES5);
    scanner.setLanguageVariant(ts.LanguageVariant.Standard);
}

// 用例
initializeState(
    `
    var foo = 123;
    `.trim()
)

// 开始扫描
var token = scanner.scan();
while (token != ts.SyntaxKind.EndOfFileToken) {
    console.log(ts.formatSyntaxKind(token));
    token = scanner.scan();
}

这段代码会输出以下内容:

VarKeyword
Identifier
FirstAssignment
FirstLiteralToken
SemicolonToken

2、扫描器状态

调用scan后,扫描器更新其局部状态,如扫描位置、当前token详情等。扫描器提供了一组工具函数来获取当前扫描器的状态。在下面的示例中,我们创建一个扫描器并用它来识别token以及token在代码中的位置(code/compiler/scanner/runScannerWithPosition.ts)。

// 用例
initializeState(
    `
    var foo = 123;
    `.trim()
);

// 开始扫描
var token = scanner.scan();
while (token != ts.SyntaxKind.EndOfFileToken) {
    let currentToken = ts.formatSyntaxKind(token);
    let tokenStart = scanner.getStartPos();
    token = scanner.scan();
    let tokenEnd = scanner.getStartPos();
    console.log(currentToken, tokenStart, tokenEnd);
}

这段代码会输出以下内容:

VarKeyword 0 3
Identifier 3 7
FirstAssignment 7 9
FirstLiteralToken 9 13
SemicolonToken 13 14

3、独立扫描器

即使TypeScript解析器有单例扫描器,你仍然可以使用createScanner创建独立的扫描器,然后用setTextsetTextPos来随意扫描文件的不同位置。

五、解析器——praser.ts

前面已经介绍过,由解析器控制扫描器将源代码转化为AST。解析器的实现使用了单例模式(原因与扫描器类似)。它实际上被实现为namespace Parser,其中包含解析器的各种状态变量和单例扫描器(const scanner)。

1、程序对解析器的调用

解析器由程序间接驱动(通过之前提到过的CompilerHost)。基本上,简化的调用栈如下所示:

程序->
    CompilerHost.getSourceFile ->
        (全局函数 parser.ts).createSourceFile ->
            Parser.parseSourceFile

parseSourceFile不仅准备好了解析器的状态,还调用initializeState准备好了扫描器的状态。然后,使用parseSourceFileWorker继续解析源代码。

import * as ts from 'typescript';

function printAllChildren(node: ts.Node, depth = 0) {
    console.log(new Array(depth + 1).join('----'), ts.formatSyntaxKind(node.kind), node.pos, node.end);
    depth++;
    node.getChildren().forEach(c => printAllChildren(c, depth));
}

var sourceCode = 'var foo = 123;'.trim();
var sourceFile = ts.createSourceFile('foo.ts', sourceCode, ts.ScriptTarget.ES5, true);

printAllChildren(sourceFile);

这段代码会输出以下内容:

SourceFile 0 14
---- SyntaxList 0 14
-------- VariableStatement 0 14
---------------- VariableDeclarationList 0 13
-------------------- VarKeyword 0 3
-------------------- SyntaxList 3 13
------------------------ VariableDeclaration 3 13
---------------------------- Identifier 3 7
---------------------------- FirstAssignment 7 9
---------------------------- FirstLiteralToken 9 13
------------ SemicolonToken 13 14
---- EndOfFileToken 14 14

3、解析器函数

parseSourceFile设置初始状态并将工作交给parseSourceFileWorker函数去处理。

  • parseSourceFileWorker

parseSourceFileWorker函数会先创建一个SourceFile AST节点,然后从parseStatements函数开始解析源代码。一旦返回了结果,就用额外信息(如nodeCount、identifierCount等)来完善SourceFile节点。

  • parseStatements

parseStatements函数是最重要的parseFoo系函数之一。它根据扫描器返回的当前token来进行切换(调用相应的parseFoo函数)。例如,如果当前token是一个SemicolonToken(分号标记),那么它会调用paserEmptyStatement为空语句创建一个AST节点。

  • 节点创建

    解析器有一系列parseFoo函数用来创建类型为Foo的节点,这些函数通常在需要相应类型的节点时被其他解析器函数调用。该过程的典型示例是解析空语句(如;;;;;;)时要用的parseEmptyStatement函数,其代码如下所示:

function parseEmptyStatement(): Statement {
    let node = <Statement>createNode(SyntaxKind.EmptyStatement);
    parseExpected(SyntaxKind.SemicolonToken);
    
    return finishNode(node);
}

它展示了3个关键函数:createNodeparseExpectedfinishNode

  • createNode

解析器函数function createNode(kind:SyntaxKind,pos?:number):Node负责创建节点,设置传入的SyntaxKind(语法类别)和初始位置(默认使用当前扫描器状态提供的位置信息)。

  • parseExpected

解析器的parseExpected函数function parseExpected(kind:SyntaxKind,diagnosticMessage?:DiagnosticMessage):boolean会检查解析器状态中的当前token是否与指定的SyntaxKind相匹配。如果不匹配,则会向传入的诊断消息(diagnosticMessage)报告,或创建某种通用形式foo expected。该函数内部用parseErrorAtPosition函数(它使用扫描位置)提供良好的错误报告。

  • finishNode

解析器的finishNode函数function finishNode<T extends Node>(node:T,end?:number):T设置节点的end位置,并添加一些有用的信息。

例如上下文标记parserContextFlags,以及解析在该节点之前出现的错误(如果有错误的话,就不能在增量解析中重用此AST节点了)。

六、绑定器

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

源代码~~扫描器->token~~解析器->AST~~发射器->JavaScript

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

1、符号

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

function Symbol(flags: SymbolFlags, name: string) {
    this.flags = flags;
    this.name = name;
    this.declarations = undefined;
}

SymbolFlags符号标记是个标记枚举,用于识别额外的符号类别,如变量作用域标记FunctionScopedVariable、BlockScopedVariable等。

2、检查器对绑定器的使用

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

program.getTypeChecker =>
    ts.createTypeChecker(检查器中)=>
        initializeTypeChecker(检查器中)=>
            for each SourceFile 'ts.bindSourceFile'(绑定器中)
            // 接下来
            for each SourceFile 'ts.mergeSymbolTree'(检查器中)

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

3、绑定器函数

bindSourceFilemergeSymbolTable是两个关键的绑定器函数。

现在,我们重点讨论一下bindSourceFile。

  • bindSourceFile

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

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

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

  • bind

bind能处理任意节点(不只是SourceFile),它做的第一件事是分配node.parent,即使parent变量已经被设置,绑定器在bindChildren函数的处理中仍会再次设置;然后交给bindWorker做很多“重活儿”;最后调用bindChildren。

该函数简单地将绑定器的状态(如parent)存入函数的本地变量中,接着在每个子节点上调用bind,然后再将状态转存回绑定器中。

  • bindWorker

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

  • bindXXX函数

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

function createSymbol(flags: SymbolFlags, name: string): Symbol {
    symbolCount++;
    return new Symbol(flags, name);
}

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

4、绑定器声明

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

注意:外部模块源文件的符号设置方式是flags:SymbolFlags.ValueModule和name:'"'+removeFileExtension(file.fileName)+'"'。

function addDeclarationToSymbol(symbol: Symbol, node: Declaration, symbolFlags: SymbolFlags) {
  symbol.flags |= symbolFlags;

  // 创建 AST 节点到 symbol 的连接
  node.symbol = symbol;

  if (!symbol.declarations) {
    symbol.declarations = [];
  }
  // 将该节点添加为该符号的一个声明
  symbol.declarations.push(node);

  if (symbolFlags & SymbolFlags.HasExports && !symbol.exports) {
    symbol.exports = {};
  }

  if (symbolFlags & SymbolFlags.HasMembers && !symbol.members) {
    symbol.members = {};
  }

  if (symbolFlags & SymbolFlags.Value && !symbol.valueDeclaration) {
    symbol.valueDeclaration = node;
  }
}

上述代码主要执行的操作如下。● 创建一个从AST节点到符号的连接(node.symbol)。● 将节点添加为该符号的一个声明。

2)代码声明示例代码声明就是一个有可选的名字的节点,下面是在types.ts中进行声明的一个示例。

interface Declaration extends Node {
    _declarationBrand: any;
    name?: DeclarationName;
}

5、绑定器容器

AST的节点可以被当作容器。这决定了节点及相关符号的SymbolTables的类别。

AST的节点可以被当作容器。这决定了节点及相关符号的SymbolTables的类别。容器是一个抽象的概念(没有相关的数据结构)。该概念是由一些东西驱动的,ContainerFlags枚举是其中之一。

getContainerFlags函数(位于binder.ts)驱动ContainerFlags标记的示例如下。

function getContainerFlags(node: Node): ContainerFlags {
  switch (node.kind) {
    case SyntaxKind.ClassExpression:
    case SyntaxKind.ClassDeclaration:
    case SyntaxKind.InterfaceDeclaration:
    case SyntaxKind.EnumDeclaration:
    case SyntaxKind.TypeLiteral:
    case SyntaxKind.ObjectLiteralExpression:
      return ContainerFlags.IsContainer;

    case SyntaxKind.CallSignature:
    case SyntaxKind.ConstructSignature:
    case SyntaxKind.IndexSignature:
    case SyntaxKind.MethodDeclaration:
    case SyntaxKind.MethodSignature:
    case SyntaxKind.FunctionDeclaration:
    case SyntaxKind.Constructor:
    case SyntaxKind.GetAccessor:
    case SyntaxKind.SetAccessor:
    case SyntaxKind.FunctionType:
    case SyntaxKind.ConstructorType:
    case SyntaxKind.FunctionExpression:
    case SyntaxKind.ArrowFunction:
    case SyntaxKind.ModuleDeclaration:
    case SyntaxKind.SourceFile:
    case SyntaxKind.TypeAliasDeclaration:
      return ContainerFlags.IsContainerWithLocals;

    case SyntaxKind.CatchClause:
    case SyntaxKind.ForStatement:
    case SyntaxKind.ForInStatement:
    case SyntaxKind.ForOfStatement:
    case SyntaxKind.CaseBlock:
      return ContainerFlags.IsBlockScopedContainer;

    case SyntaxKind.Block:
      // 不要将函数内部的块直接当做块作用域的容器。
      // 本块中的本地变量应当置于函数中,否则下例中的 'x' 不会重新声明为一个块作用域的本地变量:
      //
      //     function foo() {
      //         var x;
      //         let x;
      //     }
      //
      // 如果将 'var x' 留在函数中,而将 'let x' 放到本块中(函数外),就不会有冲突了。
      //
      // 如果不在这里创建一个新的块作用域容器,'var x' 和 'let x' 都会进入函数容器本地中,这样就会有碰撞冲突。
      return isFunctionLike(node.parent) ? ContainerFlags.None : ContainerFlags.IsBlockScopedContainer;
  }

  return ContainerFlags.None;
}

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

// 所有容器节点都以声明顺序保存在一个链表中。
// 类型检查器中的 getLocalNameOfContainer 函数会使用该链表对容器使用的本地名称的唯一性做验证。
function bindChildren(node: Node) {
  // 在递归到子节点之前,我们先要保存父节点,容器和块容器。处理完弹出的子节点后,再将这些值存回原处。
  let saveParent = parent;
  let saveContainer = container;
  let savedBlockScopeContainer = blockScopeContainer;

  // 现在要将这个节点设为父节点,我们要递归它的子节点。
  parent = node;

  // 根据节点的类型,需要对当前容器或块容器进行调整。 如果当前节点是个容器,则自动将其视为当前的块容器。
  // 由于我们知道容器可能包含本地变量,因此提前初始化 .locals 字段。
  // 这样做是因为很可能需要将一些子(节点)置入 .locals 中(例如:函数参数或变量声明)。
  //
  // 但是,我们不会主动为块容器创建 .locals,因为通常块容器中不会有块作用域变量。
  // 我们不想为遇到的每个块都分配一个对象,大多数情况没有必要。
  //
  // 最后,如果是个块容器,我们就清理该容器中可能存在的 .locals 对象。这种情况常在增量编译场景中发生。
  // 由于我们可以重用上次编译的节点,而该节点可能已经创建了 locals 对象。
  // 因此必须清理,以免意外地从上次的编译中移动了过时的数据。
  let containerFlags = getContainerFlags(node);
  if (containerFlags & ContainerFlags.IsContainer) {
    container = blockScopeContainer = node;

    if (containerFlags & ContainerFlags.HasLocals) {
      container.locals = {};
    }

    addToContainerChain(container);
  } else if (containerFlags & ContainerFlags.IsBlockScopedContainer) {
    blockScopeContainer = node;
    blockScopeContainer.locals = undefined;
  }

  forEachChild(node, bind);

  container = saveContainer;
  parent = saveParent;
  blockScopeContainer = savedBlockScopeContainer;
}

6、绑定器符号表

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

interafce SymbolTable {
    [index: string]: Symbol;
}

符号表通过绑定进行初始化,下面是编译器使用的一些符号表。在节点上。

locals?: SymbolTable;

在符号上。

members?: SymbolTable;
exports?: SymbolTable;

请注意:bindChildren是基于ContainerFlags去初始化locals({})的。

符号表填充:

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

/**
 * 为指定的节点声明一个符号并加入 symbols。标识名冲突时报告错误。
 * @param symbolTable - 要将节点加入进的符号表
 * @param parent - 指定节点的父节点的声明
 * @param node - 要添加到符号表的(节点)声明
 * @param includes - SymbolFlags,指定节点额外的声明类型(例如:export, ambient 等)
 * @param excludes - 不能在符号表中声明的标志,用于报告禁止的声明
 */
function declareSymbol(
  symbolTable: SymbolTable,
  parent: Symbol,
  node: Declaration,
  includes: SymbolFlags,
  excludes: SymbolFlags
): Symbol {
  Debug.assert(!hasDynamicName(node));

  // 默认导出的函数节点或类节点的符号总是"default"
  let name = node.flags & NodeFlags.Default && parent ? 'default' : getDeclarationName(node);

  let symbol: Symbol;
  if (name !== undefined) {
    // 检查符号表中是否已有同名的符号。若没有,创建此名称的新符号并加入表中。
    // 注意,我们尚未给新符号指定任何标志。这可以确保不会和传入的 excludes 标志起冲突。
    //
    // 如果已存在的一个符号,查看是否与要创建的新符号冲突。
    // 例如:同一符号表中,'var' 符号和 'class' 符号会冲突。
    // 如果有冲突,报告该问题给该符号的每个声明,然后为该声明创建一个新符号
    //
    // 如果我们创建的新符号既没在符号表中重名也没和现有符号冲突,就将该节点添加为新符号的唯一声明。
    //
    // 否则,就要(将新符号)合并进兼容的现有符号中(例如同一容器中有多个同名的 'var' 时)。这种情况下要把该节点添加到符号的声明列表中。
    symbol = hasProperty(symbolTable, name)
      ? symbolTable[name]
      : (symbolTable[name] = createSymbol(SymbolFlags.None, name));

    if (name && includes & SymbolFlags.Classifiable) {
      classifiableNames[name] = name;
    }

    if (symbol.flags & excludes) {
      if (node.name) {
        node.name.parent = node;
      }

      // 报告每个重复声明的错误位置
      // 报告之前遇到的声明错误
      let message =
        symbol.flags & SymbolFlags.BlockScopedVariable
          ? Diagnostics.Cannot_redeclare_block_scoped_variable_0
          : Diagnostics.Duplicate_identifier_0;
      forEach(symbol.declarations, declaration => {
        file.bindDiagnostics.push(
          createDiagnosticForNode(declaration.name || declaration, message, getDisplayName(declaration))
        );
      });
      file.bindDiagnostics.push(createDiagnosticForNode(node.name || node, message, getDisplayName(node)));

      symbol = createSymbol(SymbolFlags.None, name);
    }
  } else {
    symbol = createSymbol(SymbolFlags.None, '__missing');
  }

  addDeclarationToSymbol(symbol, node, includes);
  symbol.parent = parent;

  return symbol;
}

7、绑定器错误报告

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

七、检查器

检查器使TypeScript更独特,比其他JavaScript转译器更强大。检查器位于checker.ts中,当前有23000行以上的代码,是编译器中最大的部分。

1、程序对检查器的使用

检查器是由程序初始化的,下面是调用栈的代码示例(在绑定器一节也展示过)。

program.getTypeChecker ->
    ts.createTypeChecker(在检查器中)->
        initializeTypeChecker(在检查器中)->
            for each SourceFile 'ts.bindSourceFile'(在绑定器中)
            // 接下来
            for each SourceFile 'ts.mergeSymbolTable'(在检查器中)

2、与发射器的联系

真正的类型检查在调用getDiagnostics时才会发生。

当该函数被调用,例如当向Program.emit发出请求时,检查器会返回一个EmitResolver(由程序调用检查器的getEmitResolver函数得到),EmitResolver是createTypeChecker的一个本地函数的集合。

下面是checkSourceFile的调用栈(checkSourceFile是createTypeChecker的一个本地函数)。

program.emit ->
    emitWorker (program local) ->
        createTypeChecker.getEmitResolver ->
            // 第一次调用下面的几个 createTypeChecker 的本地函数
            call getDiagnostics ->
                getDiagnosticsWorker ->
                    checkSourceFile

            // 接着
            return resolver
            // 通过对本地函数 createResolver() 的调用,resolver 已在 createTypeChecker 中初始化。

3、全局命名空间合并

在initializeTypeChecker中存在以下代码。

// 初始化全局符号表(SymbolTable)。
forEach(host.getSourceFiles(), file => {
  if (!isExternalModule(file)) {
    mergeSymbolTable(globals, file.locals);
  }
});

上述代码基本上将所有的global符号都合并到let globals:SymbolTable={}符号表中(位于createTypeChecker中)了。mergeSymbolTable主要调用mergeSymbol函数。

4、检查器错误报告

检查器使用本地的error函数报告错误,如下所示。

function error(location: Node, message: DiagnosticMessage, arg0?: any, arg1?: any, arg2?: any): void {
  let diagnostic = location
    ? createDiagnosticForNode(location, message, arg0, arg1, arg2)
    : createCompilerDiagnostic(message, arg0, arg1, arg2);
  diagnostics.add(diagnostic);
}

八、发射器

TypeScript编译器提供了两个发射器。

  • emitter.ts:它是把TypeScript编译为JavaScript的发射器。

  • declarationEmitter.ts:这个发射器用于为TypeScript源文件(.ts)创建声明文件(.d.ts)

本节我们将介绍emitter.ts。

1、Promgram对发射器的使用

Program提供了一个emit函数。该函数主要将功能委托给emitter.ts中的emitFiles函数。

下面是调用栈示例:

Program.emit ->
    'emitWorker'(在program.ts中的createProgram) ->
        'emitFiles'(在emitter.ts中的函数)

2、发射器函数

1)emitFiles

emitFiles定义在emitter.ts中,下面是该函数的签名:

// targetSourceFile,当用户想发射项目中的某个文件时被指定
// 保存时编译(compileOnSave)功能使用此函数
export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile?: SourceFile): EmitResult

EmitHost是CompilerHost的简化版,在运行时,很多用例实际上都是CompilerHost。


2)emitJavaScript该函数有良好的注释,如下所示。

```ts
function emitJavaScript(jsFilePath: string, root?: SourceFile) {
  let writer = createTextWriter(newLine);
  let write = writer.write;
  let writeTextOfNode = writer.writeTextOfNode;
  let writeLine = writer.writeLine;
  let increaseIndent = writer.increaseIndent;
  let decreaseIndent = writer.decreaseIndent;

  let currentSourceFile: SourceFile;
  // 导出器函数的名称,如果文件是个系统外部模块的话
  // System.register([...], function (<exporter>) {...})
  // System 模块中的导出像这样:
  // export var x; ... x = 1
  // =>
  // var x;... exporter("x", x = 1)
  let exportFunctionForFile: string;

  let generatedNameSet: Map<string> = {};
  let nodeToGeneratedName: string[] = [];
  let computedPropertyNamesToGeneratedNames: string[];

  let extendsEmitted = false;
  let decorateEmitted = false;
  let paramEmitted = false;
  let awaiterEmitted = false;
  let tempFlags = 0;
  let tempVariables: Identifier[];
  let tempParameters: Identifier[];
  let externalImports: (ImportDeclaration | ImportEqualsDeclaration | ExportDeclaration)[];
  let exportSpecifiers: Map<ExportSpecifier[]>;
  let exportEquals: ExportAssignment;
  let hasExportStars: boolean;

  /** 将发射输出写入磁盘 */
  let writeEmittedFiles = writeJavaScriptFile;

  let detachedCommentsInfo: { nodePos: number; detachedCommentEndPos: number }[];

  let writeComment = writeCommentRange;

  /** 发射一个节点 */
  let emit = emitNodeWithoutSourceMap;

  /** 在发射节点前调用 */
  let emitStart = function(node: Node) {};

  /** 发射结点完成后调用 */
  let emitEnd = function(node: Node) {};

  /** 从 startPos 位置开始,为指定的 token 发射文本。默认写入的文本由 tokenKind 提供,
   * 但是如果提供了可选的 emitFn 回调,将使用该回调来代替默认方式发射文本。
   * @param tokenKind 要搜索并发射的 token 的类别
   * @param startPos 源码中搜索 token 的起始位置
   * @param emitFn 如果给出,会被调用来进行文本的发射。
   */
  let emitToken = emitTokenText;

  /** 该函数由于节点的缘故,在被发射的代码中的函数或类中,会在启用词法作用域前被调用
   * @param scopeDeclaration 启动词法作用域的节点
   * @param scopeName 可选的作用域的名称,默认从节点声明中推导
   */
  let scopeEmitStart = function(scopeDeclaration: Node, scopeName?: string) {};

  /** 出了作用域后调用 */
  let scopeEmitEnd = function() {};

  /** 会被编码的 Sourcemap 数据 */
  let sourceMapData: SourceMapData;

  if (compilerOptions.sourceMap || compilerOptions.inlineSourceMap) {
    initializeEmitterWithSourceMaps();
  }

  if (root) {
    // 不要直接调用 emit,那样不会设置 currentSourceFile
    emitSourceFile(root);
  } else {
    forEach(host.getSourceFiles(), sourceFile => {
      if (!isExternalModuleOrDeclarationFile(sourceFile)) {
        emitSourceFile(sourceFile);
      }
    });
  }

  writeLine();
  writeEmittedFiles(writer.getText(), /*writeByteOrderMark*/ compilerOptions.emitBOM);
  return;

  /// 一批本地函数
}

它主要设置了一批本地变量和函数(这些函数构成emitter.ts的大部分内容),接着交给本地函数emitSourceFile去发射文本。emitSourceFile函数设置currentSourceFile,然后交给本地函数emit去处理。

function emitSourceFile(sourceFile: SourceFile): void {
    currentSourceFile = sourceFile;
    exportFunctionForFile = undefined;
    emit(sourceFile);
}

emit函数处理注释和实际的JavaScript的发射。实际的JavaScript的发射是emitJavaScriptWorker函数的工作。

3)emitJavaScriptWorker完整的函数如下所示。

function emitJavaScriptWorker(node: Node) {
  // 检查节点是否可以忽略 ScriptTarget 发射
  switch (node.kind) {
    case SyntaxKind.Identifier:
      return emitIdentifier(<Identifier>node);
    case SyntaxKind.Parameter:
      return emitParameter(<ParameterDeclaration>node);
    case SyntaxKind.MethodDeclaration:
    case SyntaxKind.MethodSignature:
      return emitMethod(<MethodDeclaration>node);
    case SyntaxKind.GetAccessor:
    case SyntaxKind.SetAccessor:
      return emitAccessor(<AccessorDeclaration>node);
    case SyntaxKind.ThisKeyword:
      return emitThis(node);
    case SyntaxKind.SuperKeyword:
      return emitSuper(node);
    case SyntaxKind.NullKeyword:
      return write('null');
    case SyntaxKind.TrueKeyword:
      return write('true');
    case SyntaxKind.FalseKeyword:
      return write('false');
    case SyntaxKind.NumericLiteral:
    case SyntaxKind.StringLiteral:
    case SyntaxKind.RegularExpressionLiteral:
    case SyntaxKind.NoSubstitutionTemplateLiteral:
    case SyntaxKind.TemplateHead:
    case SyntaxKind.TemplateMiddle:
    case SyntaxKind.TemplateTail:
      return emitLiteral(<LiteralExpression>node);
    case SyntaxKind.TemplateExpression:
      return emitTemplateExpression(<TemplateExpression>node);
    case SyntaxKind.TemplateSpan:
      return emitTemplateSpan(<TemplateSpan>node);
    case SyntaxKind.JsxElement:
    case SyntaxKind.JsxSelfClosingElement:
      return emitJsxElement(<JsxElement | JsxSelfClosingElement>node);
    case SyntaxKind.JsxText:
      return emitJsxText(<JsxText>node);
    case SyntaxKind.JsxExpression:
      return emitJsxExpression(<JsxExpression>node);
    case SyntaxKind.QualifiedName:
      return emitQualifiedName(<QualifiedName>node);
    case SyntaxKind.ObjectBindingPattern:
      return emitObjectBindingPattern(<BindingPattern>node);
    case SyntaxKind.ArrayBindingPattern:
      return emitArrayBindingPattern(<BindingPattern>node);
    case SyntaxKind.BindingElement:
      return emitBindingElement(<BindingElement>node);
    case SyntaxKind.ArrayLiteralExpression:
      return emitArrayLiteral(<ArrayLiteralExpression>node);
    case SyntaxKind.ObjectLiteralExpression:
      return emitObjectLiteral(<ObjectLiteralExpression>node);
    case SyntaxKind.PropertyAssignment:
      return emitPropertyAssignment(<PropertyDeclaration>node);
    case SyntaxKind.ShorthandPropertyAssignment:
      return emitShorthandPropertyAssignment(<ShorthandPropertyAssignment>node);
    case SyntaxKind.ComputedPropertyName:
      return emitComputedPropertyName(<ComputedPropertyName>node);
    case SyntaxKind.PropertyAccessExpression:
      return emitPropertyAccess(<PropertyAccessExpression>node);
    case SyntaxKind.ElementAccessExpression:
      return emitIndexedAccess(<ElementAccessExpression>node);
    case SyntaxKind.CallExpression:
      return emitCallExpression(<CallExpression>node);
    case SyntaxKind.NewExpression:
      return emitNewExpression(<NewExpression>node);
    case SyntaxKind.TaggedTemplateExpression:
      return emitTaggedTemplateExpression(<TaggedTemplateExpression>node);
    case SyntaxKind.TypeAssertionExpression:
      return emit((<TypeAssertion>node).expression);
    case SyntaxKind.AsExpression:
      return emit((<AsExpression>node).expression);
    case SyntaxKind.ParenthesizedExpression:
      return emitParenExpression(<ParenthesizedExpression>node);
    case SyntaxKind.FunctionDeclaration:
    case SyntaxKind.FunctionExpression:
    case SyntaxKind.ArrowFunction:
      return emitFunctionDeclaration(<FunctionLikeDeclaration>node);
    case SyntaxKind.DeleteExpression:
      return emitDeleteExpression(<DeleteExpression>node);
    case SyntaxKind.TypeOfExpression:
      return emitTypeOfExpression(<TypeOfExpression>node);
    case SyntaxKind.VoidExpression:
      return emitVoidExpression(<VoidExpression>node);
    case SyntaxKind.AwaitExpression:
      return emitAwaitExpression(<AwaitExpression>node);
    case SyntaxKind.PrefixUnaryExpression:
      return emitPrefixUnaryExpression(<PrefixUnaryExpression>node);
    case SyntaxKind.PostfixUnaryExpression:
      return emitPostfixUnaryExpression(<PostfixUnaryExpression>node);
    case SyntaxKind.BinaryExpression:
      return emitBinaryExpression(<BinaryExpression>node);
    case SyntaxKind.ConditionalExpression:
      return emitConditionalExpression(<ConditionalExpression>node);
    case SyntaxKind.SpreadElementExpression:
      return emitSpreadElementExpression(<SpreadElementExpression>node);
    case SyntaxKind.YieldExpression:
      return emitYieldExpression(<YieldExpression>node);
    case SyntaxKind.OmittedExpression:
      return;
    case SyntaxKind.Block:
    case SyntaxKind.ModuleBlock:
      return emitBlock(<Block>node);
    case SyntaxKind.VariableStatement:
      return emitVariableStatement(<VariableStatement>node);
    case SyntaxKind.EmptyStatement:
      return write(';');
    case SyntaxKind.ExpressionStatement:
      return emitExpressionStatement(<ExpressionStatement>node);
    case SyntaxKind.IfStatement:
      return emitIfStatement(<IfStatement>node);
    case SyntaxKind.DoStatement:
      return emitDoStatement(<DoStatement>node);
    case SyntaxKind.WhileStatement:
      return emitWhileStatement(<WhileStatement>node);
    case SyntaxKind.ForStatement:
      return emitForStatement(<ForStatement>node);
    case SyntaxKind.ForOfStatement:
    case SyntaxKind.ForInStatement:
      return emitForInOrForOfStatement(<ForInStatement>node);
    case SyntaxKind.ContinueStatement:
    case SyntaxKind.BreakStatement:
      return emitBreakOrContinueStatement(<BreakOrContinueStatement>node);
    case SyntaxKind.ReturnStatement:
      return emitReturnStatement(<ReturnStatement>node);
    case SyntaxKind.WithStatement:
      return emitWithStatement(<WithStatement>node);
    case SyntaxKind.SwitchStatement:
      return emitSwitchStatement(<SwitchStatement>node);
    case SyntaxKind.CaseClause:
    case SyntaxKind.DefaultClause:
      return emitCaseOrDefaultClause(<CaseOrDefaultClause>node);
    case SyntaxKind.LabeledStatement:
      return emitLabelledStatement(<LabeledStatement>node);
    case SyntaxKind.ThrowStatement:
      return emitThrowStatement(<ThrowStatement>node);
    case SyntaxKind.TryStatement:
      return emitTryStatement(<TryStatement>node);
    case SyntaxKind.CatchClause:
      return emitCatchClause(<CatchClause>node);
    case SyntaxKind.DebuggerStatement:
      return emitDebuggerStatement(node);
    case SyntaxKind.VariableDeclaration:
      return emitVariableDeclaration(<VariableDeclaration>node);
    case SyntaxKind.ClassExpression:
      return emitClassExpression(<ClassExpression>node);
    case SyntaxKind.ClassDeclaration:
      return emitClassDeclaration(<ClassDeclaration>node);
    case SyntaxKind.InterfaceDeclaration:
      return emitInterfaceDeclaration(<InterfaceDeclaration>node);
    case SyntaxKind.EnumDeclaration:
      return emitEnumDeclaration(<EnumDeclaration>node);
    case SyntaxKind.EnumMember:
      return emitEnumMember(<EnumMember>node);
    case SyntaxKind.ModuleDeclaration:
      return emitModuleDeclaration(<ModuleDeclaration>node);
    case SyntaxKind.ImportDeclaration:
      return emitImportDeclaration(<ImportDeclaration>node);
    case SyntaxKind.ImportEqualsDeclaration:
      return emitImportEqualsDeclaration(<ImportEqualsDeclaration>node);
    case SyntaxKind.ExportDeclaration:
      return emitExportDeclaration(<ExportDeclaration>node);
    case SyntaxKind.ExportAssignment:
      return emitExportAssignment(<ExportAssignment>node);
    case SyntaxKind.SourceFile:
      return emitSourceFileNode(<SourceFile>node);
  }
}

通过简单地调用相应的emitXXX函数来完成递归,例如emitFunctionDeclaration。

function emitFunctionDeclaration(node: FunctionLikeDeclaration) {
  if (nodeIsMissing(node.body)) {
    return emitOnlyPinnedOrTripleSlashComments(node);
  }

  if (node.kind !== SyntaxKind.MethodDeclaration && node.kind !== SyntaxKind.MethodSignature) {
    // 会把注释当做方法声明的一部分去发射。
    emitLeadingComments(node);
  }

  // 目标为 es6 之前时,使用 function 关键字来发射类函数(functions-like)声明,包括箭头函数
  // 目标为 es6 时,可以发射原生的 ES6 箭头函数,并使用宽箭头代替 function 关键字.
  if (!shouldEmitAsArrowFunction(node)) {
    if (isES6ExportedDeclaration(node)) {
      write('export ');
      if (node.flags & NodeFlags.Default) {
        write('default ');
      }
    }

    write('function');
    if (languageVersion >= ScriptTarget.ES6 && node.asteriskToken) {
      write('*');
    }
    write(' ');
  }

  if (shouldEmitFunctionName(node)) {
    emitDeclarationName(node);
  }

  emitSignatureAndBody(node);
  if (
    languageVersion < ScriptTarget.ES6 &&
    node.kind === SyntaxKind.FunctionDeclaration &&
    node.parent === currentSourceFile &&
    node.name
  ) {
    emitExportMemberAssignments((<FunctionDeclaration>node).name);
  }
  if (node.kind !== SyntaxKind.MethodDeclaration && node.kind !== SyntaxKind.MethodSignature) {
    emitTrailingComments(node);
  }
}

3、发射器SourceMap

如前所述,emitter.ts中的大部分代码是函数

emitJavaScript(我们之前展示过该函数的初始化过程)的代码。

它主要是设置一批本地变量并交给emitSourceFile处理。下面我们再来看一下这个函数,这次我们重点关注SourceMap部分。

function emitJavaScript(jsFilePath: string, root?: SourceFile) {

    // 无关代码 ........... 已移除
    let writeComment = writeCommentRange;

    /** 将发射的输出写到磁盘上 */
    let writeEmittedFiles = writeJavaScriptFile;

    /** 发射一个节点 */
    let emit = emitNodeWithoutSourceMap;

    /** 节点发射前调用 */
    let emitStart = function (node: Node) { };

    /** 节点发射完成后调用 */
    let emitEnd = function (node: Node) { };

    /** 从 startPos 位置开始,为指定的 token 发射文本。默认写入的文本由 tokenKind 提供,
      * 但是如果提供了可选的 emitFn 回调,将使用该回调来代替默认方式发射文本。
      * @param tokenKind 要搜索并发射的 token 的类别
      * @param startPos 源码中搜索 token 的起始位置
      * @param emitFn 如果给出,会被调用来进行文本的发射。*/
    let emitToken = emitTokenText;

    /** 该函数因为节点,会在发射的代码中于函数或类中启用词法作用域前调用
      * @param scopeDeclaration 启动词法作用域的节点
      * @param scopeName 可选的作用域的名称,而不是从节点声明中推导
      */
    let scopeEmitStart = function(scopeDeclaration: Node, scopeName?: string) { };

    /** 出了作用域后调用 */
    let scopeEmitEnd = function() { };

    /** 会被编码的 Sourcemap 数据 */
    let sourceMapData: SourceMapData;

    if (compilerOptions.sourceMap || compilerOptions.inlineSourceMap) {
        initializeEmitterWithSourceMaps();
    }

    if (root) {
        // 不要直接调用 emit,那样不会设置 currentSourceFile
        emitSourceFile(root);
    }
    else {
        forEach(host.getSourceFiles(), sourceFile => {
            if (!isExternalModuleOrDeclarationFile(sourceFile)) {
                emitSourceFile(sourceFile);
            }
        });
    }

    writeLine();
    writeEmittedFiles(writer.getText(), /*writeByteOrderMark*/ compilerOptions.emitBOM);
    return;
}

重要的函数调用:initializeEmitterWithSourceMaps,该函数是emitJavaScript的本地函数,

它覆盖了部分已定义的本地函数。被覆盖的函数可以在initalizeEmitterWithSourceMap的底部找到。

// initializeEmitterWithSourceMaps 函数的最后部分

writeEmittedFiles = writeJavascriptAndSourceMapFile;
emit = emitNodeWithSourceMap;
emitStart = recordEmitNodeStartSpan;
emitEnd = recordEmitNodeEndSpan;
emitToken = writeTextWithSpanRecord;
scopeEmitStart = recordScopeNameOfNode;
scopeEmitEnd = recordScopeNameEnd;
writeComment = writeCommentRangeWithMap;

也就是说大部分的发射器代码不关心SourceMap,它们以相同的方式使用这些含有或不含有SourceMap的本地函数。

参考: jkchao.github.io/typescript-…