[Node] 淡如止水 TypeScript (五):语法错误

0. 回顾

0.1 第一篇回顾

在第一篇中,我们克隆了 TypeScript 源码仓库,并配置了调试环境,
VSCode 断点成功停在了 lib/tsc 的第一行。

我们新建了一个 debug/index.ts 文件,作为编译的源代码,内容如下,

const i: number = 1;

0.2 第二篇回顾

第二篇,我们从 lib/tsc 第一行往下单步调试,
解决了 require 无法跳转到 .ts 文件的问题。

我们一路跟踪 TypeScript 从命令行工具 lib/tsc 到 parser 的过程。

lib/tsc -> ts.executeCommandLine -> executeCommandLineWorker -> performCompilation

performCompilation 包含了 TypeScript 编译的两个主要步骤,

createProgram: 创建 Program 对象
emitFilesAndReportErrorsAndGetExitStatus: 写文件

我们就暂时略过了写文件的逻辑,
第二篇文章的后半部分,后面第三、四篇文章,都在介绍 createProgram

TypeScript 每次编译会创建一个 Program,但有可能会创建多个 SourceFile
用于处理每一个待编译的源文件

createProgram     // 创建一个 Program 对象
processRootFile   // 处理每个文件,创建多个 SourceFile 对象
processSourceFile
getSourceFileFromReferenceWorker
getSourceFile
findSourceFile
host.getSourceFile
createSourceFile  // 来到了 parser 中

0.3 第三篇回顾

第三篇,我们从 parser 中的 createSourceFile 开始往下执行,

createSourceFile
Parser.parseSourceFile
parseSourceFileWorker
(nextToken)
parseList

parseList 又是一个关键函数,它是整个语法解析器的入口。
不过语法分析我们放到了第四篇中来介绍。
第三篇中,我们跟进了 parseList 之前的 nextToken 执行过程。

nextToken()token() 是 TypeScript 词法分析器最常见的两个函数,
词法分析器内部保存了状态,token() 用来返回当前在处理的 token (的种类 SyntaxKind),
nextToken() 逻辑非常复杂,覆盖了词法分析扫描下一个 token 是所有细节。

简略介绍 nextToken() 的基本原理的话,
它首先在字符流中,从当前位置,向后扫描一个字符,然后分情况分析,
比如说遇到了一个英文字符 c,就认为它可能是一个标识符或关键字,
然后就继续往后扫描,直到积累到的字符串不再构成标识符位置,比如扫描到了空格。

这样就会扫描到一个完整的,合法的标识符序列,例如 const,将字符串存为 tokenValue
然后,词法分析器会识别出这是一个关键字,并返回该 token 的种类为 SyntaxKind.ConstKeyword

这就是一个简单示例词法分析的全过程了。

0.4 第四篇回顾

第四篇,我们研究了 TypeScript 的语法分析,
从顶层的语法解析函数 parseList 开始往下调试。

TypeScript 采用了手工编写的递归下降解析方法,
AST 的创建过程,由大量的互相调用的 parseXXX 函数来完成。

最顶层的 parseXXX 函数是 parseList,返回了 AST 的根节点,
AST 的每个子树(子节点)都由相应的 parseXXX 函数返回。

在解析的过程中,解析器还可能会调用 nextToken()token()
用以获取下一个或当前的 token,来填充 AST 节点内容。

parseList
  parseDeclaration
    parseVariableStatement
      parseVariableDeclarationList
        parseVariableDeclaration
          parseIdentifierOrPattern
            parseIdentifier
          parseTypeAnnotation
            parseType
          parseInitializer
            parseAssignmentExpressionOrHigher
      parseSemicolon

实际的解析链路会非常长,以上只是粗略写了一些关键的 parseXXX 函数。
语法分析器会通过前瞻(look ahead)来决定使用哪一个函数进行解析。
即,通过自顶向下构造 AST 的方式,实现了产生式的最左推导。

解析是根据给定的文法,结构化一段线性表示的过程。
TypeScript 语法分析,最终目的是创建一棵 AST。

小结

以上我们回顾了前四篇文章的内容,有几个关键点需要一览,

performCompilation          // 执行编译
  createProgram             // 创建 Program 对象
    Parser.parseSourceFile  // 每个文件单独解析,创建 SourceFile 对象
      parseList             // 返回一个 AST
  emitFilesAndReportErrorsAndGetExitStatus

前四篇中,我们已经对 createProgram 的流程打探清楚了,
从本文开始,我们来 emitFilesAndReportErrorsAndGetExitStatus

其中包含了语法检查,语义检查,写文件等等业务逻辑。

1. 回溯到 performCompilation

书接上文,第四篇中我们已经了解了 parseList
它执行完之后返回到了,parseSourceFileWorker 函数中,位于 src/compiler/parser.ts#L858

function parseSourceFileWorker(...): SourceFile {
  ...

  sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);
  Debug.assert(token() === SyntaxKind.EndOfFileToken);
  
  ...
  return sourceFile;
}

看到 parseList 返回后,后面一句的判断,当前 token 就已经是文件结尾了。

完整的调用链路是这样的,

