I forked Clippy to write 5 custom lints in order to enforce Evil Rust

Today, I found a repository on GitHub about “Crust”

Crust is a version of Rust in which you program like in C by following a list of simple rules.

The Rules of Crust

  1. Every function is unsafe.
  2. No references &, only pointers: *mut and *const.
  3. No cargo, build with rustc directly.
  4. No std, but libc is allowed.
  5. Everything is pub by default.

Programming in Rust using the above rules requires having to constantly make sure you’re following all of the rules, because breaking any of them is possible and there’s no automatic checker that can enforce them for you.

But what if there was? Clippy contains hundreds of rules that find specific patterns in code and lint against them. I thought a good way to enforce the “Crust” would be to create custom clippy rules.

I have a few actually useful lints to add to the real Clippy that people can use. And this sounds like a great way to learn how to implement them, so let’s do it!

The issue that extending Clippy isn’t really possible. The only way is to fork Clippy, and then use the fork. But you cannot write a lint as a library and then use that lint with Clippy.

So what? Let’s fork clippy! I need to implement the following 5 lints in order to follow the rules of Crust:

These is one problem that we have, though. We can’t use Cargo in Evil Rust. It is simply banned. What do we do? Well, there is a binary called clippy-driver that gets automatically installed when you install clippy with rustup.

clippy-driver is made specifically for those projects that don’t use cargo, which is ours! It runs clippy on our project, provided a main.rs file - but it also calls rustc and compiles our code. So we don’t have to use both rustc and clippy, a single clippy-driver can do both for us.

Forking clippy

In Evil Rust, we also have an additional rule where we’re not allowed to get any more help from the compiler than possible. This includes the built-in lints like unused_mut. I need to disable all of the default lints that come with Clippy as well as Rustc.

Doing this is quite simple, as the clippy-driver allows passing command-line arguments that are forwarded to rustc. In clippy source code, src/driver.rs is the entry point to clippy-driver. clippy_args contains the arguments that are passed to clippy:

let clippy_args = clippy_args_var
    .as_deref()
    .unwrap_or_default()
    .split("__CLIPPY_HACKERY__")
    .filter_map(|s| match s {
        "" => None,
        "--no-deps" => {
            no_deps = true;
            None
        },
        _ => Some(s.to_string()),
    })
   .chain(vec![
        "--cfg".into(),
        "clippy".into()
    ])
    .collect::<Vec<String>>();

Interestingly enough, the cfg(clippy) is passed simply via the command line. That makes it trivial to add our command-line overrides.

// .chain(vec![
//     "--cfg".into(),
//     "clippy".into()
       // enable our custom lints on Deny
       "-D".into(),
       "clippy::missing_mut".into(),
       "-D".into(),
       "clippy::safe_fn".into(),
       "-D".into(),
       "clippy::missing_pub".into(),
       "-D".into(),
       "clippy::reference_used".into(),
       "-D".into(),
       "clippy::missing_no_std".into(),
       // set default lints to Allow
       "-A".into(),
       "warnings".into(),
       "-A".into(),
       "ambiguous-associated-items".into(),
       // and of course more. There are about 100+
       // lints that we disable!
       // ...
// ])

Implementing the 5 Custom Lints

I opened the clippy contributing guide. One of the sections is Adding lints. I followed it, and the first lint I’ll implement is the safe_code one which seems like the easiest so I can get a grip of implementing Clippy lints.

Ban all safe code

Clippy has a command which writes the boilerplate for me:

cargo dev new_lint --name=foo_functions --pass=early --category=pedantic

This creates a new module which represents my lint. It registers the lint and writes all the boilerplate for me.

If you notice --pass=early, there is also --pass=late. You have to make the choice of using either the late or early pass. The difference between them is that during the late pass, you have type information about the program. You can know the exact type for a variable such as x. In the early pass, this information is not available and you’ll only operate on the Abstract Syntax Tree (AST), without any of the type information.

The boilerplate generated looks something like this:

declare_clippy_lint! {
    pub SAFE_CODE,
    evil,
    "safe functions are not allowed in evil rust"
}

declare_lint_pass!(SafeFn => [SAFE_CODE]);

impl EarlyLintPass for SafeFn {}

All associated functions of the EarlyLintPass trait are optional. They’re called e.g. check_fn. This function visits every function item. Or check_item which visits any item.

