• 特性名称:syntax-tree-patterns
  • 开始日期:2019-03-12
  • RFC PR:#3875

摘要

引入一种特定领域的语言(类似于正则表达式),允许使用语法树模式来描述 lint。

动机

在编写 lint 时,查找具有某些属性的语法树(AST、HIR 等)部分(例如,“具有代码块作为条件的 if 语句”)是一项主要任务。对于非平凡的 lint,它通常需要 AST / HIR 节点的嵌套模式匹配。例如,测试一个表达式是否为布尔文字需要以下检查

if let ast::ExprKind::Lit(lit) = &expr.node {
    if let ast::LitKind::Bool(_) = &lit.node {
        ...
    }
}

编写这种匹配代码很快就会变得复杂,并且生成的代码通常难以理解。下面的代码显示了 collapsible_if lint 所需的模式匹配的简化版本

// simplified version of the collapsible_if lint
if let ast::ExprKind::If(check, then, None) = &expr.node {
    if then.stmts.len() == 1 {
        if let ast::StmtKind::Expr(inner) | ast::StmtKind::Semi(inner) = &then.stmts[0].node {
            if let ast::ExprKind::If(check_inner, content, None) = &inner.node {
                ...
            }
        }
    }
}

if_chain 宏可以通过展平嵌套的 if 语句来提高可读性,但生成的代码仍然很难阅读

#![allow(unused)]
fn main() {
// simplified version of the collapsible_if lint
if_chain! {
    if let ast::ExprKind::If(check, then, None) = &expr.node;
    if then.stmts.len() == 1;
    if let ast::StmtKind::Expr(inner) | ast::StmtKind::Semi(inner) = &then.stmts[0].node;
    if let ast::ExprKind::If(check_inner, content, None) = &inner.node;
    then {
        ...
    }
}
}

上面的代码匹配仅包含另一个 if 表达式的 if 表达式(其中两个 if 都没有 else 分支)。虽然很容易解释 lint 的作用,但从上面的代码示例中很难看出这一点。

遵循上述动机,此 RFC 的首要目标是简化 lint 的编写和阅读

第二个动机是 clippy 对不稳定的编译器内部数据结构的依赖。Clippy lint 当前是针对编译器的 AST / HIR 编写的,这意味着即使这些数据结构中的微小更改也可能会破坏许多 lint。此 RFC 的第二个目标是使 lint 独立于编译器的 AST / HIR 数据结构

方法

目前,编写 lint 的许多复杂性似乎来自于必须手动实现匹配逻辑(请参阅上面的代码示例)。这是一种命令式风格,它描述了如何匹配语法树节点,而不是声明式地指定什么应该匹配。在其他领域,通常使用声明式模式来描述所需的信息,并让实现来完成实际的匹配。这种方法的一个众所周知的例子是正则表达式。与其编写检测特定字符序列的代码,不如使用特定领域的语言来描述搜索模式,并使用该模式搜索匹配项。使用声明式特定领域语言的优势在于,其有限的领域(例如,在正则表达式的情况下匹配字符序列)允许以一种非常自然且富有表现力的方式来表达该领域中的实体。

虽然正则表达式在搜索平面字符序列中的模式时非常有用,但它们不能轻易地应用于像语法树这样的分层数据结构。因此,此 RFC 提出了一种受正则表达式启发并为分层语法树设计的模式匹配系统。

指导级解释

此提案添加了一个 pattern! 宏,可用于指定要搜索的语法树模式。下面显示了一个简单的模式

#![allow(unused)]
fn main() {
pattern!{
    my_pattern: Expr =
        Lit(Bool(false))
}
}

此宏调用定义了一个名为 my_pattern 的模式,可以针对 Expr 语法树节点进行匹配。实际的模式(本例中为 Lit(Bool(false)))定义了哪些语法树应与该模式匹配。此模式匹配值为 false 的布尔文字的表达式。

然后可以使用该模式以以下方式实现 lint

...

