深入Babel原理系列(二)Parser代码结构简介

上一篇文章分析 Babel 编译流程的时候,提到 Babel 会将 JS 代码转换成 AST(抽象语法树)。这种行为是一种通用的行为,无论什么编程语言都会将源代码解析成 AST,AST 不是 Babel 特有的,更不是 JS 特有的

为什么要这么做呢?原始的 JS 文件是计算机是无法理解的,计算机也很难直接修改 JS 代码,但是转换成 AST 后,由于 AST 本质上是一组表示程序结构的对象,我们可以通过修改这个对象,间接的实现修改代码的目的。chrome V8 引擎也是这么做的,比起 Bable 更进一步的是,V8 引擎会编译 AST 生成字节码。

Parser的过程分为两步,第一步,词法分析,也就是编译原理中的有限状态机,将一段代码拆分为一个个Token,第二步,语法分析,将Token数组,转换为AST树。

这次我就看一下源码,简单分析一下这个过程。

首先看下Babel-Parser的目录结构

主要是个四个文件夹,util,plugins,tokeinzer,parser

入口

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
export function parse(input: string, options?: Options): File {
if (options?.sourceType === "unambiguous") {
options = {
...options,
};
try {
options.sourceType = "module";
const parser = getParser(options, input);
const ast = parser.parse();

//省略部分其他代码

return ast;
} catch (moduleError) {
try {
options.sourceType = "script";
return getParser(options, input).parse();
} catch {}

throw moduleError;
}
} else {
return getParser(options, input).parse();
}
}

这段代码核心就是通过getParser方法获取一个parser,然后用获取的parser去进行解析。

我们再来看一下这个getParser

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
function getParser(options: ?Options, input: string): Parser {
// 获取Parser
let cls = Parser;
// 如果options中声明了插件,首先校验插件的声明方式是否合理,如果合理,则开启插件功能
// 是的,开启插件功能,Parser的插件都是内置的,只能通过配置去选择是否开启这些插件
if (options?.plugins) {
validatePlugins(options.plugins);
cls = getParserClass(options.plugins);
}

return new cls(options, input);
}

const parserClassCache: { [key: string]: Class<Parser> } = {};

/** Get a Parser class with plugins applied. */
function getParserClass(pluginsFromOptions: PluginList): Class<Parser> {
// mixinPluginNames就是所有内置插件的名称
const pluginList = mixinPluginNames.filter(name =>
hasPlugin(pluginsFromOptions, name),
);

// 对当前的插件组合进行缓存
const key = pluginList.join("/");
let cls = parserClassCache[key];
if (!cls) {
cls = Parser;
for (const plugin of pluginList) {
cls = mixinPlugins[plugin](cls);
}
parserClassCache[key] = cls;
}
return cls;
}

Parser解析流程

到现在,我们搞清楚了入口文件的逻辑,主要就是两部分,第一部分声明Parser,第二部分,如果配置了插件,为Parser开启插件功能。

那我们就继续看一下Parser的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class Parser extends StatementParser {
constructor(options: ?Options, input: string) {
}

parse(): File {
this.enterInitialScopes();
const file = this.startNode();
const program = this.startNode();
this.nextToken();
file.errors = null;
this.parseTopLevel(file, program);
file.errors = this.state.errors;
return file;
}
}

构造函数中都是一些准备工作,先不关注,主要逻辑还是在这个parse函数中

1.enterInitialScopes

1
2
3
4
5
6
7
8
enterInitialScopes() {
let paramFlags = PARAM;
if (this.hasPlugin("topLevelAwait") && this.inModule) {
paramFlags |= PARAM_AWAIT;
}
this.scope.enter(SCOPE_PROGRAM);
this.prodParam.enter(paramFlags);
}

这一步,就是初始化一开始的根节点,以及对应的参数和作用域

2. startNode

1
2
3
4
startNode<T: NodeType>(): T {
// $FlowIgnore
return new Node(this, this.state.start, this.state.startLoc);
}

3. nextToken

这部分就是解析的重点了,这一部分的代码会比较复杂,解析过程中会一个个字符得向后解析,利用有限状态机的状态转移去判断不同的状态,最终在到达某种状态,去产生一个token。