I need to write this lint for:

There’s a special function check_fn that I’ll implement to check all functions and add unsafe prefix.

// impl EarlyLintPass for SafeFn {
fn check_fn(
    &mut self,
    cx: &EarlyContext<'_>,
    fn_kind: rustc_ast::visit::FnKind<'_>,
    // Span: contains information about location of items
    // This specific Span represents the exact file and character range that
    // the given function declaration spans
    _: rustc_span::Span,
    _: NodeId,
) {
    if let rustc_ast::visit::FnKind::Fn(_, _, func) = fn_kind {
        // This function is: `fn`. Not `unsafe fn`. We'll have to fix this!
        if func.sig.header.safety == Safety::Default {
            // This `span` is 0 characters wide, it represents the position
            // where we'll insert the string: `unsafe ` (with a space)
            let span = if let Extern::Implicit(span) | Extern::Explicit(_, span) = func.sig.header.ext {
                // `ItemSafety` must come before the `extern`, if it exists
                // `unsafe extern fn` is valid
                // `extern unsafe fn` is not
                //
                // We don't want to make the above mistake

                // `shrink_to_lo` takes a span and makes it as small as possible
                //
                // Say you have a span like this:
                //
                // extern fn
                // ^^^^^^

                // shrink_to_lo will transform it to be as small as it can be:

                // extern fn
                // ^

                // We will then insert the `unsafe` keyword *before* the `extern`.
                span.shrink_to_lo()
            } else {
                // In this case, the function is not in an `extern` block
                // Nor is it an `extern fn`

                // We receive a span that looks like this, for an
                // async function called `hello_world`:

                // async fn hello_world
                // ^^^^^^^^^^^^^^^^^^^^

                // We want to have our span be like this:
                // async fn hello_world
                //       ^

                // That is where we will insert `unsafe`

                // Step 1
                // ---
                // We shift the lower end of the current span
                // to be here:

                // async fn hello_world
                //       ^^^^^^^^^^^^^^

                // Step 2
                // ---
                // Make the span as small as possible

                // async fn hello_world
                //       ^

                // We'll insert: `unsafe ` (including the space) right before the span later.
                func.ident
                    .span
                    // step 1
                    .with_lo(func.ident.span.lo() - BytePos("fn ".len() as u32))
                    // step 2
                    .shrink_to_lo()
                // If `extern` does not exist, `ItemSafety` comes before the `fn`
            };

            // Fire the lint
            span_lint_and_help(
                cx,
                SAFE_CODE,
                span,
                "function must be `unsafe`",
                Some(span),
                "make this function unsafe: `unsafe`".to_string(),
            );
        } else if let Safety::Safe(span) = func.sig.header.safety {
            // Some functions in Rust can be marked as `safe fn`
            // Namely, functions in extern blocks.

            // This is the easy path. Here, the `span` we receive for the `safe fn`
            // is the span of the `safe` keyword itself.
            span_lint_and_help(
                cx,
                SAFE_CODE,
                span,
                "function must be `unsafe`",
                Some(span),
                "make this function unsafe: `unsafe fn`".to_string(),
            );
        }
    }
}

For each rule that we add, we will use clippy’s UI test framework to help us make sure our lint does not have any bugs in it.

Here are the tests for this lint:

#![no_main]
#![allow(unused)]
#![warn(clippy::safe_code)]

trait TraitSafe {}
//~^ safe_code
// Syntax like the above: `~^` is saying "I expect the lint for the line above"
unsafe trait TraitUnsafe {}

fn foo_safe() {}
//~^ safe_code
unsafe fn foo_unsafe() {}

struct A;
impl A {
    pub fn bar_safe() {}
    //~^ safe_code
    pub unsafe fn bar_unsafe() {}
}

trait B {
    //~^ safe_code
    fn baz_safe() {}
    //~^ safe_code
    unsafe fn baz_unsafe() {}

    fn quux_safe();
    //~^ safe_code
    unsafe fn quux_unsafe();
}

#[unsafe(export_name = "main")]
pub extern "C" fn entry() {
    //~^ safe_code
    // We don't want to lint calls
    foo_safe();
    A::bar_safe();
}

Ban all references

The lint reference_used lints against usages of the ref keyword as well as the & reference operator. Use raw, unsafe pointers instead.

use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
use rustc_ast::ast::*;
use rustc_lint::{EarlyContext, EarlyLintPass};
use rustc_session::declare_lint_pass;

