... 2022-6-2 About 32 min

Babel 的核心之一就是其转译模块,包括 babel-core、babel-parser 等。Babel 的转译过程分为"解析/转换/生成"三步。

转译环节 操作 功能
解析 词法分析 生成标记数组(tokens)
语法分析 根据 tokens 数组生成源码的树形结构,即抽象语法树
语义分析 向抽象语法树补充源码的语义信息,如作用域、重复定义检查等
转换 借助 Babel 插件对抽象语法树进行转换 生成目标抽象语法树
生成 根据抽象语法树对象,重新生成代码字符串 声明目标代码字符串

本章主要分为两部分讲解 Babel 相关的转译原理,分别是"理论知识"和"Babel的转译过程"。阅读顺序建议先了解转译原理的理论知识。

《编译原理》本身是大学的计算机相关课程之一,Babel 用到的理论也是基于其中的部分原理,可惜笔者学过的知识早就还给老师了。

为了帮助开发者了解 Babel 的基本原理,Babel 官网推荐了一个项目: the-super-tiny-compiler,除去注释代码仅 200 行,可读性很好,建议阅读,作为了解 Babel 原理的前菜。

# the-super-tiny-compiler

# 解析目标

the-super-tiny-compiler 项目解析的目标是将 lisp 风格的函数调用转换为 C 风格。

假设有两个函数,add 和 subtract,那么它们的写法将会是下面这样:

                LISP                      C

2 + 2          (add 2 2)                 add(2, 2)
4 - 2          (subtract 4 2)            subtract(4, 2)
2 + (4 - 2)    (add 2 (subtract 4 2))    add(2, subtract(4, 2))
1
2
3
4
5

# 解析过程

# 词法单元

the-super-tiny-compiler 定义了如下词法单元:

语素 标记类型
( paren
) paren
/[0-9]/ number
/[a-z]/i name

其中,简略起见,空格没有作为输出的 tokens 中的一员;name 也仅限于字母组成,没有覆盖 add3 这种含字母和数字的情况。

# 代码逻辑

the-super-tiny-compiler 定义了方法 tokenizer(input: string): Array<{ type: String; value: String }> 作为词法分析器。

这个方法不到 100 行,整体运行流程如下:

