1. 概览

Typescript 解析器代码 位于 parser.ts 中。 在内部 由解析器控制扫描器将源码解析成AST。

其实我们在 上面一个篇章中关于 扫描器中也这样解释过 扫描器的入口 是有解析器来控制 解析器实现原理是单例模式。

2. 程序对解析器的使用

解析器由程序间接驱动 简化的调用栈如下所示

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

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

3. demo 示例

想了想 单纯的看代码 还是有些枯燥 这样 我们还是先写个demo 根据输入输出来判断 整个解析器内部 是怎样生成AST的呢

3.1 输入

  1. import * as ts from 'ntypescript';
  2. function printAllChildren(node: ts.Node, depth = 0) {
  3. console.log(new Array(depth + 1).join('----'), ts.formatSyntaxKind(node.kind), node.pos, node.end);
  4. depth++;
  5. node.getChildren().forEach(c => printAllChildren(c, depth));
  6. }
  7. var sourceCode = `const foo = 123;`;
  8. var sourceFile = ts.createSourceFile('foo.ts', sourceCode, ts.ScriptTarget.ES5, true)
  9. console.log(sourceFile)
  10. printAllChildren(sourceFile);

3.2 输出

  1. // SourceFile 0 16
  2. // ---- SyntaxList 0 16
  3. // -------- VariableStatement 0 16
  4. // ------------ VariableDeclarationList 0 15
  5. // ---------------- ConstKeyword 0 5
  6. // ---------------- SyntaxList 5 15
  7. // -------------------- VariableDeclaration 5 15
  8. // ------------------------ Identifier 5 9
  9. // ------------------------ EqualsToken 9 11
  10. // ------------------------ NumericLiteral 11 15
  11. // ------------ SemicolonToken 15 16
  12. // ---- EndOfFileToken 16 16

这其实就是一个 AST 树 当然了关于AST 又是一个可能比较深的话题 我们先看 在typescript 源码中 对于 这颗树的定义

4. 源码解析

4.1 createSourceFile

  1. // parser 的入口
  2. // sourceText const foo = 123;
  3. export function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes = false, scriptKind?: ScriptKind): SourceFile {
  4. performance.mark("beforeParse");
  5. const result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind);
  6. performance.mark("afterParse");
  7. performance.measure("Parse", "beforeParse", "afterParse");
  8. return result;
  9. }

其中 performance 是关于 性能分析的代码 与 主流程无关 我们 只需要 关心 Parser.parseSourceFile 函数

4.2 Parser.parseSourceFile

  1. // sourceText const foo = 123;
  2. export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor, setParentNodes?: boolean, scriptKind?: ScriptKind): SourceFile {
  3. scriptKind = ensureScriptKind(fileName, scriptKind);
  4. // 初始化状态
  5. // scan 扫描器 的初始化
  6. // scanner.setText(text);
  7. initializeState(sourceText, languageVersion, syntaxCursor, scriptKind);
  8. // 解析目标文件
  9. const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes, scriptKind);
  10. // 清除状态
  11. clearState();
  12. return result;
  13. }

其中 initializeState 是 关于 扫描器初始化的函数 我们简单看以下 initializeState 代码

  1. function initializeState(_sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor, scriptKind: ScriptKind) {
  2. // Initialize and prime the scanner before parsing the source elements.
  3. scanner.setText(sourceText);
  4. scanner.setOnError(scanError);
  5. scanner.setScriptTarget(languageVersion);
  6. scanner.setLanguageVariant(getLanguageVariant(scriptKind));
  7. }

4.3 parseSourceFileWorker

我们继续往下走 来到了 parseSourceFileWorker

  1. function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile {
  2. // 创建解析的目标
  3. sourceFile = createSourceFile(fileName, languageVersion, scriptKind);
  4. sourceFile.flags = contextFlags;
  5. // Prime the scanner.
  6. // 新扫描的token替换currentToken
  7. // 执行 scanner.scan();
  8. nextToken();
  9. // 生成每个range的各种信息(包括起点和终点)
  10. processReferenceComments(sourceFile);
  11. sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);
  12. Debug.assert(token() === SyntaxKind.EndOfFileToken);
  13. sourceFile.endOfFileToken = addJSDocComment(parseTokenNode() as EndOfFileToken);
  14. setExternalModuleIndicator(sourceFile);
  15. sourceFile.nodeCount = nodeCount;
  16. sourceFile.identifierCount = identifierCount;
  17. sourceFile.identifiers = identifiers;
  18. sourceFile.parseDiagnostics = parseDiagnostics;
  19. if (setParentNodes) {
  20. fixupParentReferences(sourceFile);
  21. }
  22. return sourceFile;
  23. }