declare_clippy_lint! {
    /// ### What it does
    ///
    /// Checks for references
    ///
    /// ### Why restrict this?
    ///
    /// References are not allowed
    ///
    /// ### Example
    /// ```no_run
    /// let a = 4;
    /// let b = &4;
    /// ```
    /// Use instead:
    /// ```no_run
    /// let a = 4;
    /// let b = &raw const 4;
    /// ```
    #[clippy::version = "1.88.0"]
    pub REFERENCE_USED,
    restriction,
    "default lint description"
}

declare_lint_pass!(ReferenceUsed => [REFERENCE_USED]);

impl EarlyLintPass for ReferenceUsed {

    // This lint is a bit more involved than the previous ones.
    fn check_pat(&mut self, cx: &EarlyContext<'_>, pat: &Pat) {
        if let PatKind::Ident(
        // Six binding modes `ref`, `ref mut`, `mut ref` and `mut ref mut` all add a reference
        // and we want to make sure that does not happen
            BindingMode::REF | BindingMode::REF_MUT | BindingMode::MUT_REF | BindingMode::MUT_REF_MUT,
            _,
            _,
        ) = &pat.kind
            // Spans coming from macro expansions should be ignored
            && !pat.span.from_expansion()
        {
            span_lint(
                cx,
                REFERENCE_USED,
                pat.span,
                "`ref` is not allowed, as it binds by reference",
            );
        }
    }

    fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
        if let ExprKind::AddrOf(BorrowKind::Ref, mutability, _) = &expr.kind {
            span_lint_and_help(
                cx,
                REFERENCE_USED,
                expr.span,
                "references are not allowed",
                None,
                format!(
                    "use a raw borrowing instead: `{}`",
                    if mutability.is_mut() { "&raw mut" } else { "&raw const" }
                ),
            );
        }
    }

    fn check_ty(&mut self, cx: &EarlyContext<'_>, ty: &Ty) {
        if let TyKind::Ref(_, mutability) = &ty.kind {
            span_lint_and_help(
                cx,
                REFERENCE_USED,
                ty.span,
                "references are not allowed",
                None,
                format!(
                    "use a raw pointer instead: `{}`",
                    if mutability.mutbl.is_mut() { "*raw" } else { "*const" }
                ),
            );
        }
    }
}

Tests

#![warn(clippy::reference_used)]

fn main() {
    let x = 10;
    let y = &x;
    //~^ reference_used

    let z = &mut 20;
    //~^ reference_used

    let (a, ref b) = (1, 2);
    //~^ reference_used

    let c = Some(3);
    if let Some(ref d) = c {
        //~^ reference_used
        println!("{}", d);
    }

    let s = String::from("hello");
    let ref_s = &s;
    //~^ reference_used

    takes_ref(&x);
    //~^ reference_used

    takes_mut_ref(&mut 42);
    //~^ reference_used

    let raw_const: *const i32 = &x;
    //~^ reference_used
    let raw_mut: *mut i32 = &mut 100;
    //~^ reference_used
    unsafe {
        println!("{}", *raw_const);
        *raw_mut = 200;
    }

    let raw_v1 = &raw const x;
    let mut temp = 50;
    let raw_v2 = &raw mut temp;
    unsafe {
        println!("{}", *raw_v1);
        *raw_v2 = 123;
    }

    // Closure capturing by reference
    let closure = |val: &i32| println!("{}", val);
    //~^ reference_used
    closure(&x);
    //~^ reference_used

    let mut val = 5;
    let closure_mut = |v: &mut i32| *v += 1;
    //~^ reference_used
    closure_mut(&mut val);
    //~^ reference_used

    struct RefStruct<'a> {
        r: &'a i32,
        //~^ reference_used
    }

    trait RefTrait {
        fn ref_method(&self);
        //~^ reference_used
    }

    struct MyStruct;
    impl RefTrait for MyStruct {
        fn ref_method(&self) {
            //~^ reference_used
            println!("Hello from ref method");
        }
    }

    #[rustfmt::skip]
    let arr: [&i32; 2] = [
        //~^ reference_used
        &x,
        //~^ reference_used
        &42,
        //~^ reference_used
    ];

    #[rustfmt::skip]
    let tup: (
        &i32,
        //~^ reference_used
        &mut i32,
        //~^ reference_used
    ) = (
        &x,
        //~^ reference_used
        &mut val,
        //~^ reference_used
    );
}