performCompilation
  createProgram
    forEach processRootFile
      processSourceFile
        getSourceFileFromReferenceWorker
          getSourceFile
            findSourceFile
              host.getSourceFile
                createSourceFile
                  Parser.parseSourceFile
                    parseSourceFileWorker
                      parseList
  emitFilesAndReportErrorsAndGetExitStatus

就这样我们一路回到了 performCompilation
在回溯过程中,forEach processRootFile 还会处理另外一些 TypeScript 内置的 .d.ts 文件,
这里就暂且略过不写了。

createProgram 完了之后,TypeScript 就开始执行 emitFilesAndReportErrorsAndGetExitStatus 了,
调用位置位于 src/tsc/executeCommandLine.ts#L515

function performCompilation(
  ...
) {
  ...
  const program = createProgram(programOptions);
  const exitStatus = emitFilesAndReportErrorsAndGetExitStatus(
      ...
  );
  ...
}

2. 语法错误

2.1 全局搜索报错位置

我们知道 TypeScript 在编译的时候,会提示各种可能的错误,例如语法错误、类型错误。
从头跟踪编译过程,然后找到哪里出错,是一件很繁琐的事情。

为此,我们可以先构造一个错误,然后在 TypeScript 报错的位置打个断点,
再通过 VSCode 反查调用栈,得到出错的链路信息。

修改 debug/index.ts 文件的内容如下,

const 0

然后命令行调用 lib/tsc 编译一下,

$ node bin/tsc debug/index.ts
debug/index.ts:1:7 - error TS1134: Variable declaration expected.

1 const 0
        ~


Found 1 error.

得到了以上报错信息。

现在,我们就可以在 TypeScript src/ 文件夹下搜索错误码 1134 了。

搜到了 src/compiler/diagnosticInformationMap.generated.ts#L110 位置的,
Variable_declaration_expected,它是错误信息的 key 值。
根据这个错误信息,我们就可以找出,代码中哪里抛出了这个错误。


我们在这个位置打个断点。

src/compiler/parser.ts#L2095

function parsingContextErrors(context: ParsingContext): DiagnosticMessage {
  switch (context) {
    ...
    case ParsingContext.VariableDeclarations: return Diagnostics.Variable_declaration_expected;
    ...
  }
}

启动调试,程序会停在断点处,


2.2 调用栈

我们可以看到 VSCode 左侧的调用栈信息,


有一些步骤似曾相识,我们点开来看,
parseDelimitedList 之前都是前几篇文章中,我们已经分析过的内容。

parseVariableDeclarationListsrc/compiler/parser.ts#L5763
识别出了 const 关键字,正要开始解析后面的 node.declarations 部分。

function parseVariableDeclarationList(inForStatementInitializer: boolean): VariableDeclarationList {
  const node = <VariableDeclarationList>createNode(SyntaxKind.VariableDeclarationList);

  switch (token()) {
    ...
    case SyntaxKind.ConstKeyword:
      node.flags |= NodeFlags.Const;
      break;
    ...
  }

  nextToken();

  ...
  if (token() === SyntaxKind.OfKeyword && lookAhead(canFollowContextualOfKeyword)) {
    ...
  }
  else {
    ...
    node.declarations = parseDelimitedList(ParsingContext.VariableDeclarations,
      inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation);
    ...
  }

  ...
}

为此调用了 parseDelimitedListsrc/compiler/parser.ts#L2115

function parseDelimitedList<T extends Node>(...): NodeArray<T> {
  ...

  while (true) {
    if (isListElement(kind, /*inErrorRecovery*/ false)) {
      ...
      list.push(parseListElement(kind, parseElement));
      ...
    }

    ...

    if (abortParsingListOrMoveToNextToken(kind)) {
      break;
    }
  }

  ...
  const result = createNodeArray(list, listPos);
  ...
  return result;
}

正常的 parseDelimitedList 会调用 parseListElement 完成后续的解析。
但此时 isListElement 的判断失败了,因此走到了 abortParsingListOrMoveToNextToken 函数中来。

src/compiler/parser.ts#L2074

function abortParsingListOrMoveToNextToken(kind: ParsingContext) {
  parseErrorAtCurrentToken(parsingContextErrors(kind));
  ...
}

接着调用了 parsingContextErrorssrc/compiler/parser.ts#L2084

function parsingContextErrors(context: ParsingContext): DiagnosticMessage {
  switch (context) {
    ...
    case ParsingContext.VariableDeclarations: return Diagnostics.Variable_declaration_expected;
    ...
  }
}

这就是上文搜索到包含错误 key Variable_declaration_expected 的函数了。

因此,TypeScript 在解析过程中,如果遇到了预期之外的 token,
就会跑到错误处理分支,根据错误 key 来记录错误消息。


总结

本文简单展示了 TypeScript 处理语法错误的代码逻辑,实际处理过程会更加的复杂,
包括这些错误信息如何最终展示到控制台中,
以及错误消息的格式化展示问题。

但我觉得目前来看,这件事不是特别的紧急,因此先放下,等有机会再详细了解它。

参考

TypeScript v3.7.3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容