我们看上述代码 可以发现 parseSourceFileWorker 主要完成了下面四个工作

  1. createSourceFile 为我们创建解析的目标
  2. 执行 nextToken 新扫描的 token 替换 currentToken
  3. 执行 processReferenceComments 生成每个range的各种信息(包括起点和终点)
  4. 执行 parseList

4.3.1 createSourceFile 代码如下

  1. function createSourceFile(fileName: string, languageVersion: ScriptTarget, scriptKind: ScriptKind): SourceFile {
  2. // code from createNode is inlined here so createNode won't have to deal with special case of creating source files
  3. // this is quite rare comparing to other nodes and createNode should be as fast as possible
  4. const sourceFile = <SourceFile>new SourceFileConstructor(SyntaxKind.SourceFile, /*pos*/ 0, /* end */ sourceText.length);
  5. nodeCount++;
  6. sourceFile.text = sourceText;
  7. sourceFile.bindDiagnostics = [];
  8. sourceFile.languageVersion = languageVersion;
  9. sourceFile.fileName = normalizePath(fileName);
  10. sourceFile.languageVariant = getLanguageVariant(scriptKind);
  11. sourceFile.isDeclarationFile = fileExtensionIs(sourceFile.fileName, Extension.Dts);
  12. sourceFile.scriptKind = scriptKind;
  13. return sourceFile;
  14. }

在这里 我们正好可以 看下 对于 sourceFile 的 ts 定义

  1. let SourceFileConstructor: new (kind: SyntaxKind, pos: number, end: number) => Node;

4.3.2 nextToken

  1. function nextToken(): SyntaxKind {
  2. return currentToken = scanner.scan();
  3. }

4.3.3 processReferenceComments

对于 processReferenceComments 的分析 或许我们可以先看下输入输入 才能更清楚的知道其中发生了什么

执行 processReferenceComments 之前的sourceFile

  1. bindDiagnostics:(0) []
  2. end:16
  3. fileName:'foo.ts'
  4. flags:0
  5. isDeclarationFile:false
  6. kind:265
  7. languageVariant:0
  8. languageVersion:1
  9. parent:undefined
  10. pos:0
  11. scriptKind:3
  12. text:'const foo = 123;'
  13. transformFlags:undefined
  1. amdDependencies:(0) []
  2. bindDiagnostics:(0) []
  3. checkJsDirective:undefined
  4. end:16
  5. fileName:'foo.ts'
  6. flags:0
  7. isDeclarationFile:false
  8. kind:265
  9. languageVariant:0
  10. languageVersion:1
  11. moduleName:undefined
  12. parent:undefined
  13. pos:0
  14. referencedFiles:(0) []
  15. scriptKind:3
  16. text:'const foo = 123;'
  17. transformFlags:undefined
  18. typeReferenceDirectives:(0) []

看起来好像没什么差别的样式 那也就是说 processReferenceComments 并不是生成AST 的主力

那么我们继续往下看

4.3.4 parseList

  1. function parseList<T extends Node>(kind: ParsingContext, parseElement: () => T): NodeArray<T> {
  2. const saveParsingContext = parsingContext;
  3. parsingContext |= 1 << kind;
  4. const result = createNodeArray<T>();
  5. while (!isListTerminator(kind)) {
  6. if (isListElement(kind, /*inErrorRecovery*/ false)) {
  7. // parseListElement 处理
  8. const element = parseListElement(kind, parseElement);
  9. result.push(element);
  10. continue;
  11. }
  12. if (abortParsingListOrMoveToNextToken(kind)) {
  13. break;
  14. }
  15. }
  16. result.end = getNodeEnd();
  17. parsingContext = saveParsingContext;
  18. return result;
  19. }

其中我们 可以看到 其中element 生成的 还是 要靠 parseListElement 我们继续看代码

  1. function parseListElement<T extends Node>(parsingContext: ParsingContext, parseElement: () => T): T {
  2. const node = currentNode(parsingContext);
  3. if (node) {
  4. return <T>consumeNode(node);
  5. }
  6. return parseElement();
  7. }

其中 parseListElement 最终的生成结果 还是要靠 用户在最开始 传入的 parseElement

好吧 继续

这里的 parseElement 就是 最开始的 parseStatement

它会根据扫描器返回的token 来回切换 (调用相应的parseXXX函数) 例如 当前token是一个 SemicolonToken(分号标记) 就会调用 parseEmptyStatement 为空语句创建一个AST节点