fn takes_ref(val: &i32) {
    //~^ reference_used
    println!("{}", val);
}

fn takes_mut_ref(val: &mut i32) {
    //~^ reference_used
    *val += 1;
}

All items must be public

The missing_pub lint enforces using pub everywhere.

It’s the simplest lint of them all.

use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::HasSession;
use rustc_ast::ItemKind;
use rustc_lint::{EarlyContext, EarlyLintPass};
use rustc_session::declare_lint_pass;

declare_clippy_lint! {
    /// ### What it does
    ///
    /// ### Why restrict this?
    ///
    /// ### Example
    /// ```no_run
    /// // example code where clippy issues a warning
    /// ```
    /// Use instead:
    /// ```no_run
    /// // example code which does not raise clippy warning
    /// ```
    #[clippy::version = "1.88.0"]
    pub MISSING_PUB,
    restriction,
    "default lint description"
}

declare_lint_pass!(MissingPub => [MISSING_PUB]);

impl EarlyLintPass for MissingPub {
    fn check_item(&mut self, cx: &EarlyContext<'_>, item: &rustc_ast::Item) {
        if !item.vis.kind.is_pub()
            // If the item is visible, only then should we ask `pub` to be added
            // Some items come from de-sugarings, the user has zero control over them.
            && item.vis.span.is_visible(cx.sess().source_map())
            // We don't want to enforce `pub use` though, since we already do
            // `pub` mod and this would only add to the noise

            // Also, foreign `extern` items do not have a visibility.
            && !matches!(item.kind, ItemKind::ForeignMod(_) | ItemKind::Use(_))
        {
            span_lint_and_sugg(
                cx,
                MISSING_PUB,
                item.vis.span,
                "item must be `pub`",
                "make this item public",
                "pub ".to_string(),
                rustc_errors::Applicability::MachineApplicable,
            );
        }
    }
}

Tests

#![warn(clippy::missing_pub)]

use core::ffi::c_int;

// should not trigger for missing_pub
// the incorrect suggestion would be `pub unsafe extern "C"`, which is not valid Rust
unsafe extern "C" {
    pub unsafe fn printf(fmt: *const u8, ...) -> c_int;
}

fn main() {}
//~^ missing_pub

All items must be mutable

Everything that can be mut, must be marked as such. This is the missing_mut lint.

use clippy_utils::diagnostics::span_lint_and_sugg;
use rustc_ast::ast::*;
use rustc_ast::visit::{FnCtxt, FnKind};
use rustc_errors::Applicability;
use rustc_lint::{EarlyContext, EarlyLintPass};
use rustc_session::declare_lint_pass;

declare_clippy_lint! {
    /// ### What it does
    ///
    /// Checks for variables which are missing a `mut`.
    ///
    /// ### Why restrict this?
    ///
    /// Goes against the rules of evil rust.
    ///
    /// ### Example
    /// ```no_run
    /// let a = 4;
    /// ```
    /// Use instead:
    /// ```no_run
    /// let mut a = 4;
    /// ```
    #[clippy::version = "1.88.0"]
    pub MISSING_MUT,
    evil,
    "enforce mut keyword everywhere"
}

declare_lint_pass!(MissingMut => [MISSING_MUT]);

fn absorb_kind(cx: &EarlyContext<'_>, kind: &PatKind, message: &'static str, help: &'static str) {
    match kind {
        PatKind::Ident(mode, ident, _) => {
            // there are 6 binding modes
            // 4 of them are `mut`
            // 2 are not
            if *mode == BindingMode::NONE || *mode == BindingMode::REF {
                span_lint_and_sugg(
                    cx,
                    MISSING_MUT,
                    ident.span.shrink_to_lo(),
                    message,
                    help,
                    "mut ".to_string(),
                    Applicability::MachineApplicable,
                );
            }
        },
        PatKind::Slice(fields) | PatKind::TupleStruct(_, _, fields) | PatKind::Tuple(fields) | PatKind::Or(fields) => {
            for field in fields {
                absorb_kind(cx, &field.kind, message, help);
            }
        },
        PatKind::Struct(_, _, fields, _) => {
            for field in fields {
                absorb_kind(cx, &field.pat.kind, message, help);
            }
        },
        PatKind::Guard(field, _) | PatKind::Box(field) | PatKind::Deref(field) | PatKind::Paren(field) => {
            absorb_kind(cx, &field.kind, message, help);
        },
        PatKind::Ref(field, mutable) => {
            absorb_kind(cx, &field.kind, message, help);
            if mutable.is_not() {
                span_lint_and_sugg(
                    cx,
                    MISSING_MUT,
                    field.span.shrink_to_lo(),
                    "parameter must be `mut`",
                    "make this parameter mutable",
                    "mut ".to_string(),
                    Applicability::MachineApplicable,
                );
            }
        },
        _ => (),
    }
}