function tokenizer(input) {
    // `current` 字符串扫描过程中指向当前位置的指针
    var current = 0;

    // `tokens` 数组,也是要返回的对象,数组项为各个 token 单元
    var tokens = [];

    // 首先创建一个 `while` 循环, `current` 变量会在循环中自增
    while (current < input.length) {

        // 我们在这里储存了 `input` 中 `current` 位置的字符
        var char = input[current];

        // token 类型: `paren`,左圆括号
        // 检查一下是不是一个左圆括号
        if (char === '(') {
            // 如果是,那么我们 push 一个 type 为 `paren`,value 为左圆括号的对象。
            tokens.push({
                type: 'paren',
                value: '('
            });

            // 发现了一个 token,自增 `current`
            current++;

            // 结束本次循环,进入下一次循环
            continue;
        }

        // token 类型: `paren`,右圆括号
        // 然后我们检查是不是一个右圆括号。这里做的时候和之前一样: 检查右圆括号、加入新的 token、
        // 自增 `current`,然后进入下一次循环。
        if (char === ')') {
            tokens.push({
                type: 'paren',
                value: ')'
            });

            // 发现了一个 token,自增 `current`
            current++;

            // 结束本次循环,进入下一次循环
            continue;
        }

        // 过滤空格,空格对 token 而言是无意义的
        var WHITESPACE = /\s/;
        if (WHITESPACE.test(char)) {
            // 自增 `current`
            current++;

            // 结束本次循环,进入下一次循环
            continue;
        }

        // token 类型: `number`,数字
        // 下一个 token 的类型是数字。它和之前的一个字符的 token 不同,因为数字可以由多个数字字符组成,
        // 但是只能把它们识别为一个 token。
        // 
        //   (add 123 456)
        //        ^^^ ^^^
        //        这里只有两个 token
        //        
        // 当遇到一个数字字符时,会进入判断逻辑
        var NUMBERS = /[0-9]/;
        if (NUMBERS.test(char)) {
            // 创建一个 `value` 字符串,记录数字字符串
            var value = '';

            // 进入子循环,直到遇到的字符不是数字字符为止,
            // 将循环途中遇到的每个数字拼接到 `value` 字符串中,并且自增 `current`
            while (NUMBERS.test(char)) {
                value += char;
                char = input[++current];
            }

            // 然后将类型为 `number` 的 token 放入 `tokens` 数组中
            tokens.push({
                type: 'number',
                value: value
            });

            // 结束本次循环,进入下一次循环
            continue;
        }

        // token 类型: `name`,函数名
        // 最后一种类型的 token 是 `name`。它由一系列的字母组成,在 lisp 语法中代表函数。
        //
        //   (add 2 4)
        //    ^^^
        //    Name token
        //
        var LETTERS = /[a-z]/i;
        if (LETTERS.test(char)) {
            var value = '';

            // 进入子循环,直到遇到的字符不是字母为止。注意,这里没有考虑含有非字母的函数名写法。
            while (LETTERS.test(char)) {
                value += char;
                char = input[++current];
            }

            // 添加一个类型为 `name` 的 token
            tokens.push({
                type: 'name',
                value: value
            });

            // 结束本次循环,进入下一次循环
            continue;
        }

        // 最后如果没有匹配上任何类型的 token,则抛出一个错误。
        throw new TypeError('I dont know what this character is: ' + char);
    }

    // 词法分析器的最后返回 tokens 数组。
    return tokens;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

image

# 词法分析

the-super-tiny-compiler 项目里,用到了"词法分析"。

词法分析(lexical analysis)是将字符序列转换为标记(token)的过程。进行词法分析的程序或函数叫做词法分析器(lexical analyzer,简称 lexer),也叫扫描器(scanner)。

以英文句子 "today is comfortable." 为例,词法分析用于这个句子中是否有不合格的"单词",以及将"单词"进行分类,这个"单词"也就是标记(token)。

# 相关概念

# 标记(token)

标记(token)是构成源码的最小单位。从输入的字符串流中生成标记的过程叫标记化(tokenization),在这个过程中,词法分析器也会对标记进行分类。

标记(token)通常是一个对象,包括了"种别码"和"属性值"(token<种别码, 属性值>)。

以 the-super-tiny-compiler 输出的 token 为例,"种别码"也就是 "number/paren/name" 等类型的描述,"属性值"(也就是 "123" 等值)。

词法分析器通常不会关注标记之间的关系(这由语法分析器关注),比如,词法分析可以将括号识别为标记(token),但无法保证括号间是否匹配。

在词法分析过程中,会将代码字符串解析为一个个扁平的不可分割的词法单元,比如:

const a = 1;
1

这段代码被词法分析后,会分割为如下词法单元。

属性值 种别码
const const
a name
= eq
1 num
; semi

这里采用的是 Babel 内部定义的种别码,不是普遍标准。

语法分析器读取输入字符流、从中识别出语素、最后生成不同类型的标记。其间一旦发现无效标记,便会报错。

https://tc39.es/ecma262/#prod-ReservedWord

# 标识符(Identifier)

标识符指的是变量、函数、属性的名字、函数的参数。

标识符的书写规则: (规则是哪里来的?)

  • 第一个字符必须是字母、下划线(_)或者是美元符号($),不能为数字

    name  √
    _say  √
    $hi   √
    3name ×
    
    1
    2
    3
    4
  • 第一个字符之后,可以是字母、下划线(_)或者是美元符号($)、数字

    name1  √
    n_ame  √
    n$     √
    
    1
    2
    3
  • 不能将关键字、保留字用作标识符

    name √
    function ×
    
    1
    2
  • 可以使用 Unicode 转义序列。例如,字符 a 可以使用"\u0061"表示

    \u0061 = 1 √
    
    1

# 关键字和保留字

  • 关键字

    关键字是 ECMA-262 规定的 JavaScript 语言内部使用的一组名称(或称为命令)。这些名称具有特定的用途。

    关键字首先是标识名(IdentifierName),同时具有语义(也就是在 JavaScript 中有特殊用途),比如 if/while/async/await 等。

    关键字不能作为标识符(Identifier),比如变量、标签和函数名,以下这些写法是错误的:

    const if = 1; ×
    
    var while = 2; ×
    
    function await() {} ×
    
    1
    2
    3
    4
    5
  • 保留字

    保留字是 ECMA-262 规定的 JavaScript 语言内部预备使用的一组名称(或称为命令)。这些名称目前还没有具体的用途,是为 JavaScript 升级版本预留备用的,建议用户不要使用。

    保留字从字面意思来看,就是"未来的关键字",本身还不具有特殊含义,但将来是可能成为关键字的。

    保留字不能作为标识符(Identifier)。

    保留字分为无条件保留字和有条件保留字。无条件保留字指的是本身严格为保留字,没有任何条件,如 if/while;有条件保留字指的是在特定的上下文中才被视为保留字,比如 await 只是在 async 函数/模块中,才被视为保留字。

  • 关键字与保留字列举

    部分关键字同时也是保留字,部分并不是。有些关键词只在特定的上下文语义中被识别为保留字。

    名称 是关键字 是保留字
    await × 在 async 函数/模块中,才被视为保留字
    break √ √
    case √ √
    catch √ √
    class √ √
    const √ √
    continue √ √
    debugger √ √
    default √ √
    delete √ √
    do √ √
    else √ √
    enum √ √
    export √ √
    extends √ √
    false √ √
    finally √ √
    for √ √
    function √ √
    if √ √
    import √ √
    in √ √
    instanceof √ √
    new √ √
    null √ √
    return √ √
    super √ √
    switch √ √
    this √ √
    throw √ √
    true √ √
    try √ √
    typeof √ √
    var √ √
    void √ √
    while √ √
    with √ √
    yield √ √
    async √ ×
    abstract × √
    double × √
    goto × √
    native × √
    static × √
    boolean × √
    implements × √
    package × √
    byte × √
    private × √
    synchronized × √
    char × √
    extends × √
    int × √
    protected × √
    throws × √
    final × √
    interface × √
    public × √
    transient × √
    float × √
    long × √
    short × √
    volatile × √

# 词法分析过程

总的来说,词法分析分为 2 个步骤: "扫描、标记"。

# 第 1 步: 扫描识别 token

扫描是词法分析的第一阶段。词法分析器会扫描代码字符串,将其分隔为一个个 token,同时也是边扫描边识别 token 的。

这里涉及 2 个问题:

  • 代码字符串是怎样被分隔为一个个 token 的?

    利用有限状态机。

  • 识别 token 的依据是什么?

    利用字符串匹配和正则表达式。

# 有限状态机
  • 问题拆解

    有限状态机(Finite State Mechine),又称有限状态自动机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

    它有如下特征:

    状态总数(state)是有限的 任一时刻,只处在一种状态之中 某种条件下,会从一种状态转变(transition)到另一种状态

    在词法分析中,扫描源码时,遇到不同的字符,会进入不同的"状态",这个"状态"有标识符、操作符、数字、字符串等。整个源码的扫描过程就是不同"状态"的迁移过程。

    比如,待扫描的源码为: height >= 20,我们自定义的状态转译过程:

    image

    其中,涉及的状态有:

    • 标识符: 标记为 name。第一个字符必须是字母,后面的字符可以是字母或数字
    • 比较操作符: 标记为 largerthan,就是 >=
    • 数字字面量: 标记为 num,全部由数字构成

    对于 JavaScript 而言,很显然这些状态的定义不是很完整,比如"数字字面量"还可以包含小数点,我们自定义的状态定义里缺失了这一点,不过对于我们理解有限状态机已经足够了。

    并且,上图并不是真正的优先状态机的描述方式,正确的描述方式是:

    image

  • 状态转移过程

    • 空状态

      刚开始启动或从其他状态返回的默认空状态。

    • 标识符状态

      初始状态时,遇到的第一个字符是字母的话,会转移到"标识符状态"。

      然后,继续"预扫描"后续的字符。

      如果"预扫描"到的下一个字符是字母或数字,则存储该字符到 token 对象的 value 属性,并停留在"标识符状态";

      如果"预扫描"到的下一个字符不是字母或数字,则回退到上一个字符(也就是"回溯"),记录当前 token 为"标识符",并退回"空状态"。

    • 数字字面量状态

      空状态时,遇到的第一个字符是数字的话,会转译到"数字字面量状态"。

      然后,继续"预扫描"后续的字符。

      如果"预扫描"到的下一个字符是数字,则存储该字符到 token 对象的 value 属性,并停留在"数字字面量状态";

      如果"预扫描"到的下一个字符不是数字,则回退到上一个字符(也就是"回溯"),记录当前 token 为"数字字面量状态",并退回"空状态"。

  • 回溯

    在描述状态转译过程时,提到了"回溯"。

    回溯法是一种暴力搜索法,采用“试错”的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现,现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。

  • 总结

    通过上述分析过程,可以看到,词法分析的过程,其实就是根据构造的有限状态自动机,通过回溯等方式,持续进行状态转移,从而解析出各个 token。

    例子中只是包含了 3 个状态的有限状态自动机,要构造一个强大的词法分析器,可以根据场景扩展状态数量。

# 正则表达式

这里的"正则表达式"就是 JavaScript 中的正则表达式,在进行状态判断时,通常用正则表达式判断状态类型,比如上文中我们自定义的状态对应的正则如下:

  • 标识符: /[a-zA-Z]([a-zA-Z_]|[0-9])*/
  • 数字字面量: /\d+/
  • 赋值操作符: /=/

当然,也可以用字符串匹配来识别各类状态,词法分析器大量采用了正则表达式和字符串匹配进行状态识别。

Babel 的词法分析器组合使用了正则表达式/字符串匹配(重度使用了 charcodes 这个 npm 包)进行状态识别,后文会详细介绍。

# 第2步: 标记 token

扫描器识别出可标记的字符序列后,会交由标记生成器处理,标记生成器处理的过程叫标记化(tokenization)。

标记化处理时,会将输入字符串分割为标记、进而将标记进行分类。这样生成的标记会被用于语法分析。

比如字符串: The quick brown fox jumps over the lazy dog。

计算机并不知道这是以空格分隔的九个英语单词,只知道这是普通的 43 个字符构成的字符串。可以通过一定的方法(这里使用空格作为分隔符)将语素(这里即英语单词)从输入字符串中分割出来。分割后的结果用 XML 可以表示如下:

<sentence>
    <word>The</word>
    <word>quick</word>
    <word>brown</word>
    <word>fox</word>
    <word>jumps</word>
    <word>over</word>
    <word>the</word>
    <word>lazy</word>
    <word>dog</word>
</sentence>
1
2
3
4
5
6
7
8
9
10
11

尽管在某些情况下需要手工编写词法分析器,一般情况下词法分析器都用自动化工具生成。

TODO

# Babel 的词法分析

Babel 的词法分析模块定义在 Tokenizer 模块,对应的目录是 babel/packages/babel-parser/src/tokenizer/,并且,Tokenizer 会作为后续语法分析、语义分析的一个中间模块提供服务。

# Babel 的词法单元

Babel 的 Tokenizer 模块定义了基本的词法单元,并深度依赖了第三方模块 charcodes 辅助进行词法分析。

  • 内置词法单元

    文件 babel/packages/babel-parser/src/tokenizer/types.js 列举了 Babel 支持的所有词法单元。

    语素 Babel代码中的助记符 简介 分类
    num num 数字
    bigint bigint 用于表示大于 2^53 - 1 的整数
    decimal decimal 更语义化地表示数字,如 1_000_000
    regexp regexp 正则
    string string 字符串
    name name 名称
    eof eof end of file
    bracketL bracketL [ 字符 标点符号
    bracketHashL bracketHashL #[ 字符 标点符号
    bracketBarL bracketBarL [\| 字符 标点符号
    bracketR bracketR ]字符 标点符号
    bracketBarR bracketBarR \|] 字符 标点符号
    bracketR bracketR }字符 标点符号
    braceL braceL {字符 标点符号
    braceBarL braceBarL {\| 字符 标点符号
    braceHashL braceHashL #{ 字符 标点符号
    braceR braceR }字符 标点符号
    braceBarR braceBarR \|} 字符 标点符号
    parenL parenL ( 字符 标点符号
    parenR parenR ) 字符 标点符号
    comma comma , 字符 标点符号
    semi semi ; 字符 标点符号
    colon colon : 字符 标点符号
    doubleColon doubleColon :: 字符 标点符号
    dot dot . 字符 标点符号
    question question ? 字符 标点符号
    questionDot questionDot ?. 字符 标点符号
    arrow arrow => 字符 标点符号
    template template template 字符 标点符号
    ellipsis ellipsis ... 字符 标点符号
    backQuote backQuote ``` 字符 标点符号
    dollarBraceL dollarBraceL ${ 字符 标点符号
    at at @ 字符
    hash hash # 字符 标点符号
    interpreterDirective interpreterDirective #! 字符
    eq eq =字符 操作符
    assign assign _= 字符 操作符
    slashAssign slashAssign _= 字符 操作符
    incDec incDec ++ / -- 字符 操作符
    bang bang ! 字符 操作符
    tilde tilde ~ 字符 操作符
    pipeline pipeline \|> 字符 操作符
    nullishCoalescing nullishCoalescing ?? 字符 操作符
    logicalOR logicalOR \|\| 字符 操作符
    logicalAND logicalAND && 字符 操作符
    bitwiseOR bitwiseOR \| 字符 操作符
    bitwiseXOR bitwiseXOR ^ 字符 操作符
    bitwiseAND bitwiseAND & 字符 操作符
    equality equality == / != / === / !== 字符 操作符
    relational relational < / > / <= / >= 字符 操作符
    bitShift bitShift << / >> / >>>字符 操作符
    plusMin plusMin + / - 字符 操作符
    modulo modulo % 字符 操作符
    star star * 字符 操作符
    slash slash / 字符 操作符
    exponent exponent ** 字符 操作符
    break _break break 字符 关键词
    case _case case 字符 关键词
    catch _catch catch 字符 关键词
    continue _continue continue 字符 关键词
    debugger _debugger debugger 字符 关键词
    default _default default 字符 关键词
    do _do do字符 关键词
    else _else else字符 关键词
    finally _finally finally 字符 关键词
    for _for for字符 关键词
    function _function function 字符 关键词
    if _if if 字符 关键词
    return _return return 字符 关键词
    switch _switch switch 字符 关键词
    throw _throw throw 字符 关键词
    try _try try 字符 关键词
    var _var var 字符 关键词
    const _const const 字符 关键词
    while _while while 字符 关键词
    with _with with 字符 关键词
    new _new new 字符 关键词
    this _this this 字符 关键词
    super _super super 字符 关键词
    class _class class 字符 关键词
    extends _extends extends 字符 关键词
    export _export export 字符 关键词
    import _import import 字符 关键词
    null _null null 字符 关键词
    true _true true 字符 关键词
    false _false false 字符 关键词
    in _in in 字符 关键词
    instanceof _instanceof instanceof 字符 关键词
    typeof _typeof typeof 字符 关键词
    void _void void 字符 关键词
    delete _delete delete 字符 关键词
  • charcodes

    Babel 利用社区的 npm 包 charcodes 帮助进行词法单元的识别,charcodes 定义的词法单元如下:

    https://compart.com/en/unicode/U+2029

    charcodes 命名 编码 十进制数字 说明
    backSpace U+0008 8
    tab U+0009 9 \t
    lineFeed U+000A 10 \n
    carriageReturn U+000D 13 \r
    shiftOut U+000E 14
    space U+0020 32 空格
    exclamationMark U+0021 33 !
    quotationMark U+0022 34 "
    numberSign U+0023 35 #
    dollarSign U+0024 36 $
    percentSign U+0025 37 %
    ampersand U+0026 38 &
    apostrophe U+0027 39 '
    leftParenthesis U+0028 40 (
    rightParenthesis U+0029 41 )
    asterisk U+002A 42 *
    plusSign U+002B 43 +
    comma U+002C 44 ,
    dash U+002D 45 -
    dot U+002E 46 .
    slash U+002F 47 /
    digit0 U+0030 48 0
    digit1 U+0031 49 1
    digit2 U+0032 50 2
    digit3 U+0033 51 3
    digit4 U+0034 52 4
    digit5 U+0035 53 5
    digit6 U+0036 54 6
    digit7 U+0037 55 7
    digit8 U+0038 56 8
    digit9 U+0039 57 9
    colon U+003A 58 :
    semicolon U+003B 59 \|
    lessThan U+003C 60 <
    equalsTo U+003D 61 =
    greaterThan U+003E 62 >
    questionMark U+003F 63 ?
    atSign U+0040 64 @
    uppercaseA U+0041 65 A
    uppercaseB U+0042 66 B
    uppercaseC U+0043 67 C
    uppercaseD U+0044 68 D
    uppercaseE U+0045 69 E
    uppercaseF U+0046 70 F
    uppercaseG U+0047 71 G
    uppercaseH U+0048 72 H
    uppercaseI U+0049 73 I
    uppercaseJ U+004A 74 J
    uppercaseK U+004B 75 K
    uppercaseL U+004C 76 L
    uppercaseM U+004D 77 M
    uppercaseN U+004E 78 N
    uppercaseO U+004F 79 O
    uppercaseP U+0050 80 P
    uppercaseQ U+0051 81 Q
    uppercaseR U+0052 82 R
    uppercaseS U+0053 83 S
    uppercaseT U+0054 84 T
    uppercaseU U+0055 85 U
    uppercaseV U+0056 86 V
    uppercaseW U+0057 87 W
    uppercaseX U+0058 88 X
    uppercaseY U+0059 89 Y
    uppercaseZ U+005A 90 Z
    leftSquareBracket U+005B 91 [
    backslash U+005C 92 \
    rightSquareBracket U+005D 93 ]
    caret U+005E 94 ^
    underscore U+005F 95 _
    graveAccent U+0060 96 `
    lowercaseA U+0061 97 a
    lowercaseB U+0062 98 b
    lowercaseC U+0063 99 c
    lowercaseD U+0064 100 d
    lowercaseE U+0065 101 e
    lowercaseF U+0066 102 f
    lowercaseG U+0067 103 g
    lowercaseH U+0068 104 h
    lowercaseI U+0069 105 i
    lowercaseJ U+006A 106 j
    lowercaseK U+006B 107 k
    lowercaseL U+006C 108 l
    lowercaseM U+006D 109 m
    lowercaseN U+006E 110 n
    lowercaseO U+006F 111 o
    lowercaseP U+0070 112 p
    lowercaseQ U+0071 113 q
    lowercaseR U+0072 114 r
    lowercaseS U+0073 115 s
    lowercaseT U+0074 116 t
    lowercaseU U+0075 117 u
    lowercaseV U+0076 118 v
    lowercaseW U+0077 119 w
    lowercaseX U+0078 120 x
    lowercaseY U+0079 121 y
    lowercaseZ U+007A 122 z
    leftCurlyBrace U+007B 123 {
    verticalBar U+007C 124 \|
    rightCurlyBrace U+007D 125 }
    tilde U+007E 126 ~
    nonBreakingSpace U+00A0 160 不换行空格
    oghamSpaceMark U+1680 5760
    lineSeparator U+2028 8232
    paragraphSeparator U+2029 8233