impl EarlyLintPass for MyAwesomeLint {
    fn check_expr(&mut self, cx: &EarlyContext, expr: &syntax::ast::Expr) {

        if my_pattern(expr).is_some() {
            cx.span_lint(
                MY_AWESOME_LINT,
                expr.span,
                "This is a match for a simple pattern. Well done!",
            );
        }

    }
}

pattern! 宏调用扩展为一个函数 my_pattern,该函数期望一个语法树表达式作为其参数,并返回一个 Option,指示该模式是否匹配。

注意:结果类型将在后面的部分中详细说明。现在,只需知道如果模式匹配,结果为 Some,否则为 None 就足够了。

模式语法

以下示例演示了模式语法

任何 (_)

最简单的模式是 any 模式。它匹配任何内容,因此类似于正则表达式的 *

#![allow(unused)]
fn main() {
pattern!{
    // matches any expression
    my_pattern: Expr =
        _
}
}

节点 (<node-name>(<args>))

节点用于匹配 AST 节点的特定变体。节点具有一个名称和许多参数,具体取决于节点类型。例如,Lit 节点具有一个描述文字类型的参数。作为另一个示例,If 节点具有三个参数,描述 if 的条件、then 代码块和 else 代码块。

#![allow(unused)]
fn main() {
pattern!{
    // matches any expression that is a literal
    my_pattern: Expr =
        Lit(_)
}

pattern!{
    // matches any expression that is a boolean literal
    my_pattern: Expr =
        Lit(Bool(_))
}

pattern!{
    // matches if expressions that have a boolean literal in their condition
    // Note: The `_?` syntax here means that the else branch is optional and can be anything.
    //       This is discussed in more detail in the section `Repetition`.
    my_pattern: Expr =
        If( Lit(Bool(_)) , _, _?)
}
}

文字 (<lit>)

模式还可以包含 Rust 文字。这些文字匹配它们自身。

#![allow(unused)]
fn main() {
pattern!{
    // matches the boolean literal false
    my_pattern: Expr =
        Lit(Bool(false))
}

pattern!{
    // matches the character literal 'x'
    my_pattern: Expr =
        Lit(Char('x'))
}
}

交替 (a | b)

#![allow(unused)]
fn main() {
pattern!{
    // matches if the literal is a boolean or integer literal
    my_pattern: Lit =
        Bool(_) | Int(_)
}

pattern!{
    // matches if the expression is a char literal with value 'x' or 'y'
    my_pattern: Expr =
        Lit( Char('x' | 'y') )
}
}

空 (())

空模式表示空序列或可选的 None 变体。

#![allow(unused)]
fn main() {
pattern!{
    // matches if the expression is an empty array
    my_pattern: Expr =
        Array( () )
}

pattern!{
    // matches if expressions that don't have an else clause
    my_pattern: Expr =
        If(_, _, ())
}
}

序列 (<a> <b>)

#![allow(unused)]
fn main() {
pattern!{
    // matches the array [true, false]
    my_pattern: Expr =
        Array( Lit(Bool(true)) Lit(Bool(false)) )
}
}

重复 (<a>*, <a>+, <a>?, <a>{n}, <a>{n,m}, <a>{n,})

元素可以重复。指定重复的语法与 正则表达式的语法相同。

#![allow(unused)]
fn main() {
pattern!{
    // matches arrays that contain 2 'x's as their last or second-last elements
    // Examples:
    //     ['x', 'x']                         match
    //     ['x', 'x', 'y']                    match
    //     ['a', 'b', 'c', 'x', 'x', 'y']     match
    //     ['x', 'x', 'y', 'z']               no match
    my_pattern: Expr =
        Array( _* Lit(Char('x')){2} _? )
}

pattern!{
    // matches if expressions that **may or may not** have an else block
    // Attn: `If(_, _, _)` matches only ifs that **have** an else block
    //
    //              | if with else block | if without else block
    // If(_, _, _)  |       match        |       no match
    // If(_, _, _?) |       match        |        match
    // If(_, _, ()) |      no match      |        match
    my_pattern: Expr =
        If(_, _, _?)
}
}

