特征检查

除了类型检查,在实现 lint 时,我们可能还需要检查特定类型 Ty 是否实现了某个特定的 trait。有三种方法可以实现这一点,具体取决于我们要检查的目标 trait 是否具有诊断项语言项,或者两者都没有。

使用诊断项

正如Rust 编译器开发指南中所解释的那样,诊断项用于通过符号来识别类型。

例如,如果我们想检查一个表达式是否实现了 Iterator trait,我们可以简单地编写以下代码,提供 LateContext (cx)、我们手头的表达式和所讨论 trait 的符号

#![allow(unused)]
fn main() {
use clippy_utils::is_trait_method;
use rustc_hir::Expr;
use rustc_lint::{LateContext, LateLintPass};
use rustc_span::symbol::sym;

impl LateLintPass<'_> for CheckIteratorTraitLint {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
		let implements_iterator = cx.tcx.get_diagnostic_item(sym::Iterator).map_or(false, |id| {
    		implements_trait(cx, cx.typeck_results().expr_ty(arg), id, &[])
		});
		if implements_iterator {
			// [...]
		}

    }
}
}

注意:请参考此索引以获取所有已定义的 Symbol

使用语言项

除了诊断项,我们还可以使用lang_items。查看文档会发现 LanguageItems 包含编译器中定义的所有语言项。

使用它的 *_trait 方法之一,我们可以获得任何特定项的 DefId,例如 CloneCopyDropEq,这些对于许多 Rustaceans 来说都很熟悉。

例如,如果我们想检查一个表达式 expr 是否实现了 Drop trait,我们可以通过 LateContextTyCtxt 访问 LanguageItems,它提供了一个 lang_items 方法,该方法将返回 Drop trait 的 id 给我们。然后,通过调用 Clippy 工具函数 implements_trait,我们可以检查 exprTy 是否实现了该 trait

#![allow(unused)]
fn main() {
use clippy_utils::implements_trait;
use rustc_hir::Expr;
use rustc_lint::{LateContext, LateLintPass};

impl LateLintPass<'_> for CheckDropTraitLint {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        let ty = cx.typeck_results().expr_ty(expr);
        if cx.tcx.lang_items()
            .drop_trait()
            .map_or(false, |id| implements_trait(cx, ty, id, &[])) {
                println!("`expr` implements `Drop` trait!");
            }
    }
}
}

使用类型路径

如果既没有诊断项也没有语言项可用,我们可以使用带有 match_trait_methodclippy_utils::paths来确定 trait 的实现。

注意:如果可能,应避免使用此方法,最好的做法是向rust-lang/rust提交 PR 以添加诊断项。

下面,我们检查给定的 expr 是否实现了 Iterator 的 trait 方法 cloned

#![allow(unused)]
fn main() {
use clippy_utils::{match_trait_method, paths};
use rustc_hir::Expr;
use rustc_lint::{LateContext, LateLintPass};

impl LateLintPass<'_> for CheckTokioAsyncReadExtTrait {
    fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        if match_trait_method(cx, expr, &paths::CORE_ITER_CLONED) {
            println!("`expr` implements `CORE_ITER_CLONED` trait!");
        }
    }
}
}

以编程方式创建类型

Trait 通常是泛型类型参数,例如 Borrow<T> 是泛型类型参数 T。Rust 允许我们为特定类型实现 trait。例如,我们可以为一个假设的类型 Foo 实现 Borrow<[u8]>。假设我们想找出我们的类型是否真的实现了 Borrow<[u8]>

为此,我们可以使用与上述相同的 implements_trait 函数,并提供一个表示 [u8] 的类型参数。由于 [u8][T] 的一个特化,我们可以使用Ty::new_slice方法来创建一个表示 [T] 的类型,并将 u8 作为类型参数提供。要以编程方式创建 ty::Ty,我们依赖于 Ty::new_* 方法。这些方法创建 TyKind,然后将其包装在 Ty 结构中。这意味着我们可以访问所有原始类型,例如 Ty::new_charTy::new_boolTy::new_int 等。我们还可以使用这些基本构建块创建更复杂的类型,例如切片、元组和引用。

对于 trait 检查,仅仅创建类型是不够的,我们需要将它们转换为 GenericArg。在 rustc 中,泛型是编译器理解的实体,有三种类型:类型、常量和生命周期。通过在构造的 Ty 上调用 .into(),我们将类型包装成泛型,然后查询系统可以使用它来确定是否实现了专门的 trait。

以下代码演示了如何执行此操作

#![allow(unused)]

fn main() {
use rustc_middle::ty::Ty;
use clippy_utils::ty::implements_trait;
use rustc_span::symbol::sym;

let ty = todo!("Get the `Foo` type to check for a trait implementation");
let borrow_id = cx.tcx.get_diagnostic_item(sym::Borrow).unwrap(); // avoid unwrap in real code
let slice_of_bytes_t = Ty::new_slice(cx.tcx, cx.tcx.types.u8);
let generic_param = slice_of_bytes_t.into();
if implements_trait(cx, ty, borrow_id, &[generic_param]) {
    todo!("Rest of lint implementation")
}
}

本质上,Ty 结构允许我们以编程方式创建编译器和查询引擎可以使用的表示形式的类型。然后,我们使用我们感兴趣的类型的 rustc_middle::Ty,并查询编译器以查看它是否确实实现了我们感兴趣的 trait。