用于编写 lint 的常用工具

您可能需要以下工具提示来掌握常用操作。

有用的 Rustc 开发指南链接

检索表达式的类型

有时您可能想检索表达式 Expr 的类型 Ty,例如回答以下问题

  • 此表达式对应于哪种类型(使用其 TyKind)?
  • 它是一个有大小的类型吗?
  • 它是一个原始类型吗?
  • 它是否实现了某个 trait?

此操作使用 expr_ty() 方法执行,该方法来自 TypeckResults 结构,使您可以访问底层结构 Ty

使用示例

#![allow(unused)]
fn main() {
impl LateLintPass<'_> for MyStructLint {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        // Get type of `expr`
        let ty = cx.typeck_results().expr_ty(expr);
        // Match its kind to enter its type
        match ty.kind {
            ty::Adt(adt_def, _) if adt_def.is_struct() => println!("Our `expr` is a struct!"),
            _ => ()
        }
    }
}
}

类似地,在 TypeckResults 方法中,您可以使用 pat_ty() 方法从模式中检索类型。

此处有两项值得注意的地方

  • cx 是 lint 上下文 LateContext。此上下文中两个最有用的数据结构是 tcxLateContext::typeck_results 返回的 TypeckResults,允许我们跳转到类型定义和其他编译阶段(例如 HIR)。
  • typeck_results 的返回值是 TypeckResults,它由类型检查步骤创建,其中包含有用的信息,例如表达式的类型、解析方法的方式等等。

检查 expr 是否在调用特定方法

expr 开始,您可以检查它是否在调用特定的方法 some_method

#![allow(unused)]
fn main() {
impl<'tcx> LateLintPass<'tcx> for MyStructLint {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
        // Check our expr is calling a method
        if let hir::ExprKind::MethodCall(path, _, _self_arg, ..) = &expr.kind
            // Check the name of this method is `some_method`
            && path.ident.name.as_str() == "some_method"
            // Optionally, check the type of the self argument.
            // - See "Checking for a specific type"
        {
                // ...
        }
    }
}
}

检查特定类型

有三种方法可以检查表达式类型是否是我们想要检查的特定类型。所有这些方法都只检查基本类型,泛型参数必须单独检查。

#![allow(unused)]
fn main() {
use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item};
use clippy_utils::{paths, match_def_path};
use rustc_span::symbol::sym;
use rustc_hir::LangItem;

impl LateLintPass<'_> for MyStructLint {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        // Getting the expression type
        let ty = cx.typeck_results().expr_ty(expr);

        // 1. Using diagnostic items
        // The last argument is the diagnostic item to check for
        if is_type_diagnostic_item(cx, ty, sym::Option) {
            // The type is an `Option`
        }

        // 2. Using lang items
        if is_type_lang_item(cx, ty, LangItem::RangeFull) {
            // The type is a full range like `.drain(..)`
        }

        // 3. Using the type path
        // This method should be avoided if possible
        if match_def_path(cx, def_id, &paths::RESULT) {
            // The type is a `core::result::Result`
        }
    }
}
}

尽可能优先使用诊断项和语言项。

检查类型是否实现了特定 trait

有三种方法可以做到这一点,具体取决于目标 trait 是否具有诊断项、语言项或两者都没有。

#![allow(unused)]
fn main() {
use clippy_utils::ty::implements_trait;
use clippy_utils::is_trait_method;
use rustc_span::symbol::sym;

impl LateLintPass<'_> for MyStructLint {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        // 1. Using diagnostic items with the expression
        // we use `is_trait_method` function from Clippy's utils
        if is_trait_method(cx, expr, sym::Iterator) {
            // method call in `expr` belongs to `Iterator` trait
        }

        // 2. Using lang items with the expression type
        let ty = cx.typeck_results().expr_ty(expr);
        if cx.tcx.lang_items()
            // we are looking for the `DefId` of `Drop` trait in lang items
            .drop_trait()
            // then we use it with our type `ty` by calling `implements_trait` from Clippy's utils
            .map_or(false, |id| implements_trait(cx, ty, id, &[])) {
                // `expr` implements `Drop` trait
            }
    }
}
}