命名子匹配 (<a>#<name>)

#![allow(unused)]
fn main() {
pattern!{
    // matches character literals and gives the literal the name foo
    my_pattern: Expr =
        Lit(Char(_)#foo)
}

pattern!{
    // matches character literals and gives the char the name bar
    my_pattern: Expr =
        Lit(Char(_#bar))
}

pattern!{
    // matches character literals and gives the expression the name baz
    my_pattern: Expr =
        Lit(Char(_))#baz
}
}

使用命名子匹配的原因在结果类型部分中描述。

摘要

下表总结了模式语法

语法概念示例
_任何_
<node-name>(<args>)节点Lit(Bool(true)), If(_, _, _)
<lit>文字'x', false, 101
<a> | <b>交替Char(_) | Bool(_)
()Array( () )
<a> <b>序列Tuple( Lit(Bool(_)) Lit(Int(_)) Lit(_) )
<a>*
<a>+
<a>?
<a>{n}
<a>{n,m}
<a>{n,}
重复





Array( _* ),
Block( Semi(_)+ ),
If(_, _, Block(_)?),
Array( Lit(_){10} ),
Lit(_){5,10},
Lit(Bool(_)){10,}
<a>#<name>命名子匹配Lit(Int(_))#foo Lit(Int(_#bar))

结果类型

许多 lint 需要检查超出上述模式语法可以表达的内容。例如,lint 可能想要检查节点是否作为宏扩展的一部分创建,或者节点上方是否没有注释。另一个示例是 lint 想要匹配两个具有相同值的节点(如 almost_swapped 等 lint 所需)。与其允许用户直接在模式中编写这些检查(这可能会使模式难以阅读),建议的解决方案允许用户为模式表达式的部分分配名称。当将模式与语法树节点匹配时,返回值将包含对这些命名子模式匹配的所有节点的引用。这类似于正则表达式中的捕获组。

例如,给定以下模式

#![allow(unused)]
fn main() {
pattern!{
    // matches character literals
    my_pattern: Expr =
        Lit(Char(_#val_inner)#val)#val_outer
}
}

可以按以下方式获取对与子模式匹配的节点的引用

...
fn check_expr(expr: &syntax::ast::Expr) {
    if let Some(result) = my_pattern(expr) {
        result.val_inner  // type: &char
        result.val        // type: &syntax::ast::Lit
        result.val_outer  // type: &syntax::ast::Expr
    }
}

result 结构中的类型取决于模式。例如,以下模式

#![allow(unused)]
fn main() {
pattern!{
    // matches arrays of character literals
    my_pattern_seq: Expr =
        Array( Lit(_)*#foo )
}
}

匹配由任意数量的文字表达式组成的数组。由于这些表达式被命名为 foo,因此结果结构包含一个 foo 属性,该属性是表达式的向量

...
if let Some(result) = my_pattern_seq(expr) {
    result.foo        // type: Vec<&syntax::ast::Expr>
}

当名称仅在交替的一个分支中定义时,会出现另一种结果类型

#![allow(unused)]
fn main() {
pattern!{
    // matches if expression is a boolean or integer literal
    my_pattern_alt: Expr =
        Lit( Bool(_#bar) | Int(_) )
}
}

在上面的模式中,仅当模式匹配布尔文字时才定义 bar 名称。如果它匹配整数文字,则不会设置该名称。为了解决这个问题,结果结构的 bar 属性是一个选项类型

...
if let Some(result) = my_pattern_alt(expr) {
    result.bar        // type: Option<&bool>
}

如果命名子匹配具有兼容的类型,也可以在多个交替分支中使用它

pattern!{
    // matches if expression is a boolean or integer literal
    my_pattern_mult: Expr =
        Lit(_#baz) | Array( Lit(_#baz) )
}
...
if let Some(result) = my_pattern_mult(expr) {
    result.baz        // type: &syntax::ast::Lit
}

命名子匹配是一个平面命名空间,这是有意为之的。在上面的示例中,两个不同的子结构被分配给一个平面名称。我希望对于大多数 lint 来说,平面命名空间就足够了,并且比分层命名空间更容易使用。

两个阶段

使用命名子模式,用户可以在两个阶段编写 lint。首先,通过模式语法生成可能的匹配项的粗略选择。在第二阶段,可以使用命名子模式引用来执行其他测试,例如断言节点不是作为宏扩展的一部分创建的。

使用模式实现 Clippy lint

作为一个“真实世界”的示例,我使用模式重新实现了 collapsible_if lint。代码可以在此处找到。基于模式的版本通过了为 collapsible_if 编写的所有测试用例。

参考级解释

概述

下图显示了建议解决方案的主要部分之间的依赖关系

                          Pattern syntax
                                |
                                |  parsing / lowering
                                v
                           PatternTree
                                ^
                                |
                                |
                          IsMatch trait
                                |
                                |
             +---------------+-----------+---------+
             |               |           |         |
             v               v           v         v
        syntax::ast     rustc::hir      syn       ...

上一节中描述的模式语法被解析/降低为所谓的 PatternTree 数据结构,该结构表示有效的语法树模式。使用 IsMatch 特征将 PatternTree 与实际语法树(例如,rust ast / hir 或 syn ast 等)进行匹配。

PatternTreeIsMatch 特征将在以下部分中详细介绍。

PatternTree

此 RFC 的核心数据结构是 PatternTree

它是一个类似于 rust 的 AST / HIR 的数据结构,但具有以下差异

  • PatternTree 不包含像 Span 这样的解析信息
  • PatternTree 可以表示交替、序列和可选

下面的代码显示了当前 PatternTree 的简化版本

注意:当前的实现可以在此处找到。

#![allow(unused)]
fn main() {
pub enum Expr {
    Lit(Alt<Lit>),
    Array(Seq<Expr>),
    Block_(Alt<BlockType>),
    If(Alt<Expr>, Alt<BlockType>, Opt<Expr>),
    IfLet(
        Alt<BlockType>,
        Opt<Expr>,
    ),
}

pub enum Lit {
    Char(Alt<char>),
    Bool(Alt<bool>),
    Int(Alt<u128>),
}

pub enum Stmt {
    Expr(Alt<Expr>),
    Semi(Alt<Expr>),
}

pub enum BlockType {
    Block(Seq<Stmt>),
}
}

AltSeqOpt 结构如下所示

注意:当前的实现可以在这里找到。

pub enum Alt<T> {
    Any,
    Elmt(Box<T>),
    Alt(Box<Self>, Box<Self>),
    Named(Box<Self>, ...)
}

pub enum Opt<T> {
    Any,  // anything, but not None
    Elmt(Box<T>),
    None,
    Alt(Box<Self>, Box<Self>),
    Named(Box<Self>, ...)
}

pub enum Seq<T> {
    Any,
    Empty,
    Elmt(Box<T>),
    Repeat(Box<Self>, RepeatRange),
    Seq(Box<Self>, Box<Self>),
    Alt(Box<Self>, Box<Self>),
    Named(Box<Self>, ...)
}

pub struct RepeatRange {
    pub start: usize,
    pub end: Option<usize>  // exclusive
}

解析/降级

pattern!宏调用的输入首先被解析成ParseTree,然后被降级为PatternTree

有效的模式取决于 PatternTree 的定义。例如,模式 Lit(Bool(_)*) 是无效的,因为 Expr 枚举的 Lit 变体的参数类型是 Any<Lit>,因此不支持重复 (*)。另一个例子, Array( Lit(_)* ) 是一个有效的模式,因为 Array 的参数类型是 Seq<Expr>,它允许序列和重复。

注意:模式语法中的名称对应于 PatternTree 枚举的**变体**。例如,上面模式中的 Lit 指的是 Expr 枚举的 Lit 变体 (Expr::Lit),而不是 Lit 枚举。

IsMatch Trait

模式语法和 PatternTree 独立于特定的语法树实现(rust ast / hir, syn, ...)。查看前面章节中的不同模式示例,可以看出这些模式不包含任何特定于某个语法树实现的信息。相反,Clippy lint 当前针对 ast / hir 语法树节点进行匹配,因此直接依赖于它们的实现。

PatternTree 和特定语法树实现之间的连接是 IsMatch trait。它定义了如何将 PatternTree 节点与特定的语法树节点进行匹配。下面展示了 IsMatch trait 的一个简化实现

pub trait IsMatch<O> {
    fn is_match(&self, other: &'o O) -> bool;
}

这个 trait 需要在 PatternTree 的每个枚举上实现(对于相应的语法树类型)。例如,用于匹配 ast::LitKindPatternTreeLit 枚举的 IsMatch 实现可能如下所示

#![allow(unused)]
fn main() {
impl IsMatch<ast::LitKind> for Lit {
    fn is_match(&self, other: &ast::LitKind) -> bool {
        match (self, other) {
            (Lit::Char(i), ast::LitKind::Char(j)) => i.is_match(j),
            (Lit::Bool(i), ast::LitKind::Bool(j)) => i.is_match(j),
            (Lit::Int(i), ast::LitKind::Int(j, _)) => i.is_match(j),
            _ => false,
        }
    }
}
}

所有用于将当前 PatternTreesyntax::ast 进行匹配的 IsMatch 实现都可以在这里找到。

缺点

性能

当前的模式匹配代码没有针对性能进行优化,因此可能比手写的匹配代码慢。此外,两阶段方法(首先匹配粗略模式,然后再检查其他属性)可能比当前在一次传递中检查结构和其他属性的做法慢。例如,以下 lint

pattern!{
    pat_if_without_else: Expr =
        If(
            _,
            Block(
                Expr( If(_, _, ())#inner )
                | Semi( If(_, _, ())#inner )
            )#then,
            ()
        )
}
...
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
    if let Some(result) = pat_if_without_else(expr) {
        if !block_starts_with_comment(cx, result.then) {
            ...
        }
}

首先匹配模式,然后检查 then 代码块是否不以注释开头。使用 clippy 当前的方法,可以更早地检查这些条件

fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
    if_chain! {
        if let ast::ExprKind::If(ref check, ref then, None) = expr.node;
        if !block_starts_with_comment(cx, then);
        if let Some(inner) = expr_block(then);
        if let ast::ExprKind::If(ref check_inner, ref content, None) = inner.node;
        then {
            ...
        }
    }
}

是否会导致性能下降取决于实际的模式。如果事实证明这是一个问题,可以扩展模式匹配算法以允许“提前过滤”(请参阅“未来可能性”中的提前过滤部分)。

话虽如此,我没有看到任何关于模式匹配性能的概念限制。

适用性

尽管我预计可以使用提议的模式语法编写许多 lint,但不太可能所有 lint 都可以用模式表示。我怀疑仍然会有一些 lint 需要通过编写自定义模式匹配代码来实现。这会导致 clippy 的代码库中出现混合,其中一些 lint 使用模式实现,而另一些则不使用。这种不一致性可能会被认为是缺点。

基本原理和替代方案

与当前手动编写匹配代码的方法相比,使用语法树模式指定 lint 有几个优点。首先,语法树模式允许用户以简单且富有表现力的方式描述模式。这使得新手和专家都可以更轻松地编写新的 lint,并且也使读取/修改现有的 lint 更简单。

另一个优点是 lint 独立于特定的语法树实现(例如,AST / HIR,...)。当这些语法树实现发生变化时,只需要调整 IsMatch trait 的实现,而现有的 lint 可以保持不变。这也意味着如果将 IsMatch trait 的实现集成到编译器中,则需要更新 IsMatch 的实现,编译器才能成功编译。这可以减少 Clippy 因编译器更改而中断的次数。模式独立性的另一个优点是,将 EarlyLintPass lint 转换为 LatePassLint 不需要重写整个模式匹配代码。实际上,该模式可能在没有任何调整的情况下正常工作。

替代方案

类似 Rust 的模式语法

提议的模式语法要求用户了解 PatternTree 的结构(它与 AST / HIR 的结构非常相似)以及模式语法。另一种方法是引入一种类似于实际 Rust 语法的模式语法(可能类似于 quote! 宏)。例如,匹配条件中带有 falseif 表达式的模式可能如下所示

if false {
    #[*]
}

问题

扩展 Rust 语法(本身就很复杂)以及指定模式(交替、序列、重复、命名子匹配等)所需的其他语法可能变得难以阅读并且很难正确解析。

例如,匹配两侧都有 0 的二进制运算的模式可能如下所示

0 #[*:BinOpKind] 0

现在考虑这个稍微复杂的例子

1 + 0 #[*:BinOpKind] 0

解析器需要知道 #[*:BinOpKind] 的优先级,因为它会影响结果 AST 的结构。1 + 0 + 0 被解析为 (1 + 0) + 0,而 1 + 0 * 0 被解析为 1 + (0 * 0)。由于模式可以是任何 BinOpKind,因此无法预先知道优先级。

另一个问题的例子是命名子匹配。看看这个模式

fn test() {
    1 #foo
}

#foo 指的是哪个节点? intast::Litast::Exprast::Stmt?在类似 rust 的语法中命名子模式很困难,因为很多 AST 节点没有可以用来放置名称标签的语法元素。在这些情况下,唯一合理的选择是将名称标签分配给最外层的节点(在上面的示例中为 ast::Stmt),因为可以通过最外层的节点检索所有子节点的信息。那么,这样做的问题是,访问内部节点(如 ast::Lit)将再次需要手动模式匹配。

一般来说,Rust 语法隐式地包含了很多代码结构。这种结构在解析期间被重建(例如,二进制运算使用运算符优先级和从左到右进行重建),这也是解析是一项复杂任务的原因之一。这种方法的优点是,用户编写代码更简单。

在编写*语法树模式*时,层次结构的每个元素都可能具有替代方案、重复项等。尊重这一点,同时仍然允许包含隐式结构的人性化语法似乎非常复杂,如果不是不可能的话。

开发这样的语法还需要维护一个至少与 Rust 解析器本身一样复杂的自定义解析器。此外,Rust 语法的未来变化可能与这样的语法不兼容。

总而言之,我认为开发这样的语法会引入很多复杂性来解决一个相对较小的问题。

用户不了解 PatternTree 结构的问题可以通过一个工具来解决,该工具在给定 rust 程序的情况下,生成一个只匹配该程序的模式(类似于 Clippy 作者 lint)。

对于某些简单的情况(如上面的第一个示例),可能会成功混合 Rust 和模式语法。这个空间可以在未来的扩展中进一步探索。

现有技术

模式语法很大程度上受到正则表达式的启发(重复、替代、序列等)。

从我目前所看到的情况来看,其他 linter 也实现了直接在语法树数据结构上工作的 lint,就像 Clippy 目前所做的那样。因此,我将认为模式语法是*新的*,但如果我错了,请纠正我。

未解决的问题

如何处理多个匹配项?

当将语法树节点与模式匹配时,模式可能存在多种匹配方式。一个简单的例子是以下模式

#![allow(unused)]
fn main() {
pattern!{
    my_pattern: Expr =
        Array( _* Lit(_)+#literals)
}
}

此模式匹配以至少一个字面量结尾的数组。现在给定数组 [x, 1, 2]1 应该匹配为模式的 _* 部分还是 Lit(_)+ 部分?区别很重要,因为命名子匹配 #literals 将包含 1 个或 2 个元素,具体取决于模式的匹配方式。在正则表达式中,这个问题通过默认“贪婪”匹配和可选“非贪婪”匹配来解决。

我还没有对此进行太多研究,因为我不知道它对大多数 lint 的相关性如何。当前实现只是返回它找到的第一个匹配项。

未来的可能性

实现剩余的 Rust 语法

当前项目只实现了 Rust 语法的一小部分。将来,应该逐步扩展到更多的语法,以允许实现更多的 lint。实现更多的 Rust 语法需要扩展 PatternTreeIsMatch 的实现,但这应该相对简单。

提前过滤

缺点/性能部分所述,允许在模式匹配期间进行额外的检查可能是有益的。

下面的模式显示了它可能的样子

#![allow(unused)]
fn main() {
pattern!{
    pat_if_without_else: Expr =
        If(
            _,
            Block(
                Expr( If(_, _, ())#inner )
                | Semi( If(_, _, ())#inner )
            )#then,
            ()
        )
    where
        !in_macro(#then.span);
}
}

与当前提出的两阶段过滤相比,区别在于使用提前过滤,条件(在本例中为 !in_macro(#then.span))将在 Block(_)#then 匹配后立即进行评估。

这个领域的另一个想法是引入反向引用的语法。它们可以用来要求模式的多个部分匹配相同的值。例如,搜索 a = a op b 并建议将其更改为 a op= bassign_op_pattern lint 要求 a 的两次出现是相同的。使用 =#... 作为反向引用的语法,lint 可以这样实现

pattern!{
    assign_op_pattern: Expr =
        Assign(_#target, Binary(_, =#target, _)
}

匹配后代

目前许多 lint 实现自定义的访问器,以检查当前节点的任何子树(可能不是直接后代)是否匹配某些属性。这无法用提议的模式语法表达。扩展模式语法以允许诸如“包含至少两个 return 语句的函数”之类的模式可能是一个实用的补充。

可选项的否定运算符

对于像“不是布尔字面量的字面量”这样的模式,目前需要列出除布尔情况外的所有选项。引入一个允许写成 Lit(!Bool(_)) 的否定运算符可能是一个好主意。这个模式将等价于 Lit( Char(_) | Int(_) )(假设目前只实现了三种字面量类型)。

函数式组合

模式目前没有任何组合的概念。这导致模式内部的重复。例如,一个可折叠的 if 模式目前必须这样写:

#![allow(unused)]
fn main() {
pattern!{
    pat_if_else: Expr =
        If(
            _,
            _,
            Block_(
                Block(
                    Expr((If(_, _, _?) | IfLet(_, _?))#else_) |
                    Semi((If(_, _, _?) | IfLet(_, _?))#else_)
                )#block_inner
            )#block
        ) |
        IfLet(
            _,
            Block_(
                Block(
                    Expr((If(_, _, _?) | IfLet(_, _?))#else_) |
                    Semi((If(_, _, _?) | IfLet(_, _?))#else_)
                )#block_inner
            )#block
        )
}
}

如果模式支持定义子模式的函数,则代码可以简化如下:

#![allow(unused)]
fn main() {
pattern!{
    fn expr_or_semi(expr: Expr) -> Stmt {
        Expr(expr) | Semi(expr)
    }
    fn if_or_if_let(then: Block, else: Opt<Expr>) -> Expr {
        If(_, then, else) | IfLet(then, else)
    }
    pat_if_else: Expr =
        if_or_if_let(
            _,
            Block_(
                Block(
                    expr_or_semi( if_or_if_let(_, _?)#else_ )
                )#block_inner
            )#block
        )
}
}

此外,像 expr_or_semi 这样的常见模式可以在不同的 lint 之间共享。

Clippy 模式作者

另一个改进是创建一个工具,给定一些有效的 Rust 语法,生成一个完全匹配此语法的模式。这将使开始编写模式变得更容易。用户可以查看为几个 Rust 代码示例生成的模式,并使用这些信息来编写匹配所有这些示例的模式。

这类似于 clippy 的 author lint。

支持其他语法

大多数提议的系统是与语言无关的。例如,模式语法也可以用于描述其他编程语言的模式。

为了支持其他语言的语法,需要实现另一个 PatternTree,它充分描述了语言的 AST,并为这个 PatternTree 和语言的 AST 实现 IsMatch

这样做的一个方面是,甚至可以编写处理模式语法本身的 lint。例如,当编写以下模式时

#![allow(unused)]
fn main() {
pattern!{
    my_pattern: Expr =
        Array( Lit(Bool(false)) Lit(Bool(false)) )
}
}

一个处理模式语法 AST 的 lint 可以建议使用此模式代替:

#![allow(unused)]
fn main() {
pattern!{
    my_pattern: Expr =
        Array( Lit(Bool(false)){2} )
}
}

将来,Clippy 可以使用此系统为宏中找到的自定义语法提供 lint。