如读取到123456这个数字的6时,判断后面是空格或者分号,就生成一个数字的token。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
nextToken(): void {
// curContext = this.state.context[this.state.context.length - 1];
const curContext = this.curContext();
// 内部会不断循环跳过所有的空格,如空格,tab等
if (!curContext.preserveSpace) this.skipSpace();
this.state.start = this.state.pos;
if (!this.isLookahead) this.state.startLoc = this.state.curPosition();
if (this.state.pos >= this.length) {
this.finishToken(tt.eof);
return;
}
if (curContext === ct.template) {
// 读取模板字符串Token
this.readTmplToken();
} else {
// 读取普通Token,codePointAtPos返回的是pos位置的字符的ASCII码
this.getTokenFromCode(this.codePointAtPos(this.state.pos));
}
}

4. parseTopLevel

1
2
3
4
5
6
7
8
parseTopLevel(file: N.File, program: N.Program): N.File {
file.program = this.parseProgram(program);
file.comments = this.state.comments;

if (this.options.tokens) file.tokens = babel7CompatTokens(this.tokens);

return this.finishNode(file, "File");
}

这里会继续调用parseProgram

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
parseProgram(
program: N.Program,
end: TokenType = tt.eof,
sourceType: SourceType = this.options.sourceType,
): N.Program {
program.sourceType = sourceType;
program.interpreter = this.parseInterpreterDirective();
this.parseBlockBody(program, true, true, end);
if (
this.inModule &&
!this.options.allowUndeclaredExports &&
this.scope.undefinedExports.size > 0
) {
for (const [name] of Array.from(this.scope.undefinedExports)) {
const pos = this.scope.undefinedExports.get(name);
// $FlowIssue
this.raise(pos, Errors.ModuleExportUndefined, name);
}
}
return this.finishNode<N.Program>(program, "Program");
}

再调用parseBlockBody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
parseBlockBody(
node: N.BlockStatementLike,
allowDirectives: ?boolean,
topLevel: boolean,
end: TokenType,
afterBlockParse?: (hasStrictModeDirective: boolean) => void,
): void {
const body = (node.body = []);
const directives = (node.directives = []);
this.parseBlockOrModuleBlockBody(
body,
allowDirectives ? directives : undefined,
topLevel,
end,
afterBlockParse,
);
}

继续调用parseBlockOrModuleBlockBody,最终进入递归,通过parserStatement,next等函数去不断递归调用nextToken,直到将一开始parser方法传入的字符串完全解析完。

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
parseBlockOrModuleBlockBody(
body: N.Statement[],
directives: ?(N.Directive[]),
topLevel: boolean,
end: TokenType,
afterBlockParse?: (hasStrictModeDirective: boolean) => void,
): void {
const oldStrict = this.state.strict;
let hasStrictModeDirective = false;
let parsedNonDirective = false;

while (!this.match(end)) {
const stmt = this.parseStatement(null, topLevel);

if (directives && !parsedNonDirective) {
if (this.isValidDirective(stmt)) {
const directive = this.stmtToDirective(stmt);
directives.push(directive);

if (
!hasStrictModeDirective &&
directive.value.value === "use strict"
) {
hasStrictModeDirective = true;
this.setStrict(true);
}

continue;
}
parsedNonDirective = true;
// clear strict errors since the strict mode will not change within the block
this.state.strictErrors.clear();
}
body.push(stmt);
}

if (afterBlockParse) {
afterBlockParse.call(this, hasStrictModeDirective);
}

if (!oldStrict) {
this.setStrict(false);
}

this.next();
}

总结

用一个简单的图来表示,大概就是这样,省略了很多细节

nextToken方法解析

readTmplToken读取模板字符串

这是我根据代码分析出来的状态机

我们尝试下看看结果

getTokenFromCode

这个函数逻辑上来讲不复杂,但是条件分之特别多,因为需要适配各种不同的字符去判断,简单展示下:

charcodes

代码中用到的各种charCodes,是另外一个仓库的内容,这是链接:https://github.com/xtuc/charcodes/blob/master/packages/charcodes/src/index.js

TokenType

而finishToken的参数其实是一个个内置好的TokenType,如tt.parentL其实就是parenL: new TokenType("(", { beforeExpr, startsExpr }),

这些TokenType就是所有babel内置的token类型,而TokenType来源有两个, 一个是Tokenizer内置的,另一种就是parser的plugin提供的,但是我们也说过,parser的plugin对于用户来说只是个开关,所以本质上,所有的TokenType都是babel-praser一开始内置好的。

主要分为四类,一类是变量类型,如number,string,一类是符号,如括号,冒号之类,一类是表达式,如等于,大于,最后就是关键字,如switch,case等

