This page documents some of the machinery around lint registration and how we run lints in the compiler.
The LintStore
is the central piece of infrastructure, around which
everything rotates. It's not available during the early parts of compilation
(i.e., before TyCtxt) in most code, as we need to fill it in with all of the
lints, which can only happen after plugin registration.
There are two parts to the linting mechanism within the compiler: lints and lint passes. Unfortunately, a lot of the documentation we have refers to both of these as just "lints."
First, we have the lint declarations themselves: this is where the name and
default lint level and other metadata come from. These are normally defined by
way of the declare_lint!
macro, which boils down to a static with type
&rustc_session::lint::Lint
.
As of February 2022, we lint against direct declarations without the use of the macro today (although this may change in the future, as the macro is somewhat unwieldy to add new fields to, like all macros).
Lint declarations don't carry any "state" - they are merely global identifiers and descriptions of lints. We assert at runtime that they are not registered twice (by lint name).
Lint passes are the meat of any lint. Notably, there is not a one-to-one relationship between lints and lint passes; a lint might not have any lint pass that emits it, it could have many, or just one -- the compiler doesn't track whether a pass is in any way associated with a particular lint, and frequently lints are emitted as part of other work (e.g., type checking, etc.).
In rustc_interface::register_plugins
the LintStore
is created and all
lints are registered.
There are four 'sources' of lints:
- internal lints: lints only used by the rustc codebase
- builtin lints: lints built into the compiler and not provided by some outside source
- plugin lints: lints created by plugins through the plugin system.
rustc_interface::Config
register_lints
: lints passed into the compiler during construction
Lints are registered via the LintStore::register_lint
function. This should
happen just once for any lint, or an ICE will occur.
Once the registration is complete, we "freeze" the lint store by placing it in
an Lrc
. Later in the driver, it's passed into the GlobalCtxt
constructor
where it lives in an immutable form from then on.
Lint passes are registered separately into one of the categories
(pre-expansion, early, late, late module). Passes are registered as a closure
-- i.e., impl Fn() -> Box<dyn X>
, where dyn X
is either an early or late
lint pass trait object. When we run the lint passes, we run the closure and
then invoke the lint pass methods. The lint pass methods take &mut self
so
they can keep track of state internally.
These are lints used just by the compiler or plugins like clippy
. They can be
found in rustc_lint::internal
.
An example of such a lint is the check that lint passes are implemented using
the declare_lint_pass!
macro and not by hand. This is accomplished with the
LINT_PASS_IMPL_WITHOUT_MACRO
lint.
Registration of these lints happens in the rustc_lint::register_internals
function which is called when constructing a new lint store inside
rustc_lint::new_lint_store
.
These are primarily described in two places: rustc_session::lint::builtin
and
rustc_lint::builtin
. Often the first provides the definitions for the lints
themselves, and the latter provides the lint pass definitions (and
implementations), but this is not always true.
The builtin lint registration happens in the rustc_lint::register_builtins
function. Just like with internal lints, this happens inside of
rustc_lint::new_lint_store
.
This is one of the primary use cases remaining for plugins/drivers. Plugins are
given access to the mutable LintStore
during registration (which happens
inside of rustc_interface::register_plugins
) and they can call any
functions they need on the LintStore
, just like rustc code.
Plugins are intended to declare lints with the plugin
field set to true
(e.g., by way of the declare_tool_lint!
macro), but this is purely for
diagnostics and help text; otherwise plugin lints are mostly just as first
class as rustc builtin lints.
These are the lints provided by drivers via the rustc_interface::Config
register_lints
field, which is a callback. Drivers should, if finding it
already set, call the function currently set within the callback they add. The
best way for drivers to get access to this is by overriding the
Callbacks::config
function which gives them direct access to the Config
structure.
Within the compiler, for performance reasons, we usually do not register dozens
of lint passes. Instead, we have a single lint pass of each variety (e.g.,
BuiltinCombinedModuleLateLintPass
) which will internally call all of the
individual lint passes; this is because then we get the benefits of static over
dynamic dispatch for each of the (often empty) trait methods.
Ideally, we'd not have to do this, since it adds to the complexity of understanding the code. However, with the current type-erased lint store approach, it is beneficial to do so for performance reasons.