1. 概览

TypeScript 编译器源文件位于 src/compiler 目录下 (不过有一点差别的是 因为我这边参考的文档是 深入理解Typescript 它里面用的源码 是 参考的是 ntypescript 所以在往下面的源码介绍中 我用的也是这个。)

它分为以下几个部分

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

他们的工作线路如下
二 编译器 - 图1

下面我们介绍下 扫描器的流程

2. 解析器对扫描器的使用

因为这里我们其实还没有讲到解析器相关的知识 所以 我们在这里 先知道一个知识点就好了 扫描器的调用入口 其实是在 解析器中的。

下面有一个解析器中的代码简化版

  1. import * as ts from 'ntypescript';
  2. // 单例扫描器
  3. const scanner = ts.createScanner(ts.ScriptTarget.Latest, /* 忽略杂项 */ true);
  4. // 此函数与初始化使用的 `initializeState` 函数相似
  5. function initializeState(text: string) {
  6. scanner.setText(text);
  7. scanner.setOnError((message: ts.DiagnosticMessage, length: number) => {
  8. console.error(message);
  9. });
  10. scanner.setScriptTarget(ts.ScriptTarget.ES5);
  11. scanner.setLanguageVariant(ts.LanguageVariant.Standard);
  12. }
  13. // 使用示例
  14. initializeState(
  15. `
  16. var foo = 123;
  17. `.trim()
  18. );
  19. // 开始扫描
  20. var token = scanner.scan();
  21. while (token != ts.SyntaxKind.EndOfFileToken) {
  22. let currentToken = ts.formatSyntaxKind(token);
  23. let tokenStart = scanner.getStartPos();
  24. token = scanner.scan();
  25. let tokenEnd = scanner.getStartPos();
  26. console.log(currentToken, tokenStart, tokenEnd);
  27. }

运行上面的代码

  1. ConstKeyword 0 5
  2. Identifier 5 9
  3. EqualsToken 9 11
  4. NumericLiteral 11 15
  5. SemicolonToken 15 16

