#linter #formatter #parse-error #parser

biome_parser

Biome 解析器共享基础设施

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

Download history 1451/week @ 2024-04-15 814/week @ 2024-04-22 507/week @ 2024-04-29 831/week @ 2024-05-06 986/week @ 2024-05-13 951/week @ 2024-05-20 1101/week @ 2024-05-27 1651/week @ 2024-06-03 1280/week @ 2024-06-10 964/week @ 2024-06-17 809/week @ 2024-06-24 645/week @ 2024-07-01 965/week @ 2024-07-08 1323/week @ 2024-07-15 3608/week @ 2024-07-22 4397/week @ 2024-07-29

每月下载量 10,309
13 包中使用(直接使用4个)

MIT/Apache

725KB
16K SLoC

编写解析规则

这是一份简短或不太简短的指南,介绍如何使用 Biome 解析器基础设施实现解析规则。

命名规范

规范是在解析规则前加上 parse_ 前缀,然后使用语法文件中定义的名称。

例如,parse_for_statementparse_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* 节点。

您通常希望将结束列表的终端令牌、元素分隔符令牌和语句结束令牌包含在恢复集中。

现在,恢复的问题在于它可能会失败,有两个原因

  • 解析器到达了文件末尾;
  • 下一个令牌是恢复集中指定的令牌之一,这意味着没有可以恢复的内容;

在这种情况下,ParseSeparatedListParseNodeList 将为您恢复解析器。

条件语法

条件语法允许您表达某些语法可能不是所有源文件都有效的。一些用例包括

  • 仅在严格或宽松模式下支持的语法:例如,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_bogusConditionalParsedSyntax转换为常规的ParsedSyntax。如果解析器处于严格模式,它将使用BOGUS节点包装整个with语句,否则返回未更改的with语句。

如果不存在与您的解析规则节点匹配的BOGUS节点,那么您必须返回不带or_invalid_to_bogus恢复的ConditionalParsedSyntax。此时,调用者需要恢复可能无效的语法。

摘要

  • 解析规则命名为parse_rule_name
  • 解析规则应返回一个ParsedSyntax
  • 如果该规则消耗任何令牌,则必须返回Present,因此它可以解析至少包含其子节点的一些节点。
  • 否则,它返回Absent,并且不能继续解析,也不能添加任何错误。
  • 列表必须执行错误恢复以避免无限循环。
  • 请参考语法,以确定在您的规则上下文中有效的BOGUS节点。

依赖项

~9–19MB
~237K SLoC