I forked Clippy to write 5 custom lints in order to enforce Evil Rust
19 May, 2025
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
- Every function is
unsafe
.- No references
&
, only pointers:*mut
and*const
.- No
cargo
, build withrustc
directly.- No
std
, butlibc
is allowed.- 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:
reference_used
: Any reference&
is banned, use raw unsafe pointers instead.missing_mut
: You must to add themut
keyword before everything.missing_pub
: Everything that can be markedpub
, must be.safe_code
: All functions and traits must beunsafe
.std_used
: Usingstd
is not allowed, you must to have a#![no_std]
present in your crate.
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
.filter_map
.chain
.;
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:
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!
declare_lint_pass!;
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:
trait
=>unsafe trait
fn
=>unsafe fn
There’s a special function check_fn
that I’ll implement to check all functions and add unsafe
prefix.
// impl EarlyLintPass for SafeFn {
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:
//~^ safe_code
// Syntax like the above: `~^` is saying "I expect the lint for the line above"
unsafe
//~^ safe_code
unsafe
;
pub extern "C"
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 ;
use *;
use ;
use declare_lint_pass;
declare_clippy_lint!
declare_lint_pass!;
Tests
All items must be public
The missing_pub
lint enforces using pub
everywhere.
It’s the simplest lint of them all.
use span_lint_and_sugg;
use HasSession;
use ItemKind;
use ;
use declare_lint_pass;
declare_clippy_lint!
declare_lint_pass!;
Tests
use 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"
//~^ missing_pub
All items must be mutable
Everything that can be mut
, must be marked as such. This is the missing_mut
lint.
use span_lint_and_sugg;
use *;
use ;
use Applicability;
use ;
use declare_lint_pass;
declare_clippy_lint!
declare_lint_pass!;
Tests
, , );
use c_int;
// you can't use patterns in extern blocks
unsafe extern "C"
static FOO: = ;
//~^ missing_mut
static mut BAR: = ;
The crate must be #![no_std]
Using std
is not allowed (use libc
instead). This is the missing_no_std
lint.
use span_lint_and_help;
use is_no_std_crate;
use ;
use declare_lint_pass;
use DUMMY_SP;
declare_clippy_lint!
declare_lint_pass!;
And that is it! Instructions to run it can be viewed in the Evil Rust Github