1. 概览
Typescript 解析器代码 位于 parser.ts 中。 在内部 由解析器控制扫描器将源码解析成AST。
其实我们在 上面一个篇章中关于 扫描器中也这样解释过 扫描器的入口 是有解析器来控制 解析器实现原理是单例模式。
2. 程序对解析器的使用
解析器由程序间接驱动 简化的调用栈如下所示
程序 ->CompilerHost.getSourceFile ->(全局函数 parser.ts).createSourceFile ->Parser.parseSourceFile
parseSourceFile 不仅准备好 解析器的状态 还调用 initialState 准备好扫描器的状态 然后使用 parseSourceFileWorker 继续解析代码
3. demo 示例
想了想 单纯的看代码 还是有些枯燥 这样 我们还是先写个demo 根据输入输出来判断 整个解析器内部 是怎样生成AST的呢
3.1 输入
import * as ts from 'ntypescript';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 = `const foo = 123;`;var sourceFile = ts.createSourceFile('foo.ts', sourceCode, ts.ScriptTarget.ES5, true)console.log(sourceFile)printAllChildren(sourceFile);
3.2 输出
// SourceFile 0 16// ---- SyntaxList 0 16// -------- VariableStatement 0 16// ------------ VariableDeclarationList 0 15// ---------------- ConstKeyword 0 5// ---------------- SyntaxList 5 15// -------------------- VariableDeclaration 5 15// ------------------------ Identifier 5 9// ------------------------ EqualsToken 9 11// ------------------------ NumericLiteral 11 15// ------------ SemicolonToken 15 16// ---- EndOfFileToken 16 16
这其实就是一个 AST 树 当然了关于AST 又是一个可能比较深的话题 我们先看 在typescript 源码中 对于 这颗树的定义
4. 源码解析
4.1 createSourceFile
// parser 的入口// sourceText const foo = 123;export function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes = false, scriptKind?: ScriptKind): SourceFile {performance.mark("beforeParse");const result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind);performance.mark("afterParse");performance.measure("Parse", "beforeParse", "afterParse");return result;}
其中 performance 是关于 性能分析的代码 与 主流程无关 我们 只需要 关心 Parser.parseSourceFile 函数
4.2 Parser.parseSourceFile
// sourceText const foo = 123;export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor, setParentNodes?: boolean, scriptKind?: ScriptKind): SourceFile {scriptKind = ensureScriptKind(fileName, scriptKind);// 初始化状态// scan 扫描器 的初始化// scanner.setText(text);initializeState(sourceText, languageVersion, syntaxCursor, scriptKind);// 解析目标文件const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes, scriptKind);// 清除状态clearState();return result;}
其中 initializeState 是 关于 扫描器初始化的函数 我们简单看以下 initializeState 代码
function initializeState(_sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor, scriptKind: ScriptKind) {// Initialize and prime the scanner before parsing the source elements.scanner.setText(sourceText);scanner.setOnError(scanError);scanner.setScriptTarget(languageVersion);scanner.setLanguageVariant(getLanguageVariant(scriptKind));}
4.3 parseSourceFileWorker
我们继续往下走 来到了 parseSourceFileWorker
function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile {// 创建解析的目标sourceFile = createSourceFile(fileName, languageVersion, scriptKind);sourceFile.flags = contextFlags;// Prime the scanner.// 新扫描的token替换currentToken// 执行 scanner.scan();nextToken();// 生成每个range的各种信息(包括起点和终点)processReferenceComments(sourceFile);sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);Debug.assert(token() === SyntaxKind.EndOfFileToken);sourceFile.endOfFileToken = addJSDocComment(parseTokenNode() as EndOfFileToken);setExternalModuleIndicator(sourceFile);sourceFile.nodeCount = nodeCount;sourceFile.identifierCount = identifierCount;sourceFile.identifiers = identifiers;sourceFile.parseDiagnostics = parseDiagnostics;if (setParentNodes) {fixupParentReferences(sourceFile);}return sourceFile;}
我们看上述代码 可以发现 parseSourceFileWorker 主要完成了下面四个工作
- createSourceFile 为我们创建解析的目标
- 执行 nextToken 新扫描的 token 替换 currentToken
- 执行 processReferenceComments 生成每个range的各种信息(包括起点和终点)
- 执行 parseList
4.3.1 createSourceFile 代码如下
function createSourceFile(fileName: string, languageVersion: ScriptTarget, scriptKind: ScriptKind): SourceFile {// code from createNode is inlined here so createNode won't have to deal with special case of creating source files// this is quite rare comparing to other nodes and createNode should be as fast as possibleconst sourceFile = <SourceFile>new SourceFileConstructor(SyntaxKind.SourceFile, /*pos*/ 0, /* end */ sourceText.length);nodeCount++;sourceFile.text = sourceText;sourceFile.bindDiagnostics = [];sourceFile.languageVersion = languageVersion;sourceFile.fileName = normalizePath(fileName);sourceFile.languageVariant = getLanguageVariant(scriptKind);sourceFile.isDeclarationFile = fileExtensionIs(sourceFile.fileName, Extension.Dts);sourceFile.scriptKind = scriptKind;return sourceFile;}
在这里 我们正好可以 看下 对于 sourceFile 的 ts 定义
let SourceFileConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node;
4.3.2 nextToken
function nextToken(): SyntaxKind {return currentToken = scanner.scan();}
4.3.3 processReferenceComments
对于 processReferenceComments 的分析 或许我们可以先看下输入输入 才能更清楚的知道其中发生了什么
执行 processReferenceComments 之前的sourceFile
bindDiagnostics:(0) []end:16fileName:'foo.ts'flags:0isDeclarationFile:falsekind:265languageVariant:0languageVersion:1parent:undefinedpos:0scriptKind:3text:'const foo = 123;'transformFlags:undefined
amdDependencies:(0) []bindDiagnostics:(0) []checkJsDirective:undefinedend:16fileName:'foo.ts'flags:0isDeclarationFile:falsekind:265languageVariant:0languageVersion:1moduleName:undefinedparent:undefinedpos:0referencedFiles:(0) []scriptKind:3text:'const foo = 123;'transformFlags:undefinedtypeReferenceDirectives:(0) []
看起来好像没什么差别的样式 那也就是说 processReferenceComments 并不是生成AST 的主力
那么我们继续往下看
4.3.4 parseList
function parseList<T extends Node>(kind: ParsingContext, parseElement: () => T): NodeArray<T> {const saveParsingContext = parsingContext;parsingContext |= 1 << kind;const result = createNodeArray<T>();while (!isListTerminator(kind)) {if (isListElement(kind, /*inErrorRecovery*/ false)) {// parseListElement 处理const element = parseListElement(kind, parseElement);result.push(element);continue;}if (abortParsingListOrMoveToNextToken(kind)) {break;}}result.end = getNodeEnd();parsingContext = saveParsingContext;return result;}
其中我们 可以看到 其中element 生成的 还是 要靠 parseListElement 我们继续看代码
function parseListElement<T extends Node>(parsingContext: ParsingContext, parseElement: () => T): T {const node = currentNode(parsingContext);if (node) {return <T>consumeNode(node);}return parseElement();}
其中 parseListElement 最终的生成结果 还是要靠 用户在最开始 传入的 parseElement
好吧 继续
这里的 parseElement 就是 最开始的 parseStatement
它会根据扫描器返回的token 来回切换 (调用相应的parseXXX函数) 例如 当前token是一个 SemicolonToken(分号标记) 就会调用 parseEmptyStatement 为空语句创建一个AST节点
4.3.5 parseStatement
// 在 parseStatement中对token做了switch处理, 根据不同的token获取不同的Node节点。比如我们以最后的; 来做一下简单的判断。// 首先;对应的 token值应该是 SyntaxKind.SemicolonToken刚刚好是条件判断中的第一个。function parseStatement(): Statement {switch (token()) {case SyntaxKind.SemicolonToken:return parseEmptyStatement();case SyntaxKind.OpenBraceToken:return parseBlock(/*ignoreMissingOpenBrace*/ false);case SyntaxKind.VarKeyword:return parseVariableStatement(scanner.getStartPos(), /*decorators*/ undefined, /*modifiers*/ undefined);case SyntaxKind.LetKeyword:if (isLetDeclaration()) {return parseVariableStatement(scanner.getStartPos(), /*decorators*/ undefined, /*modifiers*/ undefined);}break;case SyntaxKind.FunctionKeyword:return parseFunctionDeclaration(scanner.getStartPos(), /*decorators*/ undefined, /*modifiers*/ undefined);case SyntaxKind.ClassKeyword:return parseClassDeclaration(scanner.getStartPos(), /*decorators*/ undefined, /*modifiers*/ undefined);case SyntaxKind.IfKeyword:return parseIfStatement();case SyntaxKind.DoKeyword:return parseDoStatement();case SyntaxKind.WhileKeyword:return parseWhileStatement();case SyntaxKind.ForKeyword:return parseForOrForInOrForOfStatement();case SyntaxKind.ContinueKeyword:return parseBreakOrContinueStatement(SyntaxKind.ContinueStatement);case SyntaxKind.BreakKeyword:return parseBreakOrContinueStatement(SyntaxKind.BreakStatement);case SyntaxKind.ReturnKeyword:return parseReturnStatement();case SyntaxKind.WithKeyword:return parseWithStatement();case SyntaxKind.SwitchKeyword:return parseSwitchStatement();case SyntaxKind.ThrowKeyword:return parseThrowStatement();case SyntaxKind.TryKeyword:// Include 'catch' and 'finally' for error recovery.case SyntaxKind.CatchKeyword:case SyntaxKind.FinallyKeyword:return parseTryStatement();case SyntaxKind.DebuggerKeyword:return parseDebuggerStatement();case SyntaxKind.AtToken:return parseDeclaration();case SyntaxKind.AsyncKeyword:case SyntaxKind.InterfaceKeyword:case SyntaxKind.TypeKeyword:case SyntaxKind.ModuleKeyword:case SyntaxKind.NamespaceKeyword:case SyntaxKind.DeclareKeyword:case SyntaxKind.ConstKeyword:case SyntaxKind.EnumKeyword:case SyntaxKind.ExportKeyword:case SyntaxKind.ImportKeyword:case SyntaxKind.PrivateKeyword:case SyntaxKind.ProtectedKeyword:case SyntaxKind.PublicKeyword:case SyntaxKind.AbstractKeyword:case SyntaxKind.StaticKeyword:case SyntaxKind.ReadonlyKeyword:case SyntaxKind.GlobalKeyword:if (isStartOfDeclaration()) {return parseDeclaration();}break;}return parseExpressionOrLabeledStatement();}
4.3.6 createNode
我们上面 可以继续往下分析 比如说 parseEmptyStatement 这个入口来说
function parseEmptyStatement() {var node = createNode(209 /* EmptyStatement */);parseExpected(25 /* SemicolonToken */);return finishNode(node);}
可以发现 node 都是通过 createNode 来进行创建的
// note: this function creates only nodefunction createNode(kind, pos) {nodeCount++;if (!(pos >= 0)) {pos = scanner.getStartPos();}return ts.isNodeKind(kind) ? new NodeConstructor(kind, pos, pos) :kind === 71 /* Identifier */ ? new IdentifierConstructor(kind, pos, pos) :new TokenConstructor(kind, pos, pos);}
通过上面的代码我们 可以得出 createNode 负责创建节点 设置传入的 SyntaxKind(语法类型) 和初始位置(默认使用当前扫描器状态提供位置消息 而 parseExpected 将会检查解析器状态中的当前 token 是否与指定的 SyntaxKind 匹配。如果不匹配将会生成错误报告。
最后一步 finishNode 将会设置节点的 end 位置 并且添加上下文的标志 contextFlags 以及 解析该节点之前出现的错误 (如果有错的话 就不能在增量解析中重用 此AST节点)