# Babel tokenizer 源码解析

# 文件结构

Babel Tokenizer 的核心代码位于目录 github/babel/packages/babel-parser/src/tokenizer/,其中 /tokenizer/index.js 定义了 Tokenizer 类。

文件结构:

|-- babel-parser
    |-- src
        |-- tokenizer
            |-- context.js: 上下文对象 context,与 token 示例关联
            |-- index.js: 定义了 Tokenizer 类,包括各种 token 相关的处理方法和属性
            |-- state.js: 维护了当前 token 实例的状态信息
            |-- types.js: 维护了所有 token 类型
1
2
3
4
5
6
7
# tokendizer/index.js 重点代码

总体而言,tokendizer/index.js 中定义的 Tokenizer 类继承自 PaserErrors 对象,同时定义了关于 token 的各种操作方法和属性。

  • tokens: Array<Token | N.Comment> = [];

    token 数组。数组项可以是 Token 类的实例或者是注释对象。

  • nextToken()

    访问下一个 token。

    笔者在源码中添加了部分注释,便于理解。

    nextToken(): void {
        const curContext = this.curContext();
    
        // 如果不保留空白符和注释,则跳过它们
        // 一般情况下,均认为空格是可以忽略的,不需要识别为 token。有几种类型的 token 是例外:
        // 1. `template`: 模板字符串中的空格是有意义的
        // 2. jsx 中的 `expr`: `<tag>...</tag>` 标签内的空格也是有意义的
        if (!curContext.preserveSpace) this.skipSpace();
    
        this.state.start = this.state.pos;
    
        // 记录访问开始的位置信息
        if (!this.isLookahead) this.state.startLoc = this.state.curPosition();
    
        // 如果目标访问位置超过了源码长度,则认为进入源码文件末尾,也就是 "end-of-file"
        if (this.state.pos >= this.length) {
            this.finishToken(tt.eof);
            return;
        }
    
        // 区分是在读取 template 语法还是非 template 语法(数字、操作符等)
        if (curContext === ct.template) {
            this.readTmplToken();
        } else {
            this.getTokenFromCode(this.codePointAtPos(this.state.pos));
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  • lookahead()

    读取并返回下一个 token 对象。这里用到了简单的"回溯"写法,先保存当前 token 对象,再直接进入并获取下一个 token 对象,最后恢复为当前 token 对象。

    lookahead(): LookaheadState {
        // 保留状态
        const old = this.state;
    
        // 创建一个新的 token state
        this.state = this.createLookaheadState(old);
    
        // 访问下一个 token
        this.isLookahead = true;
        this.nextToken();
        this.isLookahead = false;
    
        const curr = this.state;
    
        // 恢复状态
        this.state = old;
    
        // 返回下一个 token
        return curr;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
  • codePointAtPos()

    TODO

# Babel 定义的 token 类型

词法文法: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Lexical_grammar JavaScript 参考: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference

在介绍词法分析时提到过一个案例:

const a = 1;
1

这段代码被词法分析后,会分割为如下词法单元。

词法单元 token 类型
const const
a name
= eq
1 num
; semi

很显然这些类型对 Babel 来说是不够的,笔者整理了 Babel tokendizer 中的 token 类型。

# "空白"

"空白"包括: 格式控制符、空白符、行终止符:

  • 格式控制符

    用于控制对源码文本的解释,但是并不会显示出来。

  • 空白符

    用于提升源码的可读性,并不影响源码的功能,通常用于将标记(tokens)分开。

  • 行终止符

    除了"空白符"之外,"行终止符"也可以提高源码的可读性。

    不同的是,"行终止符"可以影响 JavaScript 代码的执行。"行终止符"也会影响自动分号补全的执行。在正则表达式中,"行终止符"会被 \s 匹配。

下表是各种"空白"的介绍:

分类 编码 名称 缩写 说明 转义序列
格式控制符 U+200C 零宽不连字 <ZWNJ> 放置在一些经常会被当成连字的字符之间,用于将它们分别以独立形式显示
格式控制符 U+200D 零宽连字 <ZWJ> 放置在一些通常不会被标记为连字的字符之间,用于将这些字符以连字形式显示
格式控制符 U+FEFF 字节流方向标识 <BOM> 在脚本开头使用,除了将脚本标记为 Unicode 格式以外,还用来标记文本的字节流方向
空白符 U+0009 制表符 <HT> 水平制表符,就是TAB \t
空白符 U+000B 垂直制表符 <VT> 垂直制表符,就是垂直方向的TAB \v
空白符 U+000C 分页符 <FF> 分页符 \f
空白符 U+0020 空格 <SP> 空格
空白符 U+00A0 不换行空格 <NBSP> <SP> 的变体,非换行空格,
在文字排版中可以避免因为空格在此处发生断行,在 HTML 中使用的 &nbsp 会生成它
空白符 Others 其他 Unicode 空白 <USP>
行终止符 U+000A 换行符 <LF> 在 UNIX 系统中起新行 \n
行终止符 U+000D 回车符 <CR> 在 Commodore 和早期的 Mac 系统中起新行 \r
行终止符 U+2028 行分隔符 <LS> Unicode 中的行分隔符
行终止符 U+2029 段分隔符 <PS> Unicode 中的段落分隔符

Babel 中,有一个单独的文件提供了"空白"相关的工具方法: babel/packages/babel-parser/src/util/whitespace.js。

其核心代码如下:

const lineBreak = /\r\n?|[\n\u2028\u2029]/;

// https://tc39.github.io/ecma262/#sec-line-terminators
export function isNewLine(code: number): boolean {
    switch (code) {
        case charCodes.lineFeed: // "\n" <LF>
        case charCodes.carriageReturn: // "\r" <CR>
        case charCodes.lineSeparator: // 行分隔符 <LS>
        case charCodes.paragraphSeparator: // 段分隔符 <PS>
            return true;

        default:
            return false;
    }
}

// https://tc39.github.io/ecma262/#sec-white-space
export function isWhitespace(code: number): boolean {
    switch (code) {
        case 0x0009: // CHARACTER TABULATION
        case 0x000b: // LINE TABULATION
        case 0x000c: // FORM FEED
        case charCodes.space: // 空格 <SP>
        case charCodes.nonBreakingSpace: // 不换行空格 <NBSP>
        case charCodes.oghamSpaceMark: // ?
        case 0x2000: // EN QUAD
        case 0x2001: // EM QUAD
        case 0x2002: // EN SPACE
        case 0x2003: // EM SPACE
        case 0x2004: // THREE-PER-EM SPACE
        case 0x2005: // FOUR-PER-EM SPACE
        case 0x2006: // SIX-PER-EM SPACE
        case 0x2007: // FIGURE SPACE
        case 0x2008: // PUNCTUATION SPACE
        case 0x2009: // THIN SPACE
        case 0x200a: // HAIR SPACE
        case 0x202f: // NARROW NO-BREAK SPACE
        case 0x205f: // MEDIUM MATHEMATICAL SPACE
        case 0x3000: // IDEOGRAPHIC SPACE
        case 0xfeff: // ZERO WIDTH NO-BREAK SPACE
            return true;

        default:
            return false;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 注释

注释用来在源码中增加提示、笔记、建议、警告等信息,可以帮助阅读和理解源码。在调试时,可以用来将一段代码屏蔽掉,防止其运行。

在 JavaScript 中,有 3 种添加注释的方法。

  • 单行注释: // 后的文本都视为注释

    // 这是单行注释
    console.log('hello world!');
    
    1
    2
  • 多行注释: /* */

    /*
    * 这是多行注释 1
    * 这是多行注释 2
    */
    console.log('hello world!');
    
    1
    2
    3
    4
    5
  • Hashbang 注释: #!

    #!/usr/bin/env node
    console.log('hello world!');
    
    1
    2

    JavaScript 中的 hashbang 注释模仿 Unix 中的 shebangs,用于指定适当的解释器运行文件,它仅在脚本或模块的绝对开头有效。

    在计算领域中,Shebang(也称为 Hashbang)是一个由井号和叹号构成的字符序列 #!,其出现在文本文件的第一行的前两个字符。 在文件中存在 Shebang 的情况下,类 Unix 操作系统的程序加载器会分析 Shebang 后的内容,将这些内容作为解释器指令,并调用该指令,并将载有 Shebang 的文件路径作为该解释器的参数。

    例如,以指令 #!/bin/sh 开头的文件在执行时会实际调用 /bin/sh 程序。

    下面列出了一些典型的 shebang 解释器指令:

    • #!/bin/sh — 使用 sh,即 Bourne shell或其它兼容shell` 执行脚本
    • #!/bin/csh — 使用 csh,即 C shell 执行
    • #!/usr/bin/perl -w — 使用带警告的 Perl 执行
    • #!/usr/bin/python -O — 使用具有代码优化的 Python 执行
    • #!/usr/bin/php — 使用 PHP 的命令行解释器执行

文件 babel/packages/babel-parser/src/tokenizer/index.js 中有与注释相关的处理方法,tokenizer 对注释的处理方式分为两种:

  • 单行和多行注释,视为"空白"

    skipSpace() 方法会识别注释并添加到 commentStack 数组中。

    识别时,以 / 为关注点,检测下一个字符是 * 或 /。

    如果下一个字符是 *,进入 skipBlockComment() 的执行流程;

    如果下一个字符是 /,进入skipLineComment() 的执行流程。

  • Hashbang 注释的处理方法

    readToken_interpreter(): boolean {
        // 如果字符起始位置不是第一个字符,就直接返回,不做处理
        // 如果长度不够 2,也不做处理
        if (this.state.pos !== 0 || this.length < 2) return false;
    
        let ch = this.input.charCodeAt(this.state.pos + 1);
        if (ch !== charCodes.exclamationMark) return false; // !
    
        const start = this.state.pos;
        this.state.pos += 1;
    
        while (!isNewLine(ch) && ++this.state.pos < this.length) {
            ch = this.input.charCodeAt(this.state.pos);
        }
    
        // 取出 #! xxx 中的 xxx 作为 value 值
        const value = this.input.slice(start + 2, this.state.pos);
    
        this.finishToken(tt.interpreterDirective, value);
    
        return true;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    该方法会识别并取出 #! xxx 中的 xxx 作为该 token 的值。

# 标识符: name

标识符是代码中用来标识变量、函数、或属性的字符序列。

在 JavaScript 中,标识符只能包含字母或数字或下划线("_")或美元符号("$"),且不能以数字开头。

文件 babel/packages/babel-parser/src/tokenizer/index.js 中与标识符识别相关的方法是:

readWord(firstCode: number | void): void {
    const word = this.readWord1(firstCode);
    const type = keywordTypes.get(word) || tt.name;
    this.finishToken(type, word);
},
readWord1(firstCode: numbder | void): string {
    ...
}
1
2
3
4
5
6
7
8
# 数字直接量: num
  • JavaScript 中表示数字的方式

    • 字面量

      使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法: 十进制、十六进制、八进制、二进制。

      • 十进制: 没有前导 0 的数值。

        只有十进制可以带小数和用科学计数法表示。

        带小数时小数点前后部分都可以省略(不能同时省略),如 0.1 / 1. / 2.4。

      • 八进制: 有前缀 0o 或 0O 的数值,或者有前导 0、且只用到 0-7 的八个阿拉伯数字的数值。

      • 十六进制: 有前缀 0x 或 0X 的数值。

      • 二进制: 有前缀 0b 或 0B 的数值。

    • 科学计数法

      科学计数法表示允许字母 e 或 E 的后面,跟着一个整数,表示这个数值的指数部分。

      123e3 // 123000
      123e-3 // 0.123
      -3.1E+12
      .1e-23
      
      1
      2
      3
      4
    • 特殊数值

      • 正零和负零

        在 JavaScript 的 64 位浮点数中,有一个二进制位是符号位,那么,0 也是有一个对应的负值的。+0 和 -0 都指的是 0,只是它们的64位浮点数表示法的符号位不同。

        一般状态下,+0 和 -0 是等价的,会被当做正常的0:

        +0 // 0
        -0 // 0
        (-0).toString() // '0'
        (+0).toString() // '0'
        
        1
        2
        3
        4

        只是,如果 +0 和 -0 作为分母,则有一些区别:

        (1 / +0) === (1 / -0) // false
        
        1

        因为 1 / +0 的结果是 +Infinity,1 / -0 的结果是 -Infinity。

      • NaN

      • Infinity

  • 文件 babel/packages/babel-parser/src/tokenizer/index.js 对数字的处理

    • readInt()

      TODO

    • readRadixNumber()

      TODO

    • readNumber()

      TODO

# 正则表达式直接量: regexp
  • 正则的 flag

    正则有如下 flag: g/m/s/i/y/u/d

    charCodes.lowercaseG
    charCodes.lowercaseM
    charCodes.lowercaseS
    charCodes.lowercaseI
    charCodes.lowercaseY
    charCodes.lowercaseU
    charCodes.lowercaseD
    
    1
    2
    3
    4
    5
    6
    7
  • tokenizer 对正则的处理

    readRegexp(): void {
        const start = this.state.start + 1;
        let escaped, inClass;
        let { pos } = this.state;
        for (; ; ++pos) {
            if (pos >= this.length) {
                throw this.raise(start, Errors.UnterminatedRegExp);
            }
            const ch = this.input.charCodeAt(pos);
            if (isNewLine(ch)) { // 遇到换行符,抛出错误,遇到未完结的正则表达式
                throw this.raise(start, Errors.UnterminatedRegExp);
            }
            if (escaped) {
                escaped = false;
            } else {
                if (ch === charCodes.leftSquareBracket) { // [
                    inClass = true;
                } else if (ch === charCodes.rightSquareBracket && inClass) { // ]
                    inClass = false;
                } else if (ch === charCodes.slash && !inClass) { // /
                    break;
                }
                escaped = ch === charCodes.backslash; // \
            }
        }
        const content = this.input.slice(start, pos); // 内容
        ++pos;
    
        let mods = "";
    
        while (pos < this.length) {
            const cp = this.codePointAtPos(pos);
            // It doesn't matter if cp > 0xffff, the loop will either throw or break because we check on cp
            const char = String.fromCharCode(cp);
    
            if (VALID_REGEX_FLAGS.has(cp)) {
                if (mods.includes(char)) {
                this.raise(pos + 1, Errors.DuplicateRegExpFlags); // 重复的 flag
                }
            } else if (isIdentifierChar(cp) || cp === charCodes.backslash) {
                this.raise(pos + 1, Errors.MalformedRegExpFlags); // 非法正则
            } else {
                break;
            }
    
            ++pos;
            mods += char;
        }
        this.state.pos = pos;
    
        this.finishToken(tt.regexp, {
            pattern: content,
            flags: mods,
        });
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
# 字符串直接量: string
readString(quote: number): void {
    let out = "",
        chunkStart = ++this.state.pos;
    for (;;) {
        if (this.state.pos >= this.length) {
            throw this.raise(this.state.start, Errors.UnterminatedString);
        }
        const ch = this.input.charCodeAt(this.state.pos);
        if (ch === quote) break;
        if (ch === charCodes.backslash) {
            out += this.input.slice(chunkStart, this.state.pos);
            // $FlowFixMe
            out += this.readEscapedChar(false);
            chunkStart = this.state.pos;
        } else if (
            ch === charCodes.lineSeparator ||
            ch === charCodes.paragraphSeparator
        ) {
            ++this.state.pos;
            ++this.state.curLine;
            this.state.lineStart = this.state.pos;
        } else if (isNewLine(ch)) {
            throw this.raise(this.state.start, Errors.UnterminatedString);
        } else {
            ++this.state.pos;
        }
    }
    out += this.input.slice(chunkStart, this.state.pos++);
    this.finishToken(tt.string, out);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 模板字符串: template

模板字符串是允许嵌入表达式的字符串字面量,可以使用多行、表达式引入、标签引入功能。

它有如下形态:

// 单行
`string text`

// 多行
`string text line 1
string text line 2`

// 表达式引入
`string text ${expression} string text`

// 标签引入
tag`string text ${expression} string text`
1
2
3
4
5
6
7
8
9
10
11
12

token 解析时,需要对以下情况做处理。

  • 该代码段第一批字符是 ` 开头的话,会记录为一个 tt.backQuote 类型
  • 该代码段第一批字符是 ${ 开头的话,会记录为一个 tt.dollarBraceL 类型
  • 在扫描该代码段途中,遇到转译标志 \、换行 \r 及 \n 时,会继续扫描检查是否满足对目标格式的要求
# 符号

JavaScript 中有如下符号:

{ ( ) [ ] . ... ; , < > <= >= == != === !== + - * % ** ++ -- << >> >>> & | ^ ! ~ && || ? : = += -= *= %= **= <<= >>= >>>= &= |= ^= => / /= }
1

因需识别字符串模板,{ 和 } 也被视为可能有意义的符号 因需识别除法和正则,/ 也被视为可能有意义的符号

  • .: dot

    当源码遍历过程中,遇到 . 字符时,会调用 readToken_dot 方法。

    .开头的合法 token 的值有两种: 数字(如 .123)、扩展运算符(...)。

    readToken_dot(): void {
        const next = this.input.charCodeAt(this.state.pos + 1);
    
        // 识别 ".123"
        if (next >= charCodes.digit0 && next <= charCodes.digit9) {
            this.readNumber(true);
            return;
        }
    
        // 识别 "..."
        if (next === charCodes.dot && this.input.charCodeAt(this.state.pos + 2) === charCodes.dot) {
            this.state.pos += 3;
            this.finishToken(tt.ellipsis);
        } else {
            ++this.state.pos;
            this.finishToken(tt.dot);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  • /: slash

    当源码遍历过程中,遇到 / 字符时,会调用 readToken_slash方法。

    readToken_slash(): void {
        const next = this.input.charCodeAt(this.state.pos + 1);
        if (next === charCodes.equalsTo) { // /=
            this.finishOp(tt.slashAssign, 2);
        } else {
            this.finishOp(tt.slash, 1);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    该方法可以识别2种token: /=、/。

  • *: star 或 %: modulo

    当源码遍历过程中,遇到 * 或 % 字符时,会调用 readToken_mult_modulo 方法。

    readToken_mult_modulo(code: number): void {
        // '%*'
        let type = code === charCodes.asterisk ? tt.star : tt.modulo; // asterisk: *
        let width = 1;
        let next = this.input.charCodeAt(this.state.pos + 1);
    
        // Exponentiation operator **
        if (code === charCodes.asterisk && next === charCodes.asterisk) {
            width++;
            next = this.input.charCodeAt(this.state.pos + 2);
            type = tt.exponent; // **
        }
    
        if (next === charCodes.equalsTo && !this.state.inType) {
            width++;
            type = tt.assign; // *= / %=
        }
    
        this.finishOp(type, width);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    该方法可以识别 3 种 token: **、*=、%=。

  • | 或 &

    当源码遍历过程中,遇到 | 或 & 字符时,会调用 readToken_pipe_amp 方法。

    该方法可以识别 3 种 token: ||、 、&&、 、||=、 、&&=、|>、|}、|]。

    值得注意的是,该方法内部有对部分插件的引入要求:

    // '|}'
    if (this.hasPlugin("recordAndTuple") && next === charCodes.rightCurlyBrace) {
        // 要求引入插件 babel-plugin-syntax-record-and-tuple,
        // 并且插件的 syntaxType 配置项的值不能是 "bar"
        if (this.getPluginOption("recordAndTuple", "syntaxType") !== "bar") {
            // 没有引入插件,抛出
            throw this.raise(
                this.state.pos,
                Errors.RecordExpressionBarIncorrectEndSyntaxType,
            );
        }
    
        ...
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • ^

    当源码遍历过程中,遇到 ^ 字符时,会调用 readToken_caret 方法。

    readToken_caret(): void {
        const next = this.input.charCodeAt(this.state.pos + 1);
        if (next === charCodes.equalsTo) { // ^=
            this.finishOp(tt.assign, 2);
        } else {
            this.finishOp(tt.bitwiseXOR, 1); // 异或运算符
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    该方法可以识别 2 种 token: ^=、^。

  • + 或 -

    当源码遍历过程中,遇到 + 或 - 字符时,会调用 readToken_plus_min 方法。

    readToken_plus_min(code: number): void {
        // '+-'
        const next = this.input.charCodeAt(this.state.pos + 1);
    
        if (next === code) {
            this.finishOp(tt.incDec, 2); // ++ --
            return;
        }
    
        if (next === charCodes.equalsTo) { // += -=
            this.finishOp(tt.assign, 2);
        } else {
            this.finishOp(tt.plusMin, 1); // + -
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    该方法可以识别6种token: ++、--、+=、-=、+、-。

  • < 或 >

    当源码遍历过程中,遇到 < 或 > 字符时,会调用 readToken_lt_gt 方法。

    readToken_lt_gt(code: number): void {
        // '<>'
        const next = this.input.charCodeAt(this.state.pos + 1);
        let size = 1;
    
        if (next === code) {
            size =
                code === charCodes.greaterThan &&
                this.input.charCodeAt(this.state.pos + 2) === charCodes.greaterThan
                ? 3
                : 2;
    
            if (this.input.charCodeAt(this.state.pos + size) === charCodes.equalsTo) {
                this.finishOp(tt.assign, size + 1);
                return;
            }
            this.finishOp(tt.bitShift, size); // << >>
            return;
        }
    
        if (next === charCodes.equalsTo) {
            // <= | >=
            size = 2;
        }
    
        this.finishOp(tt.relational, size);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    readToken_lt_gt 方法可以识别 6 种 token: <<、>>、<、>、<=、>=。

  • = 或 !

    当源码遍历过程中,遇到 = 或 ! 时,会调用 readToken_eq_excl 方法。

    readToken_eq_excl(code: number): void { // = !
        const next = this.input.charCodeAt(this.state.pos + 1);
        if (next === charCodes.equalsTo) { // == !=
            this.finishOp(
                tt.equality,
                this.input.charCodeAt(this.state.pos + 2) === charCodes.equalsTo
                ? 3
                : 2,
            ); // === !== == !=
            return;
        }
        if (code === charCodes.equalsTo && next === charCodes.greaterThan) { // '=>'
            this.state.pos += 2;
            this.finishToken(tt.arrow);
            return;
        }
        this.finishOp(code === charCodes.equalsTo ? tt.eq : tt.bang, 1);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    readToken_lt_gt方法可以识别7种token: =、!、 ==、!=、===、!==、==>。

  • ?

    当源码遍历过程中,遇到 ? ,会调用 readToken_eq_excl 方法。

    readToken_question(): void {
        const next = this.input.charCodeAt(this.state.pos + 1);
        const next2 = this.input.charCodeAt(this.state.pos + 2);
        if (next === charCodes.questionMark) { // ??
            if (next2 === charCodes.equalsTo) { // ??=
                this.finishOp(tt.assign, 3);
            } else {
                this.finishOp(tt.nullishCoalescing, 2); // ??
            }
        } else if (
            next === charCodes.dot &&
            !(next2 >= charCodes.digit0 && next2 <= charCodes.digit9) // ?.
        ) {
            // '.' not followed by a number
            this.state.pos += 2;
            this.finishToken(tt.questionDot);
        } else {
            ++this.state.pos;
            this.finishToken(tt.question); // ?
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    readToken_lt_gt 方法可以识别 4 种 token: ?、??、??=、?.。

# 词法错误

  • 词法错误发生的时机

    Tokenizer 使用继承自 babel/packages/babel-parser/src/parser/error.js 的 raise() 方法抛出词法分析中扫描到的错误。

    Tokenizer 中抛出词法错误的时机有如下几种场景:

    • 严格模式中发现错误

      严格模式("use strict;")下有一系列的限制条件,Tokenizer 在分析过程中,会尝试发现这些错误并抛出。

      • 禁止出现 0 开头的数字,如 "use strict"; 010;,在词法分析阶段就会发现并抛出该错误,但允许 \\0 开头的数字
      • 禁止出现八进制数字
    • 扫描到的疑似词法单元不满足格式要求

      比如块级注释 /* */ 缺乏闭合标签、hashbang 注释 #! 匹配到了不合适的字符如 #2、正则表达式缺乏闭合标签等。

    • 缺乏必要的 Babel 插件引入

      对于某些特殊词法单元,需要显示引入对应的插件才会开启支持,否则会抛出错误。

      Tokenizer 在解析时,出现过多次对 recordAndTuple 插件的引入要求。

      • #{ 语法要求引入 recordAndTuple 插件且 syntaxType 配置项设置为 hash
      • {| 和 |} 语法要求引入 recordAndTuple 插件且 syntaxType 配置项设置为 bar
    • 未知的词法单元,也就是常见的 UnexpectedToken

      如果 Babel 定义的词法单元都没有被识别出的话,就会抛出 UnexpectedToken 类型的错误,文案描述为 "Unexpected character ..."。

      比如,源码中出现了一个不属于任何词法单元类型的字符时,会抛出 UnexpectedToken 的错误:

      const · = 1;
      
      1

      在词法解析时就会抛出错误: Unexpected character '·'.,因为 · 并不是一个合法的词法单元字符。

  • 词法错误列表

    在词法分析时,是能够发现源码中的一些问题的,比如括号缺少闭合标签等。

    这里列举了 Babel 在词法分析阶段能够发现的错误:

    错误描述定义在文件 babel/packages/babel-parser/src/parser/error-message.js

    • RecordExpressionBarIncorrectStartSyntaxType

      Record expressions starting with '{|' are only allowed when the 'syntaxType' option of the 'recordAndTuple' plugin is set to 'bar'.
      
      1

      {| 语法要求引入 recordAndTuple 插件且 syntaxType 配置项设置为 bar。

    • InvalidOrUnexpectedToken

      Unexpected character '%0'.
      
      1

      出现了非法的 token 字符。

    • UnterminatedRegExp

      Unterminated regular expression.
      
      1

      未闭合的正则表达式。

    • DuplicateRegExpFlags

      Duplicate regular expression flag.
      
      1

      重复的正则 flag。

    • MalformedRegExpFlags

      Invalid regular expression flag.
      
      1

      非法的正则 flag

    • UnexpectedNumericSeparator

      A numeric separator is only allowed between two digits.
      
      1
      1_a
      
      1
      SyntaxError: unknown: A numeric separator is only allowed between two digits. (1:1)
      
      > 1 | 1_a
          |  ^
      
      1
      2
      3
      4
    • NumericSeparatorInEscapeSequence

      Numeric separators are not allowed inside unicode escape sequences or hex escape sequences.
      
      1
    • InvalidDigit

      Expected number in radix %0.
      
      1
    • InvalidDecimal

      Invalid decimal.
      
      1
    • NumberIdentifier

      Identifier directly after number.
      
      1

      无效的小数。

    • InvalidNumber

      Invalid number.
      
      1

      非法数字。

    • ZeroDigitNumericSeparator

      Numeric separator can not be used after leading 0.
      
      1
    • InvalidOrMissingExponent

      Floating-point numbers require a valid exponent after the 'e'.
      
      1
    • InvalidBigIntLiteral

      Invalid BigIntLiteral.
      
      1

      非法的大数字面量。

    • InvalidCodePoint

      Code point out of bounds.
      
      1
    • UnterminatedString

      Unterminated string constant.
      
      1

      未闭合的字符串。

    • InvalidEscapeSequence

      Bad character escape sequence.
      
      1
    • MissingUnicodeEscape

      Expecting Unicode escape sequence \\uXXXX.
      
      1
    • EscapedCharNotAnIdentifier

      Invalid Unicode escape.
      
      1
    • InvalidEscapedReservedWord

      Escape sequence in keyword %0.
      
      1
Last update: October 10, 2022 14:49
Contributors: hoperyy , hoperyy