Play with clippy

4 minute read

Published:

I consider clippy a good start to write a static analyzer for Rust. You would be able to find all details in official clippy documentation; however, this post is combined with my own experience and summarizing the important information.

Adding my own lints

Here are two ways to add lints, and it depends on whether we want to add EarlyLintPass or LateLintPass. The difference is that EarlyLintPass works on AST level while LateLintPass works on HIR level, and LateLintPass could get type information with HIR. Here I would like to try LateLintPass with Command: cargo dev new_lint --name=foo_functions --type=functions --category=pedantic
Results:

Generated lint file: `clippy_lints/src/functions/foo_functions.rs`
Be sure to add a call to `foo_functions::check` in `clippy_lints/src/functions/mod.rs`!
Generated test file: `tests/ui/foo_functions.rs`

The second line show that Be sure to add a call to: This type of lint pass will be triggered from mod.rs. At the end of declare_clippy_lint! macros, we can find our lint has been added. The official doc guided to write a EarlyLintPass; therefore, the code and path would be different here.

// clippy_lints/src/functions/mod.rs
declare_clippy_lint! {      // declare of lint is already added
    /// ...
    #[clippy::version = "1.67.0"]
    pub FOO_FUNCTIONS,
    pedantic,
    "default lint description"
}

impl_lint_pass!(Functions => [
    /// ...
    FOO_FUNCTIONS,     // our lint is already added
])

impl<'tcx> LateLintPass<'tcx> for Functions {
    fn check_fn(
        &mut self,
        cx: &LateContext<'tcx>,
        kind: intravisit::FnKind<'tcx>,
        decl: &'tcx hir::FnDecl<'_>,
        body: &'tcx hir::Body<'_>,
        span: Span,
        def_id: LocalDefId,
    ) {
        let hir_id = cx.tcx.hir().local_def_id_to_hir_id(def_id);
        /// ...
        foo_functions::check_fn(cx, kind, decl, body, span);
    }

We will add our lint to check_fn only because this is the only we need here, it is sufficient for us to get the function name.

Now, develop our new lint. To make it simple, here my purpose is also get warn of the functions which is named foo just like described in official doc. However, I use LateLintPass to implement it, so the code would be different.

// clippy_lints/src/functions/foo_functions.rs
use clippy_utils::diagnostics::span_lint_and_help;
use rustc_hir::{intravisit::FnKind, Body, FnDecl};
use rustc_lint::{LateContext, LintContext};
use rustc_span::Span;

use super::FOO_FUNCTIONS;

// TODO: Adjust the parameters as necessary
pub(super) fn check_fn(cx: &LateContext<'_>, kind: FnKind<'_>, decl: &FnDecl<'_>, body: &Body<'_>, span: Span) {
    if is_foo_fn(kind) {
        span_lint_and_help(
            cx,
            FOO_FUNCTIONS,
            span,
            "function name `foo`",
            None,
            "You should use more meaningful name",
        );
    }
}

fn is_foo_fn(fn_kind: FnKind<'_>) -> bool {
    match fn_kind {
        FnKind::ItemFn(ident, _, header) => {
            // check if `fn` name is `foo`
            ident.name.as_str() == "foo"
        },
        FnKind::Method(ident, _) => ident.name.as_str() == "foo",
        FnKind::Closure => false,
    }
}

This file is our lint logic. We can use check_fn as we registered in mod.rs file, and the HIR information is also sent to this function as parameters. I implemented another function(is_foo_fn) to check whether function or method name is foo, and call is_foo_fn from check_fn. We can choose how to handle this information with more functions from clippy_utils/src/diagnostics.rs. Here, I choose span_lint_and help.

We can run new lints on our test file. The third line above showed us the path of generated file, we can update the content as we want. The corresponding error file will be stored in tests/ui/foo_functions.stderr, clippy will compare the results with this file each time. Here is the lifetime cycle of lint’s development:

  1. develop our new lint
  2. TESTNAME=foo_functions cargo uitest to show the current results. If satisfied, go to the next step. (We can run multiple test by TESTNAME=a,b,c at the same time)
  3. cargo dev bless will automatically help us store the current results in corresponding stderr file. (need to be executed each time we update our lint)
  4. At the end step, run cargo test.

We can also run our local clippy by compiling the source and override the toolchain. The details are recorded at this link. To run our clippy on the crate and change my lint level to warning,

cd /path/to/crate
cargo +nightly-2023-xx-xx clippy -- -Wclippy::foo-functions

The version of clippy could be found in rust-clippy/rust-toolchain file.

Reference

  1. Clippy documentation