Lint passes

Before working on the logic of a new lint, there is an important decision that every Clippy developer must make: to use EarlyLintPass or LateLintPass.

In short, the LateLintPass has access to type and symbol information while the EarlyLintPass doesn't. If you don't need access to type information, use the EarlyLintPass.

Let us expand on these two traits more below.

EarlyLintPass

If you examine the documentation on EarlyLintPass closely, you'll see that every method defined for this trait utilizes a EarlyContext. In EarlyContext's documentation, it states:

Context for lint checking of the AST, after expansion, before lowering to HIR.

VoilĂ . EarlyLintPass works only on the Abstract Syntax Tree (AST) level. And AST is generated during the lexing and parsing phase of code compilation. Therefore, it doesn't know what a symbol means or information about types, and it should be our trait choice for a new lint if the lint only deals with syntax-related issues.

While linting speed has not been a concern for Clippy, the EarlyLintPass is faster, and it should be your choice if you know for sure a lint does not need type information.

As a reminder, run the following command to generate boilerplate for lints that use EarlyLintPass:

$ cargo dev new_lint --name=<your_new_lint> --pass=early --category=<your_category_choice>

Example for EarlyLintPass

Take a look at the following code:

#![allow(unused)]
fn main() {
let x = OurUndefinedType;
x.non_existing_method();
}

From the AST perspective, both lines are "grammatically" correct. The assignment uses a let and ends with a semicolon. The invocation of a method looks fine, too. As programmers, we might raise a few questions already, but the parser is okay with it. This is what we mean when we say EarlyLintPass deals with only syntax on the AST level.

Alternatively, think of the foo_functions lint we mentioned in define new lints chapter.

We want the foo_functions lint to detect functions with foo as their name. Writing a lint that only checks for the name of a function means that we only work with the AST and don't have to access the type system at all (the type system is where LateLintPass comes into the picture).

LateLintPass

In contrast to EarlyLintPass, LateLintPass contains type information.

If you examine the documentation on LateLintPass closely, you see that every method defined in this trait utilizes a LateContext.

In LateContext's documentation we will find methods that deal with type-checking, which do not exist in EarlyContext, such as:

Example for LateLintPass

Let us take a look with the following example:

#![allow(unused)]
fn main() {
let x = OurUndefinedType;
x.non_existing_method();
}

These two lines of code are syntactically correct code from the perspective of the AST. We have an assignment and invoke a method on the variable that is of a type. Grammatically, everything is in order for the parser.

However, going down a level and looking at the type information, the compiler will notice that both OurUndefinedType and non_existing_method() are undefined.

As Clippy developers, to access such type information, we must implement LateLintPass on our lint. When you browse through Clippy's lints, you will notice that almost every lint is implemented in a LateLintPass, specifically because we often need to check not only for syntactic issues but also type information.

Another limitation of the EarlyLintPass is that the nodes are only identified by their position in the AST. This means that you can't just get an id and request a certain node. For most lints that is fine, but we have some lints that require the inspection of other nodes, which is easier at the HIR level. In these cases, LateLintPass is the better choice.

As a reminder, run the following command to generate boilerplate for lints that use LateLintPass:

$ cargo dev new_lint --name=<your_new_lint> --pass=late --category=<your_category_choice>