函数逻辑

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
getTokenFromCode(code: number): void {
switch (code) {
// The interpretation of a dot depends on whether it is followed
// by a digit or another two dots.

case charCodes.dot:
this.readToken_dot();
return;

// Punctuation tokens.
case charCodes.leftParenthesis:
++this.state.pos;
this.finishToken(tt.parenL);
return;
case charCodes.rightParenthesis:
++this.state.pos;
this.finishToken(tt.parenR);
return;
case charCodes.semicolon:
++this.state.pos;
this.finishToken(tt.semi);
return;
case charCodes.comma:

// 这里省略几十种条件

default:
if (isIdentifierStart(code)) {
this.readWord(code);
return;
}
}

throw this.raise(
this.state.pos,
Errors.InvalidOrUnexpectedToken,
String.fromCodePoint(code),
);
}

parseTopLevel方法解析

在一开始的流程解析中,我们要看到了,这个函数的主要逻辑在parseBlockOrModuleBlockBody函数中,我们就先来看看这个函数

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
parseBlockOrModuleBlockBody(
body: N.Statement[],
directives: ?(N.Directive[]),
topLevel: boolean,
end: TokenType,
afterBlockParse?: (hasStrictModeDirective: boolean) => void,
): void {
const oldStrict = this.state.strict;
let hasStrictModeDirective = false;
let parsedNonDirective = false;

while (!this.match(end)) {
const stmt = this.parseStatement(null, topLevel);

if (directives && !parsedNonDirective) {
if (this.isValidDirective(stmt)) {
const directive = this.stmtToDirective(stmt);
directives.push(directive);

if (
!hasStrictModeDirective &&
directive.value.value === "use strict"
) {
hasStrictModeDirective = true;
this.setStrict(true);
}

continue;
}
parsedNonDirective = true;
// clear strict errors since the strict mode will not change within the block
this.state.strictErrors.clear();
}
body.push(stmt);
}

if (afterBlockParse) {
afterBlockParse.call(this, hasStrictModeDirective);
}

if (!oldStrict) {
this.setStrict(false);
}

this.next();
}

这个函数看起来不短,但是主要逻辑就是while循环,只要满足!this.match(end)就会一直解析,这个end其实就是tt.eof,也就是我们刚才TokenType中的一种,表示文件结束。

循环体 主要内容就是两个const stmt = this.parseStatement(null, topLevel);body.push(stmt);,这个stmt就是一个AST的Node

parseStatement

1
2
3
4
5
6
parseStatement(context: ?string, topLevel?: boolean): N.Statement {
if (this.match(tt.at)) {
this.parseDecorators(true);
}
return this.parseStatementContent(context, topLevel);
}

第一行就是判断当前是否是@,如果是,那就是装饰器,这个暂时不管

我们看看这个parseStatementContent

parseStatementContent

这个函数就很像刚才的Tokenizer中的getTokenFromCode了,getTokenFromCode是根据code生成各种不同类型的token,而parseStatementContent是根据不同类型的token去生成AST Node。

然后在解析过程中,有些特殊情况,会重新去调用Tokenizer的nextToken继续去生成新的token,比如解析到import

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
parseStatementContent(context: ?string, topLevel: ?boolean): N.Statement {
let starttype = this.state.type;
const node = this.startNode();
let kind;

if (this.isLet(context)) {
starttype = tt._var;
kind = "let";
}

// Most types of statements are recognized by the keyword they
// start with. Many are trivial to parse, some require a bit of
// complexity.

switch (starttype) {
case tt._break:
case tt._continue:
// $FlowFixMe
return this.parseBreakContinueStatement(node, starttype.keyword);
case tt._debugger:
return this.parseDebuggerStatement(node);
case tt._do:
return this.parseDoStatement(node);
case tt._for:
return this.parseForStatement(node);

// 省略各种tokenType的判断

default: {
if (this.isAsyncFunction()) {
if (context) {
this.raise(
this.state.start,
Errors.AsyncFunctionInSingleStatementContext,
);
}
this.next(); // 这里又会去调用Tokenizer的nextToken方法
return this.parseFunctionStatement(node, true, !context);
}
}
}

// If the statement does not start with a statement keyword or a
// brace, it's an ExpressionStatement or LabeledStatement. We
// simply start parsing an expression, and afterwards, if the
// next token is a colon and the expression was a simple
// Identifier node, we switch to interpreting it as a label.
const maybeName = this.state.value;
const expr = this.parseExpression();

if (
starttype === tt.name &&
expr.type === "Identifier" &&
this.eat(tt.colon)
) {
return this.parseLabeledStatement(node, maybeName, expr, context);
} else {
return this.parseExpressionStatement(node, expr);
}
}