TypeScript的编译器的源文件位于TypeScript的源码的src/compiler目录下。
一、TypeScript编译器
1、关键组成部分
TypeScript编译器分为如下五个关键部分:
- 扫描器(Scanner),在scanner.ts中
- 解析器(Parser),在parser.ts中
- 绑定器(Binder),在binder.ts中
- 检查器(Checker),在checker.ts中
- 发射器(Emitter),在emitter.ts中
这几个关键部分之间的协作关系如下图所示:
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
创建独立的扫描器,然后用setText
、setTextPos
来随意扫描文件的不同位置。
五、解析器——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个关键函数:createNode
、parseExpected
和finishNode
。
- 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、绑定器函数
bindSourceFile
和mergeSymbolTable
是两个关键的绑定器函数。
现在,我们重点讨论一下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的本地函数。