impl EarlyLintPass for MissingMut {
    fn check_fn(&mut self, cx: &EarlyContext<'_>, f: FnKind<'_>, _: rustc_span::Span, _: NodeId) {
        if let FnKind::Fn(fn_cx, _, f) = f &&
        // functions inside `extern ... {}` block cannot have `mut` args, as they don't support patterns
         fn_cx != FnCtxt::Foreign
        {
            // function params
            for param in &f.sig.decl.inputs {
                absorb_kind(
                    cx,
                    &param.pat.kind,
                    "parameter must be `mut`",
                    "make this parameter mutable",
                );
            }
        }
    }

    // local variables
    fn check_local(&mut self, cx: &EarlyContext<'_>, local: &Local) {
        absorb_kind(
            cx,
            &local.pat.kind,
            "variable must be `mut`",
            "make this variable mutable",
        );
    }

    fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
        // static
        if let ItemKind::Static(st) = &item.kind
            && st.mutability == Mutability::Not
        {
            span_lint_and_sugg(
                cx,
                MISSING_MUT,
                st.ident.span.shrink_to_lo(),
                "static must be `mut`",
                "make this static mutable",
                "mut ".to_string(),
                Applicability::MachineApplicable,
            );
        }
    }
}

Tests

#![allow(unused_mut, unused, clippy::safe_code)]
#![warn(clippy::missing_mut)]

struct Foo {
    a: (),
}

struct Bar((), (), ());

fn foo(
    a: (),
    //~^ missing_mut
    mut b: (),
    c: (),
    //~^ missing_mut
    ref d: (),
    //~^ missing_mut
    Foo { a: mut e }: Foo,
    Foo { a: f }: Foo,
    //~^ missing_mut
    (
        g,
        //~^ missing_mut
        mut h,
        i,
        //~^ missing_mut
    ): ((), (), ()),
    Bar(
        j,
        //~^ missing_mut
        mut k,
        l,
        //~^ missing_mut
    ): Bar,
) {
    let a = 4;
    //~^ missing_mut
    let mut b = 4;
}

use core::ffi::c_int;

// you can't use patterns in extern blocks
unsafe extern "C" {
    pub unsafe fn printf(fmt: *const u8, ...) -> c_int;
}

static FOO: () = ();
//~^ missing_mut
static mut BAR: () = ();

fn main() {
    // test code goes here
}

The crate must be #![no_std]

Using std is not allowed (use libc instead). This is the missing_no_std lint.

use clippy_utils::diagnostics::span_lint_and_help;
use clippy_utils::is_no_std_crate;
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::DUMMY_SP;

declare_clippy_lint! {
    /// ### What it does
    ///
    /// Lints if the `#![no_std]` attribute is missing
    ///
    /// ### Why restrict this?
    ///
    /// Evil rust requires `#![no_std]` attribute to be present
    ///
    /// ### Example
    /// ```no_run
    /// fn foo() {}
    /// ```
    /// Use instead:
    /// ```no_run
    /// #![no_std]
    ///
    /// fn foo() {}
    /// ```
    #[clippy::version = "1.88.0"]
    pub MISSING_NO_STD,
    restriction,
    "default lint description"
}

declare_lint_pass!(StdUsed => [MISSING_NO_STD]);

impl LateLintPass<'_> for StdUsed {
    fn check_crate(&mut self, cx: &LateContext<'_>) {
        if !is_no_std_crate(cx) {
            span_lint_and_help(
                cx,
                MISSING_NO_STD,
                DUMMY_SP,
                "missing `#![no_std]`",
                None,
                "make this crate `#![no_std]`",
            );
        }
    }
}

And that is it! Instructions to run it can be viewed in the Evil Rust Github