4.3.5 parseStatement

  1. // 在 parseStatement中对token做了switch处理, 根据不同的token获取不同的Node节点。比如我们以最后的; 来做一下简单的判断。
  2. // 首先;对应的 token值应该是 SyntaxKind.SemicolonToken刚刚好是条件判断中的第一个。
  3. function parseStatement(): Statement {
  4. switch (token()) {
  5. case SyntaxKind.SemicolonToken:
  6. return parseEmptyStatement();
  7. case SyntaxKind.OpenBraceToken:
  8. return parseBlock(/*ignoreMissingOpenBrace*/ false);
  9. case SyntaxKind.VarKeyword:
  10. return parseVariableStatement(scanner.getStartPos(), /*decorators*/ undefined, /*modifiers*/ undefined);
  11. case SyntaxKind.LetKeyword:
  12. if (isLetDeclaration()) {
  13. return parseVariableStatement(scanner.getStartPos(), /*decorators*/ undefined, /*modifiers*/ undefined);
  14. }
  15. break;
  16. case SyntaxKind.FunctionKeyword:
  17. return parseFunctionDeclaration(scanner.getStartPos(), /*decorators*/ undefined, /*modifiers*/ undefined);
  18. case SyntaxKind.ClassKeyword:
  19. return parseClassDeclaration(scanner.getStartPos(), /*decorators*/ undefined, /*modifiers*/ undefined);
  20. case SyntaxKind.IfKeyword:
  21. return parseIfStatement();
  22. case SyntaxKind.DoKeyword:
  23. return parseDoStatement();
  24. case SyntaxKind.WhileKeyword:
  25. return parseWhileStatement();
  26. case SyntaxKind.ForKeyword:
  27. return parseForOrForInOrForOfStatement();
  28. case SyntaxKind.ContinueKeyword:
  29. return parseBreakOrContinueStatement(SyntaxKind.ContinueStatement);
  30. case SyntaxKind.BreakKeyword:
  31. return parseBreakOrContinueStatement(SyntaxKind.BreakStatement);
  32. case SyntaxKind.ReturnKeyword:
  33. return parseReturnStatement();
  34. case SyntaxKind.WithKeyword:
  35. return parseWithStatement();
  36. case SyntaxKind.SwitchKeyword:
  37. return parseSwitchStatement();
  38. case SyntaxKind.ThrowKeyword:
  39. return parseThrowStatement();
  40. case SyntaxKind.TryKeyword:
  41. // Include 'catch' and 'finally' for error recovery.
  42. case SyntaxKind.CatchKeyword:
  43. case SyntaxKind.FinallyKeyword:
  44. return parseTryStatement();
  45. case SyntaxKind.DebuggerKeyword:
  46. return parseDebuggerStatement();
  47. case SyntaxKind.AtToken:
  48. return parseDeclaration();
  49. case SyntaxKind.AsyncKeyword:
  50. case SyntaxKind.InterfaceKeyword:
  51. case SyntaxKind.TypeKeyword:
  52. case SyntaxKind.ModuleKeyword:
  53. case SyntaxKind.NamespaceKeyword:
  54. case SyntaxKind.DeclareKeyword:
  55. case SyntaxKind.ConstKeyword:
  56. case SyntaxKind.EnumKeyword:
  57. case SyntaxKind.ExportKeyword:
  58. case SyntaxKind.ImportKeyword:
  59. case SyntaxKind.PrivateKeyword:
  60. case SyntaxKind.ProtectedKeyword:
  61. case SyntaxKind.PublicKeyword:
  62. case SyntaxKind.AbstractKeyword:
  63. case SyntaxKind.StaticKeyword:
  64. case SyntaxKind.ReadonlyKeyword:
  65. case SyntaxKind.GlobalKeyword:
  66. if (isStartOfDeclaration()) {
  67. return parseDeclaration();
  68. }
  69. break;
  70. }
  71. return parseExpressionOrLabeledStatement();
  72. }

4.3.6 createNode

我们上面 可以继续往下分析 比如说 parseEmptyStatement 这个入口来说

  1. function parseEmptyStatement() {
  2. var node = createNode(209 /* EmptyStatement */);
  3. parseExpected(25 /* SemicolonToken */);
  4. return finishNode(node);
  5. }

可以发现 node 都是通过 createNode 来进行创建的

  1. // note: this function creates only node
  2. function createNode(kind, pos) {
  3. nodeCount++;
  4. if (!(pos >= 0)) {
  5. pos = scanner.getStartPos();
  6. }
  7. return ts.isNodeKind(kind) ? new NodeConstructor(kind, pos, pos) :
  8. kind === 71 /* Identifier */ ? new IdentifierConstructor(kind, pos, pos) :
  9. new TokenConstructor(kind, pos, pos);
  10. }

通过上面的代码我们 可以得出 createNode 负责创建节点 设置传入的 SyntaxKind(语法类型) 和初始位置(默认使用当前扫描器状态提供位置消息 而 parseExpected 将会检查解析器状态中的当前 token 是否与指定的 SyntaxKind 匹配。如果不匹配将会生成错误报告。

最后一步 finishNode 将会设置节点的 end 位置 并且添加上下文的标志 contextFlags 以及 解析该节点之前出现的错误 (如果有错的话 就不能在增量解析中重用 此AST节点)