2.1 createScanner

  1. // Creates a scanner over a (possibly unspecified) range of a piece of text.
  2. export function createScanner(languageVersion: ScriptTarget,
  3. skipTrivia: boolean,
  4. languageVariant = LanguageVariant.Standard,
  5. text?: string,
  6. onError?: ErrorCallback,
  7. start?: number,
  8. length?: number): Scanner {
  9. // Current position (end position of text of current token)
  10. let pos: number;
  11. // end of text
  12. let end: number;
  13. // Start position of whitespace before current token
  14. let startPos: number;
  15. // Start position of text of current token
  16. let tokenPos: number;
  17. let token: SyntaxKind;
  18. let tokenValue: string;
  19. let precedingLineBreak: boolean;
  20. let hasExtendedUnicodeEscape: boolean;
  21. let tokenIsUnterminated: boolean;
  22. let numericLiteralFlags: NumericLiteralFlags;
  23. setText(text, start, length);
  24. return {
  25. getStartPos: () => startPos,
  26. getTextPos: () => pos,
  27. getToken: () => token,
  28. getTokenPos: () => tokenPos,
  29. getTokenText: () => text.substring(tokenPos, pos),
  30. getTokenValue: () => tokenValue,
  31. hasExtendedUnicodeEscape: () => hasExtendedUnicodeEscape,
  32. hasPrecedingLineBreak: () => precedingLineBreak,
  33. isIdentifier: () => token === SyntaxKind.Identifier || token > SyntaxKind.LastReservedWord,
  34. isReservedWord: () => token >= SyntaxKind.FirstReservedWord && token <= SyntaxKind.LastReservedWord,
  35. isUnterminated: () => tokenIsUnterminated,
  36. getNumericLiteralFlags: () => numericLiteralFlags,
  37. // ...,
  38. scan,
  39. getText,
  40. setText,
  41. setScriptTarget,
  42. setLanguageVariant,
  43. setOnError,
  44. setTextPos,
  45. tryScan,
  46. lookAhead,
  47. scanRange,
  48. };

我们通过 createScanner 创建扫描器之后 是通过 scan 方法来进行源代码的扫描操作 我们可以继续往下看 scan 函数的执行逻辑

2.2 scan

首先 我们先根据定义来看

  1. scan(): SyntaxKind;

我们可以看到 scan 方法执行完成之后 最后返回的是 SyntaxKind

SyntaxKind 是一个枚举 代码如下

  1. export const enum SyntaxKind {
  2. Unknown,
  3. EndOfFileToken,
  4. SingleLineCommentTrivia,
  5. MultiLineCommentTrivia,
  6. NewLineTrivia,
  7. WhitespaceTrivia,
  8. // We detect and preserve #! on the first line
  9. ShebangTrivia,
  10. // We detect and provide better error recovery when we encounter a git merge marker. This
  11. // allows us to edit files with git-conflict markers in them in a much more pleasant manner.
  12. ConflictMarkerTrivia,
  13. // Literals
  14. NumericLiteral,
  15. StringLiteral,
  16. JsxText,
  17. // ...
  18. }

我们可以看到返回的是Token 的定义相关的内容 也就是 词法关键词的枚举

接下来我们看下 scan 的具体执行

  1. function scan(): SyntaxKind {
  2. startPos = pos;
  3. hasExtendedUnicodeEscape = false;
  4. precedingLineBreak = false;
  5. tokenIsUnterminated = false;
  6. numericLiteralFlags = 0;
  7. while (true) {
  8. tokenPos = pos;
  9. if (pos >= end) {
  10. return token = SyntaxKind.EndOfFileToken;
  11. }
  12. let ch = text.charCodeAt(pos);
  13. // Special handling for shebang
  14. if (ch === CharacterCodes.hash && pos === 0 && isShebangTrivia(text, pos)) {
  15. pos = scanShebangTrivia(text, pos);
  16. if (skipTrivia) {
  17. continue;
  18. }
  19. else {
  20. return token = SyntaxKind.ShebangTrivia;
  21. }
  22. }
  23. switch (ch) {
  24. case CharacterCodes.lineFeed:
  25. case CharacterCodes.carriageReturn:
  26. precedingLineBreak = true;
  27. if (skipTrivia) {
  28. pos++;
  29. continue;
  30. }
  31. else {
  32. if (ch === CharacterCodes.carriageReturn && pos + 1 < end && text.charCodeAt(pos + 1) === CharacterCodes.lineFeed) {
  33. // consume both CR and LF
  34. pos += 2;
  35. }
  36. else {
  37. pos++;
  38. }
  39. return token = SyntaxKind.NewLineTrivia;
  40. }
  41. // ...
  42. default:
  43. if (isIdentifierStart(ch, languageVersion)) {
  44. pos++;
  45. while (pos < end && isIdentifierPart(ch = text.charCodeAt(pos), languageVersion)) pos++;
  46. tokenValue = text.substring(tokenPos, pos);
  47. if (ch === CharacterCodes.backslash) {
  48. tokenValue += scanIdentifierParts();
  49. }
  50. return token = getIdentifierToken();
  51. }
  52. else if (isWhiteSpaceSingleLine(ch)) {
  53. pos++;
  54. continue;
  55. }
  56. else if (isLineBreak(ch)) {
  57. precedingLineBreak = true;
  58. pos++;
  59. continue;
  60. }
  61. error(Diagnostics.Invalid_character);
  62. pos++;
  63. return token = SyntaxKind.Unknown;
  64. }
  65. }
  66. }

我们读这段代码 可以发现 其中对于整个 scan 逻辑 处理最重要的就是 let ch = text.charCodeAt(pos); 通过生成unicode 编码 从而得到扫描的结果

下面我们 简单分析 下 const foo = 123; 是如何解析出来的 首先 我们 const 走的是 default 会把 const 取出来 然后 走 return token = getIdentifierToken

  1. function getIdentifierToken(): SyntaxKind {
  2. // Reserved words are between 2 and 11 characters long and start with a lowercase letter
  3. const len = tokenValue.length;
  4. if (len >= 2 && len <= 11) {
  5. const ch = tokenValue.charCodeAt(0);
  6. if (ch >= CharacterCodes.a && ch <= CharacterCodes.z) {
  7. token = textToToken.get(tokenValue);
  8. if (token !== undefined) {
  9. return token;
  10. }
  11. }
  12. }
  13. return token = SyntaxKind.Identifier;
  14. }

我们发现 整个 const 属于 判断条件 之后 走的是 token = textToToken.get(tokenValue);

  1. const textToToken = createMapFromTemplate({
  2. "abstract": SyntaxKind.AbstractKeyword,
  3. "any": SyntaxKind.AnyKeyword,
  4. "as": SyntaxKind.AsKeyword,
  5. "boolean": SyntaxKind.BooleanKeyword,
  6. "break": SyntaxKind.BreakKeyword,
  7. "case": SyntaxKind.CaseKeyword,
  8. "catch": SyntaxKind.CatchKeyword,
  9. "class": SyntaxKind.ClassKeyword,
  10. "continue": SyntaxKind.ContinueKeyword,
  11. "const": SyntaxKind.ConstKeyword,
  12. "constructor": SyntaxKind.ConstructorKeyword,
  13. "debugger": SyntaxKind.DebuggerKeyword,
  14. // ...
  15. });

最后 我们 可以 得到如下结果

  • const -> ConstKeyword
  • foo -> Identifier
  • = -> EqualsToken
  • 123 -> NumericLiterals
  • ; -> SemicolonToken

3. 总结

在scan 函数中包含了 对各个符号的解析方法 有兴趣的话 可以自己打断点研究下