如果目标 trait 具有诊断项和语言项,则优先使用它们。

我们通过类型上下文 tcx 访问语言项。 tcx 的类型为 TyCtxt,并在 rustc_middle crate 中定义。可以在 paths.rs 中找到 Clippy 定义的路径列表

检查类型是否定义了特定方法

要检查我们的类型是否定义了一个名为 some_method 的方法

#![allow(unused)]
fn main() {
use clippy_utils::ty::is_type_diagnostic_item;
use clippy_utils::return_ty;

impl<'tcx> LateLintPass<'tcx> for MyTypeImpl {
    fn check_impl_item(&mut self, cx: &LateContext<'tcx>, impl_item: &'tcx ImplItem<'_>) {
        // Check if item is a method/function
        if let ImplItemKind::Fn(ref signature, _) = impl_item.kind
            // Check the method is named `some_method`
            && impl_item.ident.name.as_str() == "some_method"
            // We can also check it has a parameter `self`
            && signature.decl.implicit_self.has_implicit_self()
            // We can go further and even check if its return type is `String`
            && is_type_diagnostic_item(cx, return_ty(cx, impl_item.hir_id), sym!(string_type))
        {
            // ...
        }
    }
}
}

处理宏和展开

请记住,宏已经展开,并且去糖化已经应用于您在 Clippy 中使用的代码表示形式。不幸的是,这会导致很多误报,因为宏展开是“不可见的”,除非您主动检查它们。一般来说,带有宏展开的代码应该被 Clippy 忽略,因为该代码可能会以难以或无法看到的方式动态化。使用以下函数来处理宏

  • span.from_expansion():检测 span 是否来自宏展开或去糖化。检查此项是 lint 中的常见第一步。

    if expr.span.from_expansion() {
        // just forget it
        return;
    }
  • span.ctxt():span 的上下文表示它是否来自展开,如果是,则表示哪个宏调用展开了它。有时检查两个 span 的上下文是否相等很有用。

    // expands to `1 + 0`, but don't lint
    1 + mac!()
    if left.span.ctxt() != right.span.ctxt() {
        // the coder most likely cannot modify this expression
        return;
    }

    注意:非展开的代码位于“根”上下文中。因此,任何 from_expansion 返回 true 的 span 都可以假定具有相同的上下文。因此,仅使用 span.from_expansion() 通常就足够了。

  • in_external_macro(span):检测给定的 span 是否来自在外部 crate 中定义的宏。如果您希望 lint 与宏生成的代码一起使用,这是避免当前 crate 中未定义的宏的下一道防线。 lint 程序员无法更改的代码是没有意义的。

    例如,您可能希望使用它来避免在其他 crate 的宏中开始 linting

    #![allow(unused)]
    fn main() {
    use rustc_middle::lint::in_external_macro;
    
    use a_crate_with_macros::foo;
    
    // `foo` is defined in `a_crate_with_macros`
    foo!("bar");
    
    // if we lint the `match` of `foo` call and test its span
    assert_eq!(in_external_macro(cx.sess(), match_span), true);
    }
  • span.ctxt():span 的上下文表示它是否来自展开,如果是,则表示展开它的内容

    SpanContext 的一个有用之处是检查两个 span 是否在同一个上下文中。例如,在 a == b 中,ab 具有相同的上下文。在带有 a == $bmacro_rules! 中,$b 展开为某个与 a 上下文不同的表达式。

    macro_rules! m {
        ($a:expr, $b:expr) => {
            if $a.is_some() {
                $b;
            }
        }
    }
    
    let x: Option<u32> = Some(42);
    m!(x, x.unwrap());
    
    // These spans are not from the same context
    // x.is_some() is from inside the macro
    // x.unwrap() is from outside the macro
    assert_eq!(x_is_some_span.ctxt(), x_unwrap_span.ctxt());