10 个版本 (5 个重大更新)
0.5.7 | 2024年3月12日 |
---|---|
0.5.6 | 2024年3月12日 |
0.4.0 | 2024年1月9日 |
0.3.1 | 2023年11月26日 |
0.0.2 | 2023年9月28日 |
在 开发工具 中排名 1157
每月下载量 10,309
在 13 个 包中使用(直接使用4个)
725KB
16K SLoC
编写解析规则
这是一份简短或不太简短的指南,介绍如何使用 Biome 解析器基础设施实现解析规则。
命名规范
规范是在解析规则前加上 parse_
前缀,然后使用语法文件中定义的名称。
例如,parse_for_statement
或 parse_expression
。
签名规范
大多数解析规则将解析器的引用作为其唯一参数,并返回一个 ParsedSyntax
。
fn parse_rule_name(&mut: Parser) -> ParsedSyntax {}
如果需要,您可以在函数中添加其他参数。在极少数情况下,您可能需要考虑返回 ConditionalParsedSyntax
,如条件语法中所述。
解析单个节点
假设您想解析 JS 中的 if
语句
JsIfStatement =
if
(
test: JsAnyExpression
)
consequent: JsBlockStatement
else_clause: JsElseClause?
存在性测试
现在,解析函数必须首先测试解析器是否位于一个 if
语句上,如果不是,则返回 Absent
。
if !p.at(T![if]) {
return ParsedSyntax::Absent;
}
为什么返回 ParsedSyntax::Absent
?如果规则无法通过下一个标记(或标记序列)预测它们是否能形成预期的节点,则函数必须返回 ParsedSyntax::Absent
。这样做允许调用规则决定这是否是一个错误,并在必要时执行错误恢复。第二个原因是确保规则不会返回所有子节点都缺失的节点。
您的规则实现可能需要考虑的不仅仅是第一个子节点,以确定是否可以解析至少一些预期的子节点。例如,if语句规则可以检查解析器是否位于else
子句中,然后创建一个所有子节点都缺失(除了else
子句)的if语句。
if !p.at(T![if]) && !p.at(T![else]){
return Absent
}
如果第一个子节点是一个节点而不是标记,则实现可以调用另一个解析规则。
let assignment_target = parse_assignment_target(p);
if assignment_target.is_absent() {
return Absent;
}
let my_node = assignment_target.precede_or_missing();
但是要注意调用其他规则。如果返回 Absent
,则规则不得推进解析器 - 意味着它不能在解析过程中前进并消耗标记。
解析子节点
解析规则将指导您如何编写实现,解析器基础设施提供以下便利API
- 可选标记
'ident'?
:使用p.eat(token)
。如果匹配传入的标记,则消费下一个标记。 - 必需标记
'ident'
:使用p.expect(token)
。如果匹配传入的标记,则消费下一个标记。如果标记不在源代码中,则添加一个预期错误和一个缺失标记。 - 可选节点
body: JsBlockStatement?
:使用parse_block_statement(p).or_missing(p)
。如果它在源代码中存在,则解析该块,如果不存在,则添加一个缺失标记。 - 必需节点
body: JsBlockStatement
:使用parse_block_statement(p).or_missing_with_error(p, error_builder):如果它在源代码中存在,则解析块语句,如果不存在,则添加一个缺失标记和错误。
使用上述规则,以下是对if语句规则的实现。
fn parse_if_statement(p: &mut Parser) -> ParsedSyntax {
if !p.at(T![if]) {
return Absent;
}
let m = p.start();
p.expect(T![if]);
p.expect(T!['(']);
parse_any_expression(p).or_add_diagnostic(p, js_parse_errors::expeced_if_statement);
p.expect(T![')']);
parse_block_statement(p).or_add_diagnostic(p, js_parse_errors::expected_block_statement);
// the else block is optional, handle the marker by using `ok`
parse_else_clause(p).ok();
Present(m.complete(p, JS_IF_STATEMENT));
}
等等,这些是什么缺失标记?Biome的AST外观使用固定偏移量从节点检索特定子节点。例如,if语句的第三个子节点是条件。然而,如果源文本中没有开括号(
,则条件将成为第二个元素。这就是缺失元素发挥作用的地方。
解析列表与错误恢复
解析列表与解析具有固定子节点集合的单个元素不同,因为它需要循环,直到解析器到达终止标记(或文件的末尾)。
您可能记得,parse_*
方法在返回 Absent
时不应继续解析。在 while
循环内部,不继续解析解析器是存在问题的,因为这不可避免地会导致无限循环。
这就是为什么在解析列表时必须进行错误恢复。幸运的是,解析器自带了使错误恢复变得容易的基础设施。解析列表的一般结构是(是的,这是解析器基础设施应该为您提供的)
让我们尝试解析一个数组
[ 1, 3, 6 ]
我们将使用 ParseSeparatedList
来实现这一点
struct ArrayElementsList;
impl ParseSeparatedList for ArrayElementsList {
type ParsedElement = CompletedMarker;
fn parse_element(&mut self, p: &mut Parser) -> ParsedSyntax<Self::ParsedElement> {
parse_array_element(p)
}
fn is_at_list_end(&self, p: &mut Parser) -> bool {
p.at_ts(token_set![T![default], T![case], T!['}']])
}
fn recover(
&mut self,
p: &mut Parser,
parsed_element: ParsedSyntax<Self::ParsedElement>,
) -> parser::RecoveryResult {
parsed_element.or_recover(
p,
&ParseRecoveryTokenSet::new(JS_BOGUS_STATEMENT, STMT_RECOVERY_SET),
js_parse_error::expected_case,
)
}
};
让我们一步一步来
parsed_element.or_recover(
p,
&ParseRecoveryTokenSet::new(JS_BOGUS_STATEMENT, STMT_RECOVERY_SET),
js_parse_error::expected_case,
)
or_recover
如果 parse_array_element
方法返回 Absent
,则执行错误恢复;源文本中没有数组元素。
恢复会吃掉所有令牌,直到它找到 token_set
中指定的令牌之一、行断行(如果您调用了 enable_recovery_on_line_break
)或文件结束。
恢复不会丢弃令牌,而是将它们包装在一个 JS_BOGUS_EXPRESSION
节点(第一个参数)中。存在多个 BOGUS_*
节点。您必须查阅语法来了解在您的案例中支持哪些 BOGUS*
节点。
您通常希望将结束列表的终端令牌、元素分隔符令牌和语句结束令牌包含在恢复集中。
现在,恢复的问题在于它可能会失败,有两个原因
- 解析器到达了文件末尾;
- 下一个令牌是恢复集中指定的令牌之一,这意味着没有可以恢复的内容;
在这种情况下,ParseSeparatedList
和 ParseNodeList
将为您恢复解析器。
条件语法
条件语法允许您表达某些语法可能不是所有源文件都有效的。一些用例包括
- 仅在严格或宽松模式下支持的语法:例如,
with
语句在JavaScript文件使用"use strict"
或是模块时是无效的; - 仅在特定文件类型中支持的语法:TypeScript、JSX、模块;
- 仅在特定语言版本中可用的语法:实验性功能、不同版本的编程语言(例如JavaScript的ECMA版本);
想法是解析器始终解析语法,无论该语法是否在此特定文件或上下文中受支持。这样做的主要动机是这为我们提供了完美的错误恢复,并允许我们使用相同的代码,无论语法是否受支持。
然而,必须处理条件语法,因为我们希望如果该语法当前文件不支持,则添加诊断,并且解析的令牌必须附加到某个地方。
让我们看看只有在不严格/宽松模式下允许的 with
语句
fn parse_with_statement(p: &mut Parser) -> ParsedSyntax {
if !p.at(T![with]) {
return Absent;
}
let m = p.start();
p.bump(T![with]); // with
parenthesized_expression(p).or_add_diagnostic(p, js_errors::expected_parenthesized_expression);
parse_statement(p).or_add_diagnostic(p, js_error::expected_statement);
let with_stmt = m.complete(p, JS_WITH_STATEMENT);
let conditional = StrictMode.excluding_syntax(p, with_stmt, |p, marker| {
p.err_builder("`with` statements are not allowed in strict mode", marker.range(p))
});
}
规则的开始与其他规则相同。令人兴奋的部分从
let conditional = StrictMode.excluding_syntax(p, with_stmt, |p, marker| {
p.err_builder("`with` statements are not allowed in strict mode", marker.range(p))
});
StrictMode.excluding_syntax
将解析的语法转换为无效节点,并使用诊断构建器在功能不受支持时创建诊断。
您可以通过调用or_invalid_to_bogus
将ConditionalParsedSyntax
转换为常规的ParsedSyntax
。如果解析器处于严格模式,它将使用BOGUS
节点包装整个with
语句,否则返回未更改的with
语句。
如果不存在与您的解析规则节点匹配的BOGUS
节点,那么您必须返回不带or_invalid_to_bogus
恢复的ConditionalParsedSyntax
。此时,调用者需要恢复可能无效的语法。
摘要
- 解析规则命名为
parse_rule_name
- 解析规则应返回一个
ParsedSyntax
- 如果该规则消耗任何令牌,则必须返回
Present
,因此它可以解析至少包含其子节点的一些节点。 - 否则,它返回
Absent
,并且不能继续解析,也不能添加任何错误。 - 列表必须执行错误恢复以避免无限循环。
- 请参考语法,以确定在您的规则上下文中有效的
BOGUS
节点。
依赖项
~9–19MB
~237K SLoC