Dylints Total number: 297

What crate-specific groups check

What it does

Checks the ABC size of each function, method, and closure, and warns when sqrt(assignments² + calls² + conditions²) exceeds 25.

Assignments are let statements with an initializer, =, and compound assignments such as +=. Calls are function, method, and closure calls. Conditions are if, while, for, each match arm after the first, each match guard, &&, and ||. The diagnostic shows all three counts. The limit of 25 follows the agent-feedback threshold in the big-code-analysis threshold guide.

Why is this bad?

A large ABC size marks a long function that does many separate steps, even when it has few branches. Path metrics such as cyclomatic_complexity miss a function made of 26 straight-line calls. A reader still has to follow every step and every change of state.

Known problems

Tuple struct and enum variant constructors such as Some(value) count as calls. A plain loop and ? add no conditions. Comparisons outside control flow do not count, unlike some other ABC analyzers. The lint skips code a macro generates but counts expressions written as macro arguments. It does not check a function a macro generates.

Example

fn step() {}

fn run_pipeline() {
    step(); step(); step(); step(); step(); step(); step(); step(); step();
    step(); step(); step(); step(); step(); step(); step(); step(); step();
    step(); step(); step(); step(); step(); step(); step(); step();
}

The 26 calls give an ABC size of <0, 26, 0> with magnitude 26.

Use instead

Repeat work with a loop, or split the steps into focused functions.

fn step() {}

fn run_pipeline() {
    for _ in 0..26 {
        step();
    }
}

Interpretation and sources

ABC counts assignments, branches as calls, and conditions. Its magnitude combines three dimensionless counts as sqrt(A² + B² + C²). Inspect the components to distinguish mechanical work from branching. Compare values within the same analyzer profile. The Maintainability Index combines correlated size and complexity measurements; it is a separate report rather than this lint's score. Metric definitions.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks inherent methods named as_* or get_* that take only a &self or &mut self receiver and return a reference. A shared reference suggests AsRef, and a mutable reference suggests AsMut. The lint skips names that start with get_or_ and types that already implement the matching trait for the returned type.

Why is this bad?

Generic code that asks for impl AsRef<T> cannot use a custom accessor. Callers must learn the local method name instead of using the standard trait.

Known problems

The lint checks only the name and signature. It warns on any as_* or get_* accessor, even when the returned value is one field among several rather than the type's main borrowed view. A type can implement AsRef<T> only once for each T, so the lint warns on both accessors when they return the same type.

Example

use std::path::{Path, PathBuf};

struct ConfigPath {
    path: PathBuf,
}

impl ConfigPath {
    fn as_path(&self) -> &Path {
        self.path.as_path()
    }
}

Use instead

use std::path::{Path, PathBuf};

struct ConfigPath {
    path: PathBuf,
}

impl AsRef<Path> for ConfigPath {
    fn as_ref(&self) -> &Path {
        self.path.as_path()
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks inherent methods named borrow_* that take only a &self receiver and return a shared reference. The lint skips types that already implement Borrow for the returned type.

Why is this bad?

Generic code that asks for T: Borrow<U>, such as HashMap::get, cannot call a custom borrow method. Callers must learn the local method name instead of using the standard trait.

Known problems

Borrow is correct only when Eq, Ord, and Hash give the same results for the owned and the borrowed value. The lint cannot check this, so it can warn on a method that must not become Borrow.

Example

struct UserName(String);

impl UserName {
    fn borrow_str(&self) -> &str {
        &self.0
    }
}

Use instead

use std::borrow::Borrow;

struct UserName(String);

impl Borrow<str> for UserName {
    fn borrow(&self) -> &str {
        &self.0
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks inherent associated functions named new, empty, blank, or default_config that take no arguments and return the type of the impl block, when that type does not implement Default.

Why is this bad?

Generic code that asks for T: Default cannot call a custom zero-argument constructor, and it does not work with #[derive(Default)] on containing types, ..Default::default(), or unwrap_or_default().

Known problems

The lint does not read the function body. It warns on constructors that allocate resources, have side effects, or set up an invariant that a Default value must not have. For a public new, Clippy's new_without_default lint reports the same missing implementation.

Example

struct Config {
    retries: u8,
}

impl Config {
    fn new() -> Self {
        Self { retries: 3 }
    }
}

Use instead

struct Config {
    retries: u8,
}

impl Default for Config {
    fn default() -> Self {
        Self { retries: 3 }
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks inherent methods named to_string, display, format, or render that take only a &self receiver and return String. The lint skips types that already implement Display.

Why is this bad?

A custom formatting method does not work with format!, println!, {} placeholders, logging macros, or generic T: Display bounds. It also allocates a String even when the caller only writes the text to a stream.

Known problems

The lint checks only the name and signature. It warns on a render or format method that produces a document or report rather than the type's one textual form.

Example

struct UserId(String);

impl UserId {
    fn to_string(&self) -> String {
        self.0.clone()
    }
}

Use instead

use std::fmt;

struct UserId(String);

impl fmt::Display for UserId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks free functions and inherent associated functions named make_*, build_*, convert_*, extract_*, extracted_*, or from_* that take one argument and return a struct, enum, or union other than Option or Result. The argument or return type must come from the current crate, which lets this crate add a From implementation. The lint skips names that contain _and_.

The lint skips methods with a self receiver and methods in trait implementations. It also skips arguments whose type is a bare type parameter and conversions whose From implementation already exists. Finally, it skips bodies that can panic through panic!, assert!, unreachable!, todo!, unimplemented!, or unwrap and expect on Option or Result. From promises an infallible conversion, so such a body needs TryFrom instead.

Why is this bad?

A custom conversion function does not support .into(), ? error conversion, or generic T: Into<U> bounds. Callers must learn the local function name instead of using the standard trait.

Known problems

The lint warns on conversions that have side effects or one of several valid policies. It does not see panics inside called functions, indexing, or arithmetic overflow, so a function that fails only through those still warns.

Example

struct RawUserId(u64);
struct UserId(u64);

fn make_user_id(raw: RawUserId) -> UserId {
    UserId(raw.0)
}

Use instead

struct RawUserId(u64);
struct UserId(u64);

impl From<RawUserId> for UserId {
    fn from(raw: RawUserId) -> Self {
        Self(raw.0)
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks free functions and inherent associated functions named parse_* or from_str*. They must take one &str argument, have no self receiver, and return Result<T, E>, where T is a struct, enum, or union defined in the current crate without lifetime arguments. The lint skips types that already implement FromStr and names that contain _and_, _lenient, _lossy, _or_, _strict, _unchecked, or _with_.

Why is this bad?

Callers cannot use a custom string parser with str::parse, and generic code that asks for T: FromStr cannot accept the type. Callers must learn the local function name instead of writing "42".parse::<UserId>().

Known problems

The lint does not read the function body, so it warns on parsers that are one of several valid formats for the type.

Example

struct UserId(u64);
struct InvalidUserId;

fn parse_user_id(raw: &str) -> Result<UserId, InvalidUserId> {
    raw.parse().map(UserId).map_err(|_| InvalidUserId)
}

Use instead

use std::str::FromStr;

struct UserId(u64);
struct InvalidUserId;

impl FromStr for UserId {
    type Err = InvalidUserId;

    fn from_str(raw: &str) -> Result<Self, Self::Err> {
        raw.parse().map(UserId).map_err(|_| InvalidUserId)
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks inherent methods named into_*, iter_*, or items that take only a self, &self, or &mut self receiver and return a type that implements Iterator, such as std::vec::IntoIter<T>, std::slice::Iter<'_, T>, or impl Iterator. The lint skips the method when the receiver type already implements IntoIterator. The lint skips names that contain a policy word such as filter, sorted, unique, page, owned, or active. The lint treats methods that return a collection such as Vec<T> as conversions rather than iteration views, so it skips them.

Why is this bad?

A custom iteration method does not work with for loops over the value, with Iterator::zip or Extend::extend, or with generic T: IntoIterator bounds. Callers must learn the local method name instead.

Known problems

The lint checks only the name and signature, so it warns on an iteration view that is one of several valid views of the type. The policy words match anywhere in the name, so the lint skips into_pages and iter_validated. For a method that borrows, such as iter_rows(&self), the help asks for IntoIterator for &Rows instead of IntoIterator for Rows.

Example

struct Row;

struct Rows {
    rows: Vec<Row>,
}

impl Rows {
    fn into_rows(self) -> std::vec::IntoIter<Row> {
        self.rows.into_iter()
    }
}

Use instead

struct Row;

struct Rows {
    rows: Vec<Row>,
}

impl IntoIterator for Rows {
    type Item = Row;
    type IntoIter = std::vec::IntoIter<Row>;

    fn into_iter(self) -> Self::IntoIter {
        self.rows.into_iter()
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks inherent methods named next or next_* that take only a &mut self receiver and return Option<T>, where T is not the impl type itself. The lint skips types that already implement Iterator. It also skips names that contain a word such as state, status, transition, page, retry, event, or advance.

Why is this bad?

A custom next method does not work with for loops or iterator adapters such as map, filter, and collect. Callers must write the loop by hand.

Known problems

The lint checks only the name and signature. It warns on a next_* method that is one of several ways to step through the type rather than its one item sequence. The lint skips names containing those words, so it skips next_statement.

Example

struct Row;

struct Rows {
    rows: Vec<Row>,
}

impl Rows {
    fn next_row(&mut self) -> Option<Row> {
        self.rows.pop()
    }
}

Use instead

struct Row;

struct Rows {
    rows: Vec<Row>,
}

impl Iterator for Rows {
    type Item = Row;

    fn next(&mut self) -> Option<Self::Item> {
        self.rows.pop()
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks free functions and inherent associated functions named make_*, build_*, convert_*, map_*, try_*, or validate_* that take one argument, have no self receiver, and return Result<T, E>. The argument or T must come from the current crate, so this crate can add a TryFrom implementation. The lint skips a T that is (), !, or the argument type itself. It also skips an argument that is a bare type parameter and conversions for which a TryFrom implementation already applies, including through From.

Why is this bad?

A custom fallible conversion does not support .try_into() or generic T: TryInto<U> bounds. Callers must learn the local function name instead of using the standard trait.

Known problems

The lint does not read the function body. The try_* and map_* prefixes also match operations that are not conversions, such as fn try_connect(address: Address) -> Result<Connection, Error>.

Example

struct RawUserId(u64);
struct UserId(u64);
struct InvalidUserId;

fn make_user_id(raw: RawUserId) -> Result<UserId, InvalidUserId> {
    if raw.0 == 0 {
        return Err(InvalidUserId);
    }
    Ok(UserId(raw.0))
}

Use instead

struct RawUserId(u64);
struct UserId(u64);
struct InvalidUserId;

impl TryFrom<RawUserId> for UserId {
    type Error = InvalidUserId;

    fn try_from(raw: RawUserId) -> Result<Self, Self::Error> {
        if raw.0 == 0 {
            return Err(InvalidUserId);
        }
        Ok(Self(raw.0))
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks named fields whose type is a primitive integer and whose name ends in _size, _length, or _offset. It reports a field when neither the field name nor the field's doc comment names a unit.

Why is this bad?

An integer named payload_length can count bytes, characters, or elements. Code that mixes those units still compiles and produces wrong results, for example when slicing a UTF-8 string by a character count.

Known problems

The lint recognizes a fixed unit vocabulary. It includes bits, bytes, KiB through TiB and KB through TB, characters, code points, elements, entries, items, records, rows, columns, pixels, samples, frames, pages, and words. It warns when the doc comment uses another unit, such as "kilobytes" or "lines".

It reads only the field's own doc comment, so a unit stated on the containing type does not count. It skips fields generated by macros, integer newtypes, and floating-point fields.

Example

struct DownloadLimits {
    maximum_size: u64,
}

Use instead

Name the unit in the field name or in the field's doc comment:

struct DownloadLimits {
    maximum_size_bytes: u64,
    /// Length in characters.
    title_length: usize,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::nest calls whose path resolves to "" or "/", including immutable local and const initializer chains.

Why is this bad?

Axum 0.8 does not support nesting a router at the root. Router::nest panics with either path when application code builds the router, so the error appears only when the application starts or a test builds that router. Router::merge combines two routers at the same level.

Known problems

The lint follows at most eight immutable local or const initializer references to a string literal. Mutable bindings, destructured bindings, static values, function results, and runtime-built paths remain unknown. The lint reports local or constant values at the call site and offers no machine-applicable fix; fixes apply only to direct literals.

Example

use axum::Router;

fn app(api: Router) -> Router {
    Router::new().nest("/", api)
}

Use instead

use axum::Router;

fn app(api: Router) -> Router {
    Router::new().merge(api)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::nest_service calls whose path resolves to "" or "/", including immutable local and const initializer chains.

Why is this bad?

Axum 0.8 does not support nesting a service at the root. Router::nest_service panics with either path when application code builds the router, so the error appears only when the application starts or a test builds that router. Router::fallback_service sends every request that matches no route to the service.

Known problems

The lint follows at most eight immutable local or const initializer references to a string literal. Mutable bindings, destructured bindings, static values, function results, and runtime-built paths remain unknown. The lint reports local or constant values at the call site and offers no machine-applicable fix; fixes apply only to direct literals.

Example

use axum::Router;

fn app(api: Router) -> Router {
    Router::new().nest_service("/", api)
}

Use instead

use axum::Router;

fn app(api: Router) -> Router {
    Router::new().fallback_service(api)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::nest and axum::Router::nest_service calls whose path has a wildcard capture segment such as {*rest}, including immutable local and const initializer chains.

Why is this bad?

Axum does not allow wildcard captures in the path of a nested router. Router::nest panics when application code builds the router, so the error appears only when the application starts or a test builds that router. The wildcard route belongs inside the nested router.

Known problems

The lint follows at most eight immutable local or const initializer references to a string literal. Mutable bindings, destructured bindings, static values, function results, and runtime-built paths remain unknown. The lint reports local or constant values at the call site and offers no machine-applicable fix. An escaped segment such as {{*rest}} is a literal path, so the lint does not report it.

Example

use axum::Router;

fn app(api: Router) -> Router {
    Router::new().nest("/api/{*rest}", api)
}

Use instead

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    let api = Router::new().route("/{*rest}", get(handler));
    Router::new().nest("/api", api)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::route calls whose path resolves to the empty string, including immutable local and const initializer chains.

Why is this bad?

Axum route paths must start with /. Router::route panics on an empty path when application code builds the router. The error appears only when the application starts or a test builds that router. The root route is "/".

Known problems

The lint follows at most eight immutable local or const initializer references to a string literal. Mutable bindings, destructured bindings, static values, function results, and runtime-built paths remain unknown. The lint reports local or constant values at the call site and offers no machine-applicable fix; fixes apply only to direct literals.

Example

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    Router::new().route("", get(handler))
}

Use instead

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    Router::new().route("/", get(handler))
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::route_layer called on a router that has no routes yet. The lint follows the receiver through immutable let bindings and route-free builder calls, such as layer, fallback, and with_state, to Router::new() or a Default::default() call whose result is Axum's Router.

Why is this bad?

Router::route_layer wraps only the routes that already exist. On a router with no routes it would do nothing, so Axum panics when code builds the router. The error shows up only when the application starts or a test builds that router.

Known problems

The lint does not follow an empty router returned by another function, passed as a parameter, merged with Router::merge, or held in a mutable binding. It reports only when it can prove the receiver traces to an empty router origin.

Example

use axum::{
    extract::Request,
    middleware::{self, Next},
    response::Response,
    Router,
};

async fn auth(request: Request, next: Next) -> Response {
    next.run(request).await
}

fn app() -> Router {
    Router::new().route_layer(middleware::from_fn(auth))
}

Use instead

Add the routes before the route layer. Use Router::layer instead if the layer must also wrap the fallback.

use axum::{
    extract::Request,
    middleware::{self, Next},
    response::Response,
    routing::get,
    Router,
};

async fn auth(request: Request, next: Next) -> Response {
    next.run(request).await
}

async fn handler() {}

fn app() -> Router {
    Router::new()
        .route("/", get(handler))
        .route_layer(middleware::from_fn(auth))
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::route, route_service, nest, and nest_service calls whose path has a segment that starts with :, the capture syntax from Axum 0.7 and earlier, such as "/users/:id", including immutable local and const initializer chains.

Why is this bad?

Axum 0.8 writes captures as {name}. By default, these methods panic on a segment that starts with : when application code builds the router. Code that still uses the old syntax after an upgrade fails when the application starts.

Known problems

The lint follows at most eight immutable local or const initializer references to a string literal. Mutable bindings, destructured bindings, static values, function results, and runtime-built paths remain unknown. It reports local or constant values at the call site and offers no machine-applicable fix; fixes apply only to direct literals. The lint suppresses this warning only when it can trace immutable router builders or local bindings to Router::without_v07_checks. It can warn when that call is out of sight, such as on a mutable binding or a router passed in as a parameter. A machine-applicable fix requires a plain string literal without escapes and Rust-style capture identifiers.

Example

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    Router::new().route("/users/:id", get(handler))
}

Use instead

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    Router::new().route("/users/{id}", get(handler))
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::route and route_service calls whose path has a segment that starts with *, the wildcard syntax from Axum 0.7 and earlier, such as "/assets/*path", including immutable local and const initializer chains.

Why is this bad?

Axum 0.8 writes wildcards as {*name}. By default, these methods panic on a segment that starts with * when application code builds the router. Code that still uses the old syntax after an upgrade fails when the application starts.

Known problems

The lint follows at most eight immutable local or const initializer references to a string literal. Mutable bindings, destructured bindings, static values, function results, and runtime-built paths remain unknown. It reports local or constant values at the call site and offers no machine-applicable fix; fixes apply only to direct literals. The lint suppresses this warning only when it can trace immutable router builders or local bindings to Router::without_v07_checks. It can warn when that call is out of sight, such as on a mutable binding or a router passed in as a parameter. It does not check nest paths, where any wildcard panics.

A machine-applicable fix requires a plain string literal without escapes and Rust-style wildcard identifiers.

Example

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    Router::new().route("/assets/*path", get(handler))
}

Use instead

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    Router::new().route("/assets/{*path}", get(handler))
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::route, route_service, nest, and nest_service calls whose path is a nonempty string that does not start with /, such as "health", including immutable local and const initializer chains.

Why is this bad?

Axum paths must start with /. These methods panic on any other path when application code builds the router. The error appears only when the application starts or a test builds that router.

Known problems

The lint follows at most eight immutable local or const initializer references to a string literal. Mutable bindings, destructured bindings, static values, function results, and runtime-built paths remain unknown. It reports local or constant values at the call site and leaves their initializers unchanged. Machine-applicable fixes apply only to direct string literals without escapes. The other lints report the empty path "": axum-route-empty-path, axum-route-service-empty-path, axum-nest-at-root, or axum-nest-service-at-root instead.

Example

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    Router::new().route("health", get(handler))
}

Use instead

use axum::{routing::get, Router};

async fn handler() {}

fn app() -> Router {
    Router::new().route("/health", get(handler))
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::route_service calls whose path resolves to the empty string, including immutable local and const initializer chains.

Why is this bad?

Axum route paths must start with /. Router::route_service panics on an empty path when application code builds the router. The error appears only when the application starts or a test builds that router. The root route is "/".

Known problems

The lint follows at most eight immutable local or const initializer references to a string literal. Mutable bindings, destructured bindings, static values, function results, and runtime-built paths remain unknown. The lint reports local or constant values at the call site and offers no machine-applicable fix; fixes apply only to direct literals.

Example

use axum::Router;

fn app(service: Router) -> Router {
    Router::new().route_service("", service)
}

Use instead

use axum::Router;

fn app(service: Router) -> Router {
    Router::new().route_service("/", service)
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for axum::Router::route_service calls whose service argument is an axum::Router.

Why is this bad?

Router::route_service panics when the service is another Router, so the error shows up only when the application starts or a test builds that router. Router::nest mounts a router below a path prefix, and Router::merge combines two routers at the same level.

Known problems

The lint checks the type of the service argument. It does not detect a router wrapped in another service type, such as a router passed through a Tower ServiceBuilder.

Example

use axum::Router;

fn app(api: Router) -> Router {
    Router::new().route_service("/api", api)
}

Use instead

use axum::Router;

fn app(api: Router) -> Router {
    Router::new().nest("/api", api)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks direct AssetApp::register_asset_source calls after an explicit AssetPlugin addition on one tracked App value. It also checks explicit WebAssetPlugin order in direct plugin tuples and chained App::add_plugins calls.

Why is this bad?

Bevy builds asset sources when AssetPlugin starts. A later source registration logs an error and cannot update the active AssetServer. WebAssetPlugin must be added first so it can register its HTTP sources.

Known problems

The lint follows resolved direct plugin values, tuple elements in source order, and direct local App aliases within one function body. It preserves app identity through direct add_plugins and register_asset_source chains. It skips DefaultPlugins and other plugin groups because the compiled group's member list is not available through the resolved call, and group operations can disable or reorder members. It also skips custom plugin bodies, helper calls, unrelated app methods in a chain, and aliases it cannot prove are the same value.

At branch joins, it keeps only facts shared by every path. A branch-only plugin addition does not affect later checks. If an App binding may refer to different values across paths or receives an unproven reassignment, the lint stops tracking that binding. A later assignment from a proven local app can restore tracking.

The lint invalidates plugin facts when a tracked app escapes through a mutable-reference argument to an opaque call. Implicit mutable method receivers are not analyzed. It cannot inspect the helper's behavior, so later ordering mistakes can remain unreported.

The lint clears tracked facts when it invokes a closure value or passes one to an opaque function or method. It also clears them for an opaque method invoked on a closure receiver. This conservative handling does not inspect captures, so an unrelated closure can suppress later warnings. Creating and dropping a closure without invoking or passing it preserves tracking.

Example

app.add_plugins(AssetPlugin::default());
app.register_asset_source("remote", source_builder());

Use instead

app.register_asset_source("remote", source_builder());
app.add_plugins(AssetPlugin::default());
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for function parameters that take a reborrowable Bevy type through &mut, such as &mut Query<..>, &mut Commands, or &mut ResMut<..>.

The checked types are Commands, Deferred, DeferredWorld, EntityCommands, EntityMut, FilteredEntityMut, Mut, MutUntyped, NonSendMut, PtrMut, Query, and ResMut.

Why is this bad?

These types already hold a mutable borrow and provide reborrow. Wrapping them in another &mut adds a second layer of indirection, and callers must keep a mutable binding alive just to pass it.

Known problems

The lint skips self parameters and parameters whose lifetime appears in the return type. It also skips trait method implementations because the trait fixes their parameter types.

Example

fn count_markers(query: &mut Query<&Marker>) -> usize {
    query.iter().count()
}

Use instead

fn count_markers(query: Query<&Marker>) -> usize {
    query.iter().count()
}

fn system(mut query: Query<&Marker>) {
    let _count = count_markers(query.reborrow());
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for systems added to FixedUpdate through App::add_systems when the system has a query with mutable data and a With<Camera> filter.

Why is this bad?

FixedUpdate runs zero, one, or several times per rendered frame. A camera moved there changes in fixed steps that do not line up with frames, so the view stutters.

Known problems

The lint resolves tuple members and schedule configuration methods such as .after(), .run_if(), and .chain(). It skips closure systems and recognizes only the With<Camera> filter, so it does not report queries filtered on Camera2d or Camera3d.

Example

fn move_camera(mut cameras: Query<&mut Transform, With<Camera>>) {
    for mut transform in &mut cameras {
        transform.translation.x += 1.0;
    }
}

fn build(app: &mut App) {
    app.add_systems(FixedUpdate, move_camera);
}

Use instead

fn build(app: &mut App) {
    app.add_systems(Update, move_camera);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for function parameters of type Query<..> whose query data contains &mut Children.

Why is this bad?

Children is the relationship target of ChildOf. Bevy updates it when ChildOf changes on the child entities. Editing Children directly can leave it out of sync with the ChildOf components.

Known problems

Resolved inherent Children reordering methods such as swap and sort_by are exempt, including calls inside closures, when no other mutable Children use appears. Direct collection mutation, a mutable handle passed elsewhere, and a local &mut Children reborrow or alias still trigger a warning, even when that local value only calls sort_by.

The query analysis does not look inside Option<&mut Children> or custom QueryData types.

Example


fn detach_all(mut parents: Query<&mut Children>) {
    for mut children in &mut parents {
        children.collection_mut_risky().clear();
    }
}

Use instead


fn detach_all(mut commands: Commands, parents: Query<Entity, With<Children>>) {
    for parent in &parents {
        commands.entity(parent).detach_all_children();
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a local component that two systems in the same schedule mutate through Query<&mut T> when the systems write disjoint sets of its named fields.

Why is this bad?

Bevy tracks access per component, not per field. Two systems that both take &mut Motion cannot run in parallel, even when one writes only x and the other writes only y.

Known problems

The lint compares named free functions registered through App::add_systems or SubApp::add_systems. It resolves tuple members and schedule configuration methods such as .chain(), .run_if(), and .after().

It skips a function when its parameters contain more than one mutable local-component query access or when the function uses a component as a whole value. Field accesses inside closures count.

Example

#[derive(Component)]
struct Motion {
    x: f32,
    y: f32,
}

fn move_x(mut query: Query<&mut Motion>) {
    for mut motion in &mut query {
        motion.x += 1.0;
    }
}

fn move_y(mut query: Query<&mut Motion>) {
    for mut motion in &mut query {
        motion.y += 1.0;
    }
}

fn build(app: &mut App) {
    app.add_systems(Update, (move_x, move_y));
}

Use instead

#[derive(Component)]
struct MotionX(f32);

#[derive(Component)]
struct MotionY(f32);

fn move_x(mut query: Query<&mut MotionX>) {
    for mut motion in &mut query {
        motion.0 += 1.0;
    }
}

fn move_y(mut query: Query<&mut MotionY>) {
    for mut motion in &mut query {
        motion.0 += 1.0;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Warns when a free function registered directly with App::add_systems declares overlapping Bevy query access. It checks component access within one query, between query parameters, and between a query and Res, ResMut, NonSend, or NonSendMut parameters.

The lint understands borrowed component references, tuples of supported query data, Entity, With, Without, tuple filter conjunctions, Or, and ParamSet member boundaries. It models Bevy 0.18 and 0.19's built-in Disabled default query filter and Bevy 0.19's resource-entity IsResource marker.

Why is this bad?

Bevy may reject overlapping query parameters with B0001 or query-to-resource access with B0002 while it initializes a system. Query data that requests both shared and mutable access to the same component also panics during initialization. These failures can stop an app before the system runs.

Known problems

This is a static approximation. It skips closures, indirect registrations, custom SystemParam implementations, unknown query data, and unknown filters. It expands at most 64 filter alternatives; larger forms are skipped. It recognizes disjointness from With, Without, tuples, and Or only.

Only accesses in different members of the same ParamSet are sequential alternatives. Accesses inside one member remain simultaneous, including accesses nested in ordinary tuples. It does not treat contradictory constraints inside one query as proof that the query matches no entities, matching Bevy's filtered-access compatibility behavior.

For Bevy 0.18 and 0.19, the lint assumes the built-in Disabled default filter and does not inspect runtime DefaultQueryFilters changes. Registering custom disabling components can make a reported query pair disjoint. Replacing the defaults can also make an unreported pair conflict. Query-to-Res checks use the resolved ECS crate's IsResource marker. A Without filter for either IsResource or the resource type can also prove disjointness when the same branch does not require that component.

Bevy 0.19 checks NonSend access differently. When NonSend<T> precedes a query, Without<T> can prove disjointness. When the query precedes NonSend<T>, Bevy can reject the system despite that filter. Without<IsResource> does not disjoin non-send access. The lint tracks supported parameter order and applies this distinction. Bevy 0.18 keeps resource and component access separate, so the lint skips cross-kind checks for that resolved ECS crate.

Example

use bevy::app::{App, Update};
use bevy::ecs::prelude::{Component, Query};

#[derive(Component)]
struct Position;

fn update(write: Query<&mut Position>, read: Query<&Position>) {}

fn main() {
    let mut app = App::new();
    let _configured_app = app.add_systems(Update, update);
}

Use instead

use bevy::app::{App, Update};
use bevy::ecs::prelude::{Component, Query, With, Without};

#[derive(Component)]
struct Position;

#[derive(Component)]
struct Selected;

fn update(
    selected: Query<&mut Position, With<Selected>>,
    unselected: Query<&Position, Without<Selected>>,
) {}

fn main() {
    let mut app = App::new();
    let _configured_app = app.add_systems(Update, update);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Warns when a free function registered directly with App::add_systems declares conflicting Res, ResMut, NonSend, or NonSendMut access to one resolved type. It follows direct parameters, ordinary nested tuples, and ParamSet members.

Why is this bad?

Bevy rejects conflicting resource access with B0002 while it initializes a system. A conflict can therefore stop an app before the system runs.

Known problems

The lint skips closures, indirect registrations, custom SystemParam implementations, and wrappers around supported parameters. It compares resolved resource types and reports conflicts outside a ParamSet or within one member. Only different members of the same ParamSet are sequential alternatives; accesses inside one member remain simultaneous, including accesses nested in ordinary tuples. This lint checks resource-to-resource access. Query-to-resource conflicts are checked by bevy-conflicting-query-params.

Example

use bevy::app::{App, Update};
use bevy::ecs::prelude::{Res, ResMut, Resource};

#[derive(Resource)]
struct State;

fn update(mut write: ResMut<State>, read: Res<State>) {}

fn main() {
    let mut app = App::new();
    let _configured_app = app.add_systems(Update, update);
}

Use instead

use bevy::app::{App, Update};
use bevy::ecs::prelude::{ResMut, Resource};

#[derive(Resource)]
struct State;

fn update(mut state: ResMut<State>) {}

fn main() {
    let mut app = App::new();
    let _configured_app = app.add_systems(Update, update);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks direct system registrations in Bevy's built-in repeating schedules for supported assignments that derive TextFont::font_size from time, glam::Vec3::distance, or a local value derived from those sources. The warning marks a potentially varying raster size. Bevy documents the related atlas-growth concern as B0005.

Why is this bad?

The effective font size contributes to the font-atlas key. When rendering needs a new key and glyph, Bevy may rasterize another atlas entry. An assignment alone does not prove that Bevy creates an entry, allocates a particular amount of memory, or crashes.

Known problems

The lint recognizes only direct registrations under Bevy's built-in repeating schedules and a bounded set of resolved expression forms. It tracks local values through direct assignments and +=, -=, *=, and /=; a statically known zero multiplier clears the local value's taint. It recognizes Bevy's resolved run_once condition on each registration, including through schedule-configuration wrappers, tuple groups or members, and conjunctions of Bevy system conditions. It still warns when the same function also has a registration that may repeat. Unknown or custom conditions remain potentially repeating unless a Bevy system-condition conjunction contains the resolved built-in run_once. Same-named application functions and disjunctions remain potentially repeating.

The lint also does not analyze runtime stability, paused time, whether either vector in Vec3::distance changes, or whether text rendering requests a new atlas key. A stable distance can still be assigned repeatedly.

The lint deliberately excludes rounding calls such as time.elapsed_secs().floor(), even though that value may change as elapsed time grows. It excludes Time<Fixed>::delta as a stable timestep; an application that changes that timestep at runtime can vary without a warning. Finite choices, constant reassignments, startup systems, and transform scaling are not reported.

The lint excludes Vec3::distance when both operands resolve directly to constant paths. Other constant vector expressions can still warn. Supported constant factors preserve f32 or f64 rounding at each arithmetic operation. The evaluator skips external constant bodies, integer arithmetic, casts, remainder operations, and values beyond its 16-level recursion limit.

The lint does not simplify algebraic cancellation, so a tracked value such as size -= size can still warn. It does not analyze control-flow reachability, including constant if or match branches and empty loops. A source-based warning therefore does not prove that the write executes or that its result changes.

Example

fn main() {
    let mut app = App::new();
    app.add_systems(Update, animate_nameplates);
}

fn animate_nameplates(mut fonts: Query<&mut TextFont>, time: Res<Time>) {
    for mut font in &mut fonts {
        font.font_size = FontSize::Px(20.0 + time.elapsed_secs() * 0.2);
    }
}

Use instead

Keep the raster size stable and animate the entity's transform scale when layout, wrapping, and visual quality allow it. Scaling can make enlarged text look pixelated.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks direct system registrations in Bevy's built-in repeating schedules for supported UiScale assignments derived from time, glam::Vec3::distance, or a local value derived from those sources. The warning marks a potentially varying UI scale.

Why is this bad?

When an application renders text, changing UiScale can change effective raster sizes and may cause Bevy to request additional font-atlas keys. The lint cannot prove that the application contains text or that a new key and glyph will be rendered.

Known problems

The lint recognizes only direct registrations under Bevy's built-in repeating schedules and a bounded set of resolved expression forms. It tracks local values through direct assignments and +=, -=, *=, and /=; a statically known zero multiplier clears the local value's taint. It recognizes Bevy's resolved run_once condition on each registration, including through schedule-configuration wrappers, tuple groups or members, and conjunctions of Bevy system conditions. It still warns when the same function also has a registration that may repeat. Unknown or custom conditions remain potentially repeating unless a Bevy system-condition conjunction contains the resolved built-in run_once. Same-named application functions and disjunctions remain potentially repeating.

The lint also does not analyze runtime stability, paused time, whether either vector in Vec3::distance changes, whether the app renders text, or whether text rendering requests a new atlas key. A stable distance can still be assigned repeatedly.

Constant-operand accumulation written directly to UiScale.0, such as scale.0 += 0.1, is outside this source-based analysis and is not reported. The lint deliberately excludes rounding calls such as time.elapsed_secs().floor(), even though that value may change as elapsed time grows. It excludes Time<Fixed>::delta as a stable timestep; an application that changes that timestep at runtime can vary without a warning. Finite choices, constant reassignments, startup systems, and transform scaling are not reported.

The lint excludes Vec3::distance when both operands resolve directly to constant paths. Other constant vector expressions can still warn. Supported constant factors preserve f32 or f64 rounding at each arithmetic operation. The evaluator skips external constant bodies, integer arithmetic, casts, remainder operations, and values beyond its 16-level recursion limit.

The lint does not simplify algebraic cancellation, so a tracked value such as size -= size can still warn. It does not analyze control-flow reachability, including constant if or match branches and empty loops. A source-based warning therefore does not prove that the write executes or that its result changes.

Example

fn main() {
    let mut app = App::new();
    app.add_systems(Update, animate_ui_scale);
}

fn animate_ui_scale(mut scale: ResMut<UiScale>, time: Res<Time>) {
    scale.0 = 1.0 + time.elapsed_secs() * 0.1;
}

Use instead

Set UI scale from startup configuration or discrete user choices. For a continuous visual change, scale a suitable UI subtree when its layout and interaction behavior remain correct.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for App::add_systems calls whose schedule argument is FixedUpdate.

This lint enforces a project rule: systems run in Update or another schedule the project chooses.

Why is this bad?

FixedUpdate runs zero, one, or several times per rendered frame. Presentation systems there, such as animation or camera movement, change in steps that do not line up with frames and stutter.

Known problems

The lint reports every FixedUpdate registration, including simulation systems that belong there. Projects that run systems in FixedUpdate should allow this lint.

The lint does not see systems added through Schedule::add_systems or through a generic schedule label parameter.

Example

fn build(app: &mut App) {
    app.add_systems(FixedUpdate, animate);
}

Use instead

fn build(app: &mut App) {
    app.add_systems(Update, animate);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for App::add_systems calls whose schedule argument is Update.

This lint enforces a project rule: simulation systems run in FixedUpdate or another schedule the project chooses.

Why is this bad?

Update runs once per rendered frame with a variable time step. Simulation in Update produces results that depend on frame rate, which breaks deterministic replays and networked lockstep.

Known problems

The lint reports every Update registration, including presentation systems that belong there. Projects that run systems in Update should allow this lint.

The lint does not see systems added through Schedule::add_systems or through a generic schedule label parameter.

Example

fn build(app: &mut App) {
    app.add_systems(Update, tick);
}

Use instead

fn build(app: &mut App) {
    app.add_systems(FixedUpdate, tick);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for crates that load two or more crates named bevy, for example two versions of the Bevy facade crate.

Why is this bad?

Each Bevy version defines its own Component, Plugin, and other traits and types. Types from one version do not work with the other, which causes confusing type errors. The extra copy also adds compile time and binary size.

Known problems

The lint only counts crates named bevy. It does not report duplicate versions of subcrates such as bevy_ecs when code loads only one bevy facade. It reports the whole crate, not the dependency entry in Cargo.toml.

Example

[dependencies]
bevy = "0.19.0"
bevy_018 = { package = "bevy", version = "0.18.0" }

Use instead

[dependencies]
bevy = "0.19.0"
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for repeated additions of the same local plugin type when its Plugin implementation uses the default is_unique behavior. It checks adjacent calls in one chain, repeated types inside a tuple, and separate direct calls on one local App or SubApp within a block.

Why is this bad?

Plugins are unique by default. App::add_plugins panics when it adds a unique plugin that the app already has.

Known problems

The lint resolves local plugin implementations and direct calls on local App or SubApp values. It does not infer uniqueness for external plugins, plugin groups, or local plugins that override is_unique. Separate-call analysis forgets its history when an intervening statement uses the app, so mutations, aliases, and reassignment stop the analysis. It does not trace app values across blocks or control-flow paths.

When local implementations share a generic plugin ADT, the lint resolves each concrete instantiation separately, such as GenericPlugin<u8> and GenericPlugin<u16>. If it cannot select one implementation for an instantiation, it leaves that type unclassified. Tuple and separate-call diagnostics provide help without a machine fix because removing an expression could change its side effects. A chained-call machine fix is limited to a repeated path argument with source spans outside macro expansions.

Example

use bevy::app::{App, Plugin};

struct GamePlugin;

impl Plugin for GamePlugin {
    fn build(&self, _: &mut App) {}
}

struct AudioPlugin;

impl Plugin for AudioPlugin {
    fn build(&self, _: &mut App) {}
}

fn build(app: &mut App) {
    app.add_plugins(GamePlugin).add_plugins(GamePlugin);
    app.add_plugins((AudioPlugin, AudioPlugin));
}

Use instead

use bevy::app::{App, Plugin};

struct GamePlugin;

impl Plugin for GamePlugin {
    fn build(&self, _: &mut App) {}
}

struct AudioPlugin;

impl Plugin for AudioPlugin {
    fn build(&self, _: &mut App) {}
}

fn build(app: &mut App) {
    app.add_plugins(GamePlugin);
    app.add_plugins(AudioPlugin);
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Warns when a local system directly registered with Bevy's FixedUpdate reads just_pressed or just_released from Res<ButtonInput<T>> or ResMut<ButtonInput<T>>.

Why is this bad?

With Bevy's standard InputPlugin, ButtonInput updates in PreUpdate, once per rendered frame. The fixed loop can run zero or several times after that update. A fixed system can miss an edge when no tick runs or process the same edge more than once when several ticks run.

Known problems

The lint checks direct local function registrations and cannot prove that InputPlugin is installed. It recognizes standalone resource parameters and resource parameters inside tuples. It skips Option, ParamSet, custom system parameters, and other parameter wrappers. It skips nested closure bodies because it cannot prove when they run.

It ignores system run conditions. Bevy's resolved run_once condition limits reads to at most once, but it cannot prevent edge loss when a rendered frame has no fixed tick: the standard input systems clear and rebuild ButtonInput in the next PreUpdate after its one-frame edge. Manually updated ButtonInput resources can be valid when their edges are maintained for each fixed tick.

Held-state reads such as pressed are not frame edges and do not trigger it. Custom buffered input is outside its analysis. Clearing an edge inside FixedUpdate can prevent repeated reads after a tick, but it cannot preserve an edge across a frame with no fixed tick. This semantic warning complements bevy-disallow-fixed-update-schedule, which enforces a project-wide policy.

Example


fn jump(keys: Res<ButtonInput<KeyCode>>) {
    if keys.just_pressed(KeyCode::Space) {
        // Apply the jump.
    }
}

app.add_systems(FixedUpdate, jump);

Use instead


#[derive(Resource, Default)]
struct FixedInput {
    jump: bool,
}

fn capture_jump(keys: Res<ButtonInput<KeyCode>>, mut input: ResMut<FixedInput>) {
    input.jump |= keys.just_pressed(KeyCode::Space);
}

fn apply_jump(mut input: ResMut<FixedInput>) {
    if core::mem::take(&mut input.jump) {
        // Apply the jump once.
    }
}

app.add_systems(
    RunFixedMainLoop,
    capture_jump.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
);
app.add_systems(FixedUpdate, apply_jump);

Register the capture system in RunFixedMainLoopSystems::BeforeFixedMainLoop, which runs after PreUpdate. If capture runs in PreUpdate instead, order it after bevy::input::InputSystems. The boolean buffer preserves one pending jump across zero-tick frames and consumes it once, but it coalesces multiple presses while one jump is pending.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for function parameters of type Query<..> whose query data contains &mut GlobalTransform.

Why is this bad?

Bevy computes GlobalTransform from Transform and the hierarchy during transform propagation. A direct write makes GlobalTransform disagree with Transform, and the next propagation for that entity overwrites it.

Known problems

The lint does not look inside Option<&mut GlobalTransform> or custom QueryData types.

Example

fn raise(mut query: Query<&mut GlobalTransform, With<Player>>) {
    for mut transform in &mut query {
        *transform = transform.mul_transform(Transform::from_xyz(0.0, 1.0, 0.0));
    }
}

Use instead

fn raise(mut query: Query<&mut Transform, With<Player>>) {
    for mut transform in &mut query {
        transform.translation.y += 1.0;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Warns when a direct tuple bundle passed to a recognized Bevy ECS spawn, insert, insert_if_new, or related-spawner spawn method combines an explicit enabled Msaa::Sample2, Msaa::Sample4, or Msaa::Sample8 with a render feature that conflicts with multisampling.

Deferred rendering is checked only when the bundle also contains Bevy Camera, Camera2d, or Camera3d. SSAO is checked only when it contains Camera3d and ScreenSpaceAmbientOcclusion. In Bevy 0.19.0, the OIT MSAA check selects entities with OrderIndependentTransparencySettings without a camera filter, so a bundle with OIT settings and enabled MSAA is reported even when it omits a camera.

Why is this bad?

When the Bevy 0.19.0 Core3d pipeline checks a matching camera with DeferredPrepass, it turns that camera's MSAA off. When ScreenSpaceAmbientOcclusionPlugin extracts a matching Camera3d with enabled MSAA, it logs an error and returns from the extraction system. ScreenSpaceAmbientOcclusion requires both depth and normal prepasses. When OrderIndependentTransparencyPlugin checks an entity with OIT settings and more than one MSAA sample, it panics.

The 0.19.0 SSAO extraction system's early return also stops extraction for later matching cameras in that pass. Bevy 0.19.1 changed that path to continue with the next camera.

Known problems

The runtime consequences require the corresponding pipeline or plugin system to run. The lint does not inspect the app's plugin setup, runtime state, or later changes to an entity.

The lint recognizes explicit Msaa::Sample2, Msaa::Sample4, and Msaa::Sample8 paths in direct tuple bundles. It does not infer Msaa::default(), Msaa::from_samples, local variables, custom bundles, or component combinations assembled across separate calls. It reports the components in one bundle expression and does not prove the entity's final configuration.

The lint does not evaluate whether insert_if_new applies a bundle to the current entity.

The runtime details here are checked against Bevy 0.19.0: the Core3d MSAA check, SSAO extraction, and OIT MSAA check. Later Bevy versions may change their checks or consequences.

Example

This OIT bundle is reported in Bevy 0.19.0 even though it has no camera, because the plugin's MSAA check filters only on its settings component.

use bevy::core_pipeline::oit::OrderIndependentTransparencySettings;
use bevy::ecs::world::World;
use bevy::render::view::Msaa;

fn main() {
    let mut world = World::new();
    world.spawn((Msaa::Sample4, OrderIndependentTransparencySettings::default()));
}

Use instead

Set Msaa::Off on the same entity as each incompatible render component. Deferred rendering and SSAO require a matching camera; Bevy's standard OIT setup puts its settings on the camera.

use bevy::camera::Camera3d;
use bevy::core_pipeline::oit::OrderIndependentTransparencySettings;
use bevy::ecs::world::World;
use bevy::render::view::Msaa;

fn main() {
    let mut world = World::new();
    world.spawn((
        Camera3d::default(),
        Msaa::Off,
        OrderIndependentTransparencySettings::default(),
    ));
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for function parameters of type Query<..> whose query data contains &mut InheritedVisibility.

Why is this bad?

Bevy computes InheritedVisibility from each entity's Visibility and its parent's InheritedVisibility. A direct write makes it disagree with the hierarchy, and the next visibility propagation for that entity overwrites it.

Known problems

The lint does not look inside Option<&mut InheritedVisibility> or custom QueryData types.

Example

fn hide(mut query: Query<&mut InheritedVisibility, With<Enemy>>) {
    for mut visibility in &mut query {
        *visibility = InheritedVisibility::HIDDEN;
    }
}

Use instead

fn hide(mut query: Query<&mut Visibility, With<Enemy>>) {
    for mut visibility in &mut query {
        *visibility = Visibility::Hidden;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for App::insert_resource and App::init_resource calls that add a Messages<M> resource.

Why is this bad?

App::add_message adds the Messages<M> resource and the system that clears old messages each frame. A manually added resource lacks that system, so no system drops old messages and the buffer grows without bound.

Known problems

The lint does not check World::insert_resource, World::init_resource, or Commands.

Example

fn build(app: &mut App) {
    app.init_resource::<Messages<Ping>>();
}

Use instead

fn build(app: &mut App) {
    app.add_message::<Ping>();
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks statically evaluable FontSize::Px values at or below zero and values above 1000 logical pixels, matching the Bevy 0.19 text-pipeline warnings.

Why is this bad?

Nonpositive pixel sizes display no text. Sizes above 1000 logical pixels trigger a Bevy warning because they can use excessive font-atlas memory. Bevy checks this threshold before UI scaling.

Known problems

The lint checks statically evaluable f32 literals and local f32 constant paths. It evaluates unary negation, empty blocks, and up to 16 recursive levels of +, -, *, or / when each expression resolves to f32. It skips %, casts, arithmetic performed as integers or f64, runtime expressions, viewport units, and rem units. It skips trait-associated constants, including defaults, because a generic type can override a default; local inherent associated constants with bodies are checked. A nonpositive size can be intentional, so use a visibility state when the intent is to hide text.

Example

fn main() {
    let font_size = FontSize::Px(1001.0);
    let _ = font_size;
}

Use instead

Use a positive size such as FontSize::Px(24.0). For larger visual text, keep a valid raster size and scale the entity when layout and visual quality allow it.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to Messages::iter_current_update_messages.

Why is this bad?

The method only returns messages written since the last Messages::update call. The next update drops messages that code writes after the call and before the update, without reading them.

Known problems

The lint reports every call, including code that runs at a point where that window is correct.

Example

fn count_pings(messages: Res<Messages<Ping>>) -> usize {
    messages.iter_current_update_messages().count()
}

Use instead

fn count_pings(mut pings: MessageReader<Ping>) -> usize {
    pings.read().count()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for local components with at least eight named fields and a size over 64 bytes.

Why is this bad?

Bevy tracks access and changes per component. A system that needs one field of a large component borrows all of it, so it conflicts with every system that writes any other field. Changing one field also marks the whole component as changed.

Known problems

The lint uses fixed thresholds. It still reports some cohesive components that exceed them.

The lint skips resources, tuple structs, enums, and generic components.

Example

#[derive(Component)]
struct Agent {
    position: Vec3,
    velocity: Vec3,
    acceleration: Vec3,
    health: f32,
    stamina: f32,
    hunger: f32,
    target: Option<Entity>,
    path_index: usize,
    team: u32,
}

Use instead

#[derive(Component)]
struct Kinematics {
    position: Vec3,
    velocity: Vec3,
    acceleration: Vec3,
}

#[derive(Component)]
struct Vitals {
    health: f32,
    stamina: f32,
    hunger: f32,
}

#[derive(Component)]
struct Navigation {
    target: Option<Entity>,
    path_index: usize,
}

#[derive(Component)]
struct Team(u32);
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for query parameters with a Changed<T> filter when T is a large local component and the function uses fewer than half of T's named fields.

A large component has at least eight named fields and a size over 64 bytes, as in bevy-large-component.

Why is this bad?

Bevy tracks changes per component. A write to any field of T marks all of T as changed. The filter therefore matches entities even when none of the fields this system reads have changed.

Known problems

The lint counts field reads on the filtered component type, including inside closures. Fields with the same name on another type do not count. The lint does not count fields accessed only through destructuring patterns.

Example

// `Agent` has nine named fields, including `health`, and is over 64 bytes.
fn react_to_health(query: Query<&Agent, Changed<Agent>>) {
    for agent in &query {
        info!("health: {}", agent.health);
    }
}

Use instead

Move the field into its own component and filter on that component.

fn react_to_health(query: Query<&Health, Changed<Health>>) {
    for health in &query {
        info!("health: {}", health.0);
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks, within one function body, for repeated LoadBuilder::with_guard calls on one builder returned directly by AssetServer::load_builder().

Why is this bad?

Bevy keeps only the last guard. Replacing an earlier guard drops it before loading starts, which can signal completion too early.

Known problems

The lint follows direct builder chains, with_settings and override_unapproved calls, and local moves whose value identity is clear. It does not follow other builder origins, including World::load_builder() or LoadContext::load_builder(), opaque helpers, assignments to mutable builders, or aliases it cannot prove. Replacing a guard can be intentional.

The lint invalidates guard facts when a tracked builder escapes through a mutable-reference argument to an opaque call. Implicit mutable method receivers are not analyzed. It cannot inspect the helper's behavior, so later guard replacements can remain unreported.

The lint clears tracked facts when it invokes a closure value or passes one to an opaque function or method. It also clears them for an opaque method invoked on a closure receiver. This conservative handling does not inspect captures, so an unrelated closure can suppress later warnings. Creating and dropping a closure without invoking or passing it preserves tracking.

Example

let _handle = asset_server
    .load_builder()
    .with_guard(())
    .with_guard(())
    .load_untyped("assets/example.png");

Use instead

let _handle = asset_server
    .load_builder()
    .with_guard(((), ()))
    .load_untyped("assets/example.png");
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a free function named main that returns () and calls App::run as a statement, discarding the returned AppExit.

Why is this bad?

App::run returns the AppExit that the app requested. When main discards it, the process exits with status 0 even after AppExit::error(), so scripts and CI cannot detect the failure.

Known problems

The lint checks every free function named main, including one inside a module. It does not report let _ = app.run();.

Example

fn main() {
    App::new().run();
}

Use instead

fn main() -> AppExit {
    App::new().run()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for wildcard bindings that discard direct calls to MessageReader::read() or MessageReader::read_with_id().

Why is this bad?

Both methods create lazy iterators. The reader advances its cursor as an iterator is consumed, so dropping it leaves unread messages available to the next read.

Known problems

The lint reports only wildcard let initializers that are direct, method-syntax calls to read() or read_with_id(). Bevy's iterator types do not carry a must_use attribute, so rustc and default Clippy do not diagnose these direct wildcard bindings. Clippy's opt-in let_underscore_must_use lint diagnoses standard iterator adapter chains, so this lint does not inspect those chains.

Strict configurations that enable unused_results already diagnose semicolon expressions. It offers no automatic fix because processing messages and intentionally discarding them require different code. Repeated is_empty() checks can be intentional. The lint does not analyze helper functions, local variables, explicit drop, or control flow.

Example

use bevy::ecs::message::{Message, MessageReader};

#[derive(Message)]
struct Collision;

fn play_collision_sound() {}

fn play_sound(mut collisions: MessageReader<Collision>) {
    let _ = collisions.read();
    play_collision_sound();
}

Use instead

use bevy::ecs::message::{Message, MessageReader};

#[derive(Message)]
struct Collision;

fn play_collision_sound() {}

fn play_sound(mut collisions: MessageReader<Collision>) {
    for _collision in collisions.read() {
        play_collision_sound();
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for unit structs, such as struct Player;, that implement Component but not Clone.

Why is this bad?

Bevy's entity cloning copies a component through Clone when the type implements it. Without Clone, the marker depends on the world's default clone handler, which can skip it, so a cloned entity can lose the marker. A unit struct has no state, so Clone costs nothing.

Known problems

None known.

Example

#[derive(Component)]
struct Player;

Use instead

#[derive(Component, Clone)]
struct Player;
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for unit structs, such as struct Player;, that implement Component but not Copy.

Why is this bad?

Without Copy, passing the marker by value moves it, and generic code with a Copy bound cannot use it. A unit struct has no state, so Copy costs nothing.

Known problems

None known.

Example

#[derive(Component, Clone)]
struct Player;

Use instead

#[derive(Component, Clone, Copy)]
struct Player;
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for unit structs, such as struct Player;, that implement Component but not Default.

Why is this bad?

Bevy builds required components with Default. Without it, #[require(Player)] on another component does not compile, and generic code such as insert(T::default()) cannot use the marker. A unit struct has no state, so Default costs nothing.

Known problems

None known.

Example

#[derive(Component)]
struct Player;

Use instead

#[derive(Component, Default)]
struct Player;
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for local types that implement Component, Resource, Message, or Event but not Reflect.

Why is this bad?

Bevy's reflection-based tools cannot see a type without Reflect. Scenes do not save it, the remote protocol cannot read it, and inspectors do not show it.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. Test-only types do not reach scenes, inspectors or the remote protocol. The ordinary build of the same target still checks types outside #[cfg(test)].

The lint reports every such type, including internal types that no tool needs to inspect. Deriving Reflect also requires every field type to implement Reflect, or the code must mark the field #[reflect(ignore)].

Example

#[derive(Component)]
struct Health(u32);

Use instead

#[derive(Component, Reflect)]
#[reflect(Component)]
struct Health(u32);
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Warns when resolved MouseMotion::delta or AccumulatedMouseMotion::delta is multiplied by resolved Bevy frame delta time in seconds.

Why is this bad?

Mouse delta already measures movement during the frame. Multiplying it by elapsed seconds makes camera sensitivity depend on frame duration.

Known problems

The lint checks local multiplication expressions using Bevy's mouse-motion fields and non-fixed Time delta methods: delta_secs, delta_secs_f64, or delta().as_secs_f32/as_secs_f64. It excludes resolved Time<Fixed> deltas because they represent fixed-step duration. It leaves rate conversion and direct rate-to-displacement reconstruction clean. Keyboard velocity multiplied by delta time is valid and does not trigger this lint.

The lint follows value-producing arithmetic, casts, field access and indexing, block tails, and branch results. It skips conditions, match selectors and guards, and discarded block statements. It does not trace values through locals, function calls, or custom wrappers.

Example


fn rotate_camera(mut motion: MessageReader<MouseMotion>, time: Res<Time>) {
    for event in motion.read() {
        let _rotation = event.delta.x * time.delta_secs();
    }
}

Use instead


#[derive(Resource)]
struct MouseSensitivity(f32);

fn rotate_camera(mut motion: MessageReader<MouseMotion>, sensitivity: Res<MouseSensitivity>) {
    for event in motion.read() {
        let _rotation = event.delta.x * sensitivity.0;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for systems that take &mut World and use it only for typed query, resource, or entity access. The checked World methods are entities, get, get_mut, get_resource, get_resource_mut, query, query_filtered, resource, and resource_mut, plus QueryState methods that take the world.

Why is this bad?

An exclusive system blocks every other system while it runs. Normal system parameters declare only the access the system needs, so Bevy can run it in parallel with other systems.

Known problems

The lint only checks free functions passed to App::add_systems as a plain path or in a tuple of paths. It skips methods, closures, and functions that pass the world to other code.

Example

fn count_positions(world: &mut World) {
    let mut query = world.query::<&Position>();
    let count = query.iter(world).count();
    info!("{count} positions");
}

fn build(app: &mut App) {
    app.add_systems(Update, count_positions);
}

Use instead

fn count_positions(query: Query<&Position>) {
    let count = query.iter().count();
    info!("{count} positions");
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks query parameters whose data is a local custom QueryData type with more than eight named fields. It warns when the function uses at most half of those fields.

Why is this bad?

The system keeps the access of every field in the query type, including fields it never reads. Unused &mut fields still stop other systems that use those components from running in parallel.

Known problems

The lint counts fields on the derived QueryData item types, including inside closures. Fields with the same name on unrelated types do not count. The lint does not count fields accessed only through destructuring patterns.

Example

#[derive(QueryData)]
#[query_data(mutable)]
struct AgentQuery {
    position: &'static mut Position,
    velocity: &'static Velocity,
    health: &'static mut Health,
    stamina: &'static mut Stamina,
    hunger: &'static mut Hunger,
    target: &'static mut Target,
    path: &'static mut Path,
    team: &'static Team,
    name: &'static Name,
}

fn integrate(mut query: Query<AgentQuery>) {
    for mut agent in &mut query {
        agent.position.0 += agent.velocity.0;
    }
}

Use instead

fn integrate(mut query: Query<(&mut Position, &Velocity)>) {
    for (mut position, velocity) in &mut query {
        position.0 += velocity.0;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for parameters of type Query<&T> that the function uses only through is_empty() or iter().count().

Why is this bad?

The system never reads T, but &T still adds read access to T. The system then conflicts with systems that write T. A With<T> filter matches the same entities without that access.

Known problems

Any other use of the query prevents the warning. The lint follows query uses into closure bodies, so a closure that reads a fetched component also prevents the warning. The lint recognizes only a single shared component reference, &T, as query data.

Example

fn count_enemies(query: Query<&Enemy>) {
    let count = query.iter().count();
    info!("{count} enemies");
}

Use instead

fn count_enemies(query: Query<Entity, With<Enemy>>) {
    let count = query.iter().count();
    info!("{count} enemies");
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for query parameters with &mut T in their data that the function uses only through read-only query methods: as_readonly, contains, get, get_many, is_empty, iter, iter_combinations, iter_many, many, par_iter, and single.

Why is this bad?

&mut T gives the system write access to T. Bevy then cannot run it in parallel with any other system that reads or writes T, even though this system only reads.

Known problems

Any other use of the query prevents the warning, including &query in a for loop. The lint follows query uses into closure bodies, so a mutation inside a closure prevents the warning. It recognizes mutable references directly in the query data or in tuples.

Example

fn log_positions(query: Query<&mut Position>) {
    for position in query.iter() {
        let _position = position.0;
    }
}

Use instead

fn log_positions(query: Query<&Position>) {
    for position in query.iter() {
        let _position = position.0;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Warns when a direct Bevy ECS tuple bundle passed to a recognized spawn, insert, insert_if_new, or related-spawner spawn method contains both Bevy TextSpan and TextLayout components.

Why is this bad?

Bevy's text processing emits a once-only warning when it processes a changed TextSpan entity that also has TextLayout. TextLayout belongs on a root text entity with Text or Text2d.

Known problems

The lint recognizes direct tuple bundles, including nested tuples. It does not inspect custom bundles, local variables, or combinations assembled across separate calls. It reports the component combination in one bundle expression and does not prove the entity's final hierarchy or configuration. Bevy's text-processing system must run on the span for its runtime warning to occur.

The lint does not evaluate whether insert_if_new applies a bundle to the current entity.

The runtime behavior here is checked against Bevy 0.19.0's text processing source. Later Bevy versions may change the warning or its conditions.

Example

use bevy::ecs::world::World;
use bevy::text::{TextLayout, TextSpan};

fn main() {
    let mut world = World::new();
    world.spawn((TextSpan::new("name"), TextLayout::default()));
}

Use instead

Put TextLayout on the root entity with Text or Text2d, then parent each span under that root.

use bevy::ecs::{hierarchy::ChildOf, world::World};
use bevy::text::{TextLayout, TextSpan};
use bevy::ui::widget::Text;

fn main() {
    let mut world = World::new();
    let root = world
        .spawn((Text::new("Name plate"), TextLayout::default()))
        .id();
    let child = world.spawn(TextSpan::new("Player")).id();
    world.entity_mut(child).insert(ChildOf(root));
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for Time::elapsed_secs() cast to f64 with as f64.

Why is this bad?

elapsed_secs returns an f32, which loses precision as elapsed time grows. After about five hours, consecutive f32 values are about 2 ms apart. The cast to f64 cannot recover the lost precision. elapsed_secs_f64 computes the value as an f64 from the start.

Known problems

The lint recognizes as f64, f64::from, and .into() around a direct Time::elapsed_secs() call. It does not resolve a function-pointer call to f64::from. Macro-expanded conversions get a diagnostic, but no source rewrite.

Example

fn wave(time: &Time) -> f64 {
    (time.elapsed_secs() as f64).sin()
}

Use instead

fn wave(time: &Time) -> f64 {
    time.elapsed_secs_f64().sin()
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for local types that implement Plugin without a Plugin name suffix, and local types that implement SystemSet without a Systems name suffix.

Why is this bad?

Bevy and its ecosystem use these suffixes. Without them, readers cannot tell at a call site such as add_plugins(Gameplay) or .in_set(Movement) what role the type has.

Known problems

The lint does not check plugins written as functions that take &mut App.

Example

struct Gameplay;

impl Plugin for Gameplay {
    fn build(&self, _app: &mut App) {}
}

#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)]
struct Movement;

Use instead

struct GameplayPlugin;

impl Plugin for GameplayPlugin {
    fn build(&self, _app: &mut App) {}
}

#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)]
struct MovementSystems;
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks parameters of type Query<EntityRef> or Query<EntityMut> when entities yielded or fetched directly from that query reach a typed component call: contains, get, get_components, get_components_mut, get_mut, or get_ref. It recognizes query iteration in for loops and direct iter().for_each or iter_mut().for_each closures. It also recognizes get, get_mut, single, single_mut, iter().next(), and iter_mut().next() results after direct unwrap, expect, if let, match, or let-else extraction.

Each query parameter is checked independently. An EntityRef or EntityMut obtained from another query or from World does not trigger a diagnostic for this parameter.

Why is this bad?

EntityRef claims read access to every component, and EntityMut claims write access to every component. The system then conflicts with all systems that write any component, even though it only uses a fixed set.

Known problems

The lint follows only the direct origins listed above. Aliases of the Query parameter, computed query receivers such as helper-call results, iterator adapters, and named callbacks passed to for_each remain unknown. Tuple query data, such as (Entity, EntityRef), is not checked. For an assignment, the lint checks the right-hand side with the old origin. It then drops that origin if it cannot prove that the assigned value comes from the selected query.

At control-flow joins, the lint does not restore lost origins. An assignment in one branch can make later uses unknown.

Example

fn log_positions(query: Query<EntityRef>) {
    for entity in &query {
        if let Some(position) = entity.get::<Position>() {
            info!("{}", position.0);
        }
    }
}

Use instead

fn log_positions(query: Query<&Position>) {
    for position in &query {
        info!("{}", position.0);
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for () values in the bundle passed to spawn, insert, or insert_if_new on Commands, World, EntityCommands, EntityWorldMut, RelatedSpawner, or RelatedSpawnerCommands.

Why is this bad?

A () in a bundle adds no component. It often comes from code that drops a call's result by mistake. Examples include a helper that returns () or a block that ends with a semicolon.

Known problems

The lint does not check other bundle-taking APIs, such as with_child or the children! macro.

Example

fn spawn_player(mut commands: Commands) {
    commands.spawn((Player, ()));
}

Use instead

Remove the () value. To spawn an entity without components, call spawn_empty instead of spawn(()).

fn spawn_player(mut commands: Commands) {
    commands.spawn(Player);
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for query parameters whose data contains more than five component references, or more than four &mut references. References inside local custom QueryData types count toward the total. For a local custom QueryData type, the total limit is eight references instead of five because a named type already groups related access. The &mut limit stays at four.

Why is this bad?

Each reference adds access that can conflict with other systems, so wide queries limit parallel execution. A wide query also often means one system combines several independent behaviors.

Known problems

The lint uses fixed limits. The lint counts only &T and &mut T directly in the query data, in tuples, and in fields of local QueryData types. It does not count Option<&T>, Has<T>, Ref<T>, or QueryData types from other crates.

Example

fn update(query: Query<(&Position, &Velocity, &Health, &Stamina, &Target, &Team)>) {
    for (position, velocity, health, stamina, target, team) in &query {
        // movement, combat, and targeting in one system
    }
}

Use instead

Split the system by behavior, and give each system only the components it uses.

fn movement(query: Query<(&Position, &Velocity)>) {
    for (position, velocity) in &query {
        // movement only
    }
}

fn combat(query: Query<(&Health, &Stamina, &Team)>) {
    for (health, stamina, team) in &query {
        // combat only
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::entity.

Why is this bad?

World::entity panics when the entity does not exist. A despawned or stale Entity then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the entity exists.

Example

fn has_health(world: &World, id: Entity) -> bool {
    world.entity(id).contains::<Health>()
}

Use instead

fn has_health(world: &World, id: Entity) -> bool {
    world
        .get_entity(id)
        .is_ok_and(|entity| entity.contains::<Health>())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::entity_mut.

Why is this bad?

World::entity_mut panics when the entity does not exist. A despawned or stale Entity then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the entity exists.

Example

fn heal(world: &mut World, id: Entity) {
    world.entity_mut(id).insert(Health(100));
}

Use instead

fn heal(world: &mut World, id: Entity) {
    if let Ok(mut entity) = world.get_entity_mut(id) {
        entity.insert(Health(100));
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::insert_batch.

Why is this bad?

World::insert_batch panics when any entity in the batch does not exist. One despawned entity then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that every entity exists.

Example

fn mark_all(world: &mut World, ids: Vec<Entity>) {
    world.insert_batch(ids.into_iter().map(|id| (id, Marker)));
}

Use instead

fn mark_all(world: &mut World, ids: Vec<Entity>) -> Result<(), BevyError> {
    world.try_insert_batch(ids.into_iter().map(|id| (id, Marker)))?;
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::insert_batch_if_new.

Why is this bad?

World::insert_batch_if_new panics when any entity in the batch does not exist. One despawned entity then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that every entity exists.

Example

fn mark_all(world: &mut World, ids: Vec<Entity>) {
    world.insert_batch_if_new(ids.into_iter().map(|id| (id, Marker)));
}

Use instead

fn mark_all(world: &mut World, ids: Vec<Entity>) -> Result<(), BevyError> {
    world.try_insert_batch_if_new(ids.into_iter().map(|id| (id, Marker)))?;
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::non_send.

Why is this bad?

World::non_send panics when the non-send value of that type is absent. A missing setup step then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the value exists.

Example

fn window_count(world: &World) -> usize {
    world.non_send::<WindowRegistry>().len()
}

Use instead

fn window_count(world: &World) -> usize {
    world
        .get_non_send::<WindowRegistry>()
        .map_or(0, |registry| registry.len())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::non_send_mut.

Why is this bad?

World::non_send_mut panics when the non-send value of that type is absent. A missing setup step then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the value exists.

Example

fn clear_windows(world: &mut World) {
    world.non_send_mut::<WindowRegistry>().clear();
}

Use instead

fn clear_windows(world: &mut World) {
    if let Some(mut registry) = world.get_non_send_mut::<WindowRegistry>() {
        registry.clear();
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::resource.

Why is this bad?

World::resource panics when the resource is absent. A missing setup step then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the resource exists.

Example

fn current_score(world: &World) -> u32 {
    world.resource::<Score>().0
}

Use instead

fn current_score(world: &World) -> u32 {
    world.get_resource::<Score>().map_or(0, |score| score.0)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::resource_mut.

Why is this bad?

World::resource_mut panics when the resource is absent. A missing setup step then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the resource exists.

Example

fn add_point(world: &mut World) {
    world.resource_mut::<Score>().0 += 1;
}

Use instead

fn add_point(world: &mut World) {
    if let Some(mut score) = world.get_resource_mut::<Score>() {
        score.0 += 1;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::resource_ref.

Why is this bad?

World::resource_ref panics when the resource is absent. A missing setup step then stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the resource exists.

Example

fn score_changed(world: &World) -> bool {
    world.resource_ref::<Score>().is_changed()
}

Use instead

fn score_changed(world: &World) -> bool {
    world
        .get_resource_ref::<Score>()
        .is_some_and(|score| score.is_changed())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::run_schedule.

Why is this bad?

World::run_schedule panics when no schedule with that label exists. When code never adds the label, World::run_schedule stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the schedule exists.

Example

fn run_gameplay(world: &mut World) {
    world.run_schedule(Gameplay);
}

Use instead

fn run_gameplay(world: &mut World) -> Result<(), BevyError> {
    world.try_run_schedule(Gameplay)?;
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to World::schedule_scope.

Why is this bad?

World::schedule_scope panics when no schedule with that label exists. When code never adds the label, World::schedule_scope stops the whole app instead of taking an error path.

Known problems

The lint skips crates that rustc compiles as a test harness (--test), such as unit and integration test builds. A panic there fails one test. The ordinary build of the same target still checks code outside #[cfg(test)].

The lint reports every call, including calls where the code already guarantees that the schedule exists.

Example

fn run_gameplay(world: &mut World) {
    world.schedule_scope(Gameplay, |world, schedule| schedule.run(world));
}

Use instead

fn run_gameplay(world: &mut World) -> Result<(), BevyError> {
    world.try_schedule_scope(Gameplay, |world, schedule| schedule.run(world))?;
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for function parameters of type Query<..> whose query data contains a &T or &mut T where T is a zero-sized type, such as a unit marker component.

Why is this bad?

A reference to a zero-sized component carries no data. It still adds access to T to the system, so the system conflicts with systems that write T. A With<T> filter matches the same entities without that access.

Known problems

The lint only checks references directly in the query data or in tuples. It does not look inside Option<&T> or custom QueryData types.

Example

fn move_players(mut query: Query<(&mut Transform, &Player)>) {
    for (mut transform, _player) in &mut query {
        transform.translation.x += 1.0;
    }
}

Use instead

fn move_players(mut query: Query<&mut Transform, With<Player>>) {
    for mut transform in &mut query {
        transform.translation.x += 1.0;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the names of bool struct fields, local variables, function and method parameters, functions and methods that return bool, and bool constants, statics, and associated constants. It warns when the name does not identify a predicate.

A snake_case name passes when it starts with a predicate word such as is_, has_, can_, should_, does_, contains_, needs_, or uses_, or contains one such as _is_, _has_, or _are_. A constant or static name passes when it starts with IS_ or HAS_.

Why is this bad?

A name like ready or enabled can be a flag, a count, a state enum, or a callback. Reading if config.enabled does not tell the reader whether the value is a bool. A predicate name such as is_enabled makes the type and the meaning clear at the call site.

Known problems

This repository uses a long, repository-specific list of accepted names. Names that start with words such as default_, from_, in_, or path_ pass. Names that end with _name, _type, _value, or _path, and a few exact names such as value, seen, and escaped pass without a predicate word.

The lint skips method and associated constant names in trait implementations, closure parameters, destructuring patterns, _ placeholders, and names that macros create.

Example

struct Job {
    ready: bool,
}

fn ready(enabled: bool) -> bool {
    let cached = enabled;
    cached
}

const ENABLED: bool = true;

Use instead

struct Job {
    is_ready: bool,
}

fn is_ready(is_enabled: bool) -> bool {
    let is_cached = is_enabled;
    is_cached
}

const IS_ENABLED: bool = true;
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the declared return type of functions, methods, and trait methods for a Box<dyn Future>, such as Pin<Box<dyn Future<Output = T>>> or a BoxFuture alias. A boxed future nested inside another return type, such as Option<BoxFuture<'_, T>> or a tuple, also triggers the lint.

Why is this bad?

Each call allocates the future on the heap and calls it through a vtable. The signature also hides that the function is async and loses auto traits such as Send unless the signature includes them. An async fn or -> impl Future returns the concrete future without these costs.

Known problems

Some cases require boxing: recursive async functions, dyn-compatible traits, and collections of different futures. The lint warns in these cases too.

The lint skips trait impl methods because the trait fixes their signature; it reports the trait declaration instead. It also skips return types produced by a macro, such as the boxed futures that #[async_trait] generates.

Example

use std::future::Future;
use std::pin::Pin;

fn name_len(name: String) -> Pin<Box<dyn Future<Output = usize> + Send>> {
    Box::pin(async move { name.len() })
}

Use instead

async fn name_len(name: String) -> usize {
    name.len()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks enums whose names end in Error or Errors. It reports variants that carry a String or &str in a field named message, details, reason, or error. It also reports a tuple variant with one String or &str field when the variant name is Message, Details, Reason, or Error.

Why is this bad?

A free-form string turns one variant into a catch-all. Callers cannot match on the failure kind without parsing text. The variant loses the original error source, and the text can carry user data into logs.

Known problems

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. The field type must then resolve to String or &str, so aliases of those types match. The lint misses Box<str>, Cow<'_, str>, and other text types. It skips enums generated by macros.

The lint skips enums with other names, such as Failure. It also skips other field and variant names, such as text or Description(String).

Example

enum ConfigError {
    InvalidPort { message: String },
    Reason(String),
}

Use instead

enum ConfigError {
    InvalidPort { source: std::num::ParseIntError },
    MissingKey { key: &'static str },
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Arg method chain that calls allow_hyphen_values(true) but never calls num_args.

Why is this bad?

allow_hyphen_values lets Clap treat a value such as --verbose or -x as this argument's value instead of another option. Clap requires the argument to take values, and the number of tokens it can take decides how much of the command line it can capture. Without num_args, the action supplies that count, but this setting does not show it. If the action is a flag action such as ArgAction::SetTrue, clap panics in debug builds.

Known problems

The lint checks only one method chain. It warns when a later statement calls num_args on the same Arg. It does not warn when the final action call is ArgAction::Set or ArgAction::Append, because that action already makes the argument take one value. It checks only the final allow_hyphen_values call, and only when its argument is the literal true.

Example

use clap::Arg;

fn pattern_arg() -> Arg {
    Arg::new("pattern").long("pattern").allow_hyphen_values(true)
}

Use instead

use clap::Arg;

fn pattern_arg() -> Arg {
    Arg::new("pattern")
        .long("pattern")
        .allow_hyphen_values(true)
        .num_args(1)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Arg method chain that calls allow_negative_numbers(true) but never calls num_args.

Why is this bad?

allow_negative_numbers lets Clap treat a value such as -5 as this argument's value instead of a short option. Clap requires the argument to take values, and the number of tokens it can take decides how much of the command line it can capture. Without num_args, the action supplies that count, but this setting does not show it. If the action is a flag action such as ArgAction::SetTrue, clap panics in debug builds.

Known problems

The lint checks only one method chain. It warns when a later statement calls num_args on the same Arg. It does not warn when the final action call is ArgAction::Set or ArgAction::Append, because that action already makes the argument take one value. It checks only the final allow_negative_numbers call, and only when its argument is the literal true.

Example

use clap::Arg;

fn offset_arg() -> Arg {
    Arg::new("offset").long("offset").allow_negative_numbers(true)
}

Use instead

use clap::Arg;

fn offset_arg() -> Arg {
    Arg::new("offset")
        .long("offset")
        .allow_negative_numbers(true)
        .num_args(1)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Arg method chain that sets value_hint(ValueHint::CommandWithArguments) on an argument that also has a long or short name.

Why is this bad?

Clap allows ValueHint::CommandWithArguments only on a positional argument. An argument with a long or short name is an option, so clap panics in debug builds when code builds the command.

Known problems

The lint checks only one method chain. It does not check an Arg changed in later statements. A long or short name counts when its final call passes any value other than None. A name passed as an Option value computed at runtime does not count.

Example

use clap::{Arg, ValueHint};

fn command_arg() -> Arg {
    Arg::new("command")
        .long("command")
        .value_hint(ValueHint::CommandWithArguments)
}

Use instead

Remove the long and short names to make the argument positional. If it must stay an option, use a value hint that applies to one value, such as ValueHint::CommandName.

use clap::{Arg, ValueHint};

fn command_arg() -> Arg {
    Arg::new("command").value_hint(ValueHint::CommandWithArguments)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[command(author)] or #[command(author = ...)] attribute on a Clap-derived type or Subcommand variant that does not also set help_template.

Why is this bad?

Clap's default help template does not show the author. The author set by the attribute never appears in -h or --help output unless a custom help_template includes {author}. Code that adds the attribute to show the author in help does nothing.

Known problems

The lint warns when code sets the template later through the builder API, such as Cli::command().help_template(...). It does not check whether the template contains an author placeholder.

Example

use clap::Parser;

#[derive(Parser)]
#[command(author)]
struct Cli {}

Use instead

Add a template that contains {author}, or remove author.

use clap::Parser;

#[derive(Parser)]
#[command(author, help_template = "{about}\n\nBy {author}\n\n{usage}\n\n{all-args}")]
struct Cli {}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a bool field in a Clap-derived type that uses the ArgAction::SetTrue action, inferred or explicit, and sets default_value_t = true or default_value = "true".

Why is this bad?

ArgAction::SetTrue sets the field to true when the flag is present. With a default of true, the field is true whether or not the flag is present. The flag then does nothing, and users cannot turn the behavior off.

Known problems

The lint checks only the literal defaults default_value_t = true and default_value = "true". It does not check a default computed by an expression.

Example

use clap::Parser;

#[derive(Parser)]
struct Cli {
    #[arg(long, default_value_t = true)]
    color: bool,
}

Use instead

Remove the default for a flag that turns a behavior on. For a flag that turns a default-on behavior off, use a negative name and ArgAction::SetFalse.

use clap::{ArgAction, Parser};

#[derive(Parser)]
struct Cli {
    #[arg(long = "no-color", action = ArgAction::SetFalse, default_value_t = true)]
    color: bool,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an action = ... setting on a Clap-derived field when the action is the one Clap already infers from the field type:

  • bool infers ArgAction::SetTrue.
  • T, Option<T>, and Option<Option<T>> infer ArgAction::Set.
  • Vec<T>, Option<Vec<T>>, Vec<Vec<T>>, and Option<Vec<Vec<T>>> infer ArgAction::Append.

Why is this bad?

The setting does not change field parsing. It makes the field look like it has special behavior, and it can go stale when the field type changes.

Known problems

The lint matches only the paths ArgAction::X, clap::ArgAction::X, and ::clap::ArgAction::X. It does not report an action reached through a constant, a re-export, or another expression.

Example

use clap::{ArgAction, Parser};

#[derive(Parser)]
struct Cli {
    #[arg(long, action = ArgAction::SetTrue)]
    verbose: bool,
}

Use instead

use clap::Parser;

#[derive(Parser)]
struct Cli {
    #[arg(long)]
    verbose: bool,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a value_parser = value_parser!(T) setting on a Clap-derived field when T is the field type with any Option and Vec wrappers removed.

Why is this bad?

Clap's derive already calls value_parser!(T) for the field type when the field has no parser setting. The setting does not change parsing, and it can go stale when the field type changes.

Known problems

The lint compares source text. It does not report value_parser!(u16) on a field whose type is an alias of u16, or a parser reached through a constant or a re-export. The lint skips fields with the value_enum marker.

Example

use clap::Parser;

#[derive(Parser)]
struct Cli {
    #[arg(long, value_parser = clap::value_parser!(u16))]
    port: u16,
}

Use instead

Remove the setting. Keep value_parser when it adds a constraint, such as value_parser!(u16).range(1..), or uses a custom parse function.

use clap::Parser;

#[derive(Parser)]
struct Cli {
    #[arg(long)]
    port: u16,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a Clap-derived field of type Vec<Vec<T>> or Option<Vec<Vec<T>>> that does not set num_args.

Why is this bad?

Clap groups the values of a Vec<Vec<T>> field by occurrence of the argument. Each occurrence takes the number of values set by num_args. Without num_args, each occurrence takes one value, so every inner Vec holds a single value and the grouping does nothing. Clap's derive reference says this type needs num_args to be meaningful. Clap supports these field types only with its unstable-v5 feature.

Known problems

The lint matches the type names Vec and Option as written. Like Clap, it does not treat a qualified path such as std::vec::Vec or a type alias as a Vec.

Example

use clap::Parser;

#[derive(Parser)]
struct Cli {
    #[arg(long)]
    define: Vec<Vec<String>>,
}

Use instead

use clap::Parser;

#[derive(Parser)]
struct Cli {
    #[arg(long, num_args = 2)]
    define: Vec<Vec<String>>,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for verbatim_doc_comment in a #[command(...)] or #[arg(...)] attribute on a Clap-derived type, variant, or field that has no doc comment.

Why is this bad?

verbatim_doc_comment changes only how Clap turns a doc comment into about or help text. With no doc comment, the setting does nothing. It can also hide a help message that code deleted by mistake.

Known problems

Any doc attribute counts as a doc comment, including #[doc = ""] and #[doc = include_str!(...)]. The lint does not check that the text is nonempty.

Example

use clap::Parser;

#[derive(Parser)]
struct Cli {
    #[arg(long, verbatim_doc_comment)]
    output: String,
}

Use instead

Add the doc comment so Clap preserves its formatting, or remove verbatim_doc_comment.

use clap::Parser;

#[derive(Parser)]
struct Cli {
    /// Output path.
    ///
    /// Use `-` for standard output.
    #[arg(long, verbatim_doc_comment)]
    output: String,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Command method chain that calls external_subcommand_value_parser but never calls allow_external_subcommands(true).

Why is this bad?

Clap uses the external subcommand value parser only for the arguments of an external subcommand. Without allow_external_subcommands(true), clap rejects unknown subcommands, so the parser never runs. The code suggests support for external subcommands that the command does not have.

Known problems

The lint checks only one method chain. It warns when code calls allow_external_subcommands(true) on the same Command in a later statement. It also warns when allow_external_subcommands gets a value other than the literal true, even if that value is true at runtime.

Example

use std::ffi::OsString;

use clap::{value_parser, Command};

fn cli() -> Command {
    Command::new("app").external_subcommand_value_parser(value_parser!(OsString))
}

Use instead

Enable external subcommands, or remove the parser if the command does not need them.

use std::ffi::OsString;

use clap::{value_parser, Command};

fn cli() -> Command {
    Command::new("app")
        .allow_external_subcommands(true)
        .external_subcommand_value_parser(value_parser!(OsString))
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Arg method chain that calls index on an argument that also has a long or short name.

Why is this bad?

index sets the position of a positional argument. An argument with a long or short name is an option, and options have no position. Clap panics in debug builds when code builds the command.

Known problems

The lint checks only one method chain. It does not check an Arg changed in later statements. A final .index(None) removes the index. A long or short name counts when its final call passes any value other than None. A name passed as an Option value computed at runtime does not count.

Example

use clap::Arg;

fn input_arg() -> Arg {
    Arg::new("input").long("input").index(1)
}

Use instead

Remove index for an option, or remove long and short for a positional argument.

use clap::Arg;

fn input_arg() -> Arg {
    Arg::new("input").long("input")
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Arg method chain that calls last(true) on an argument that also has a long or short name.

Why is this bad?

last applies only to positional arguments. An argument with a long or short name is an option, so clap panics in debug builds when code builds the command.

Known problems

The lint checks only one method chain. It does not check an Arg changed in later statements. It does not check a last call whose argument is not the literal true. A long or short name counts when its final call passes any value other than None. A name passed as an Option value computed at runtime does not count.

Example

use clap::Arg;

fn input_arg() -> Arg {
    Arg::new("input").long("input").last(true)
}

Use instead

Remove last(true) for an option, or remove long and short for a positional argument.

use clap::Arg;

fn input_arg() -> Arg {
    Arg::new("input").last(true)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Command method chain whose final multicall and no_binary_name settings are both true.

Why is this bad?

Clap cannot combine multicall with no_binary_name because the two settings read the first command-line token in different ways. Clap panics in debug builds when code builds the command.

Known problems

The lint checks only one method chain. It does not check a Command changed in later statements. It does not check a setting whose argument is not a literal true or false.

Example

use clap::Command;

fn cli() -> Command {
    Command::new("busybox").multicall(true).no_binary_name(true)
}

Use instead

use clap::Command;

fn cli() -> Command {
    Command::new("busybox").multicall(true)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Arg method chain that calls require_equals(true) but never calls num_args.

Why is this bad?

require_equals makes users write the value after =, as in --color=always. Clap requires the argument to take values, and it panics in debug builds if the argument must take more than one value. Without num_args, the value count comes from the action, but this setting does not show it. If the action is a flag action such as ArgAction::SetTrue, clap panics in debug builds.

Known problems

The lint checks only one method chain. It warns when a later statement calls num_args on the same Arg. It does not warn when the final action call is ArgAction::Set or ArgAction::Append, because that action already makes the argument take one value. It checks only the final require_equals call, and only when its argument is the literal true.

Example

use clap::Arg;

fn color_arg() -> Arg {
    Arg::new("color").long("color").require_equals(true)
}

Use instead

use clap::Arg;

fn color_arg() -> Arg {
    Arg::new("color")
        .long("color")
        .require_equals(true)
        .num_args(1)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Arg method chain whose final required setting is true and that also calls a conditional requirement method: required_if_eq, required_if_eq_any, required_if_eq_all, required_unless_present, required_unless_present_any, or required_unless_present_all.

Why is this bad?

An argument cannot require unconditional presence and conditional presence at the same time. Clap panics in debug builds when code builds the command.

Known problems

The lint checks only one method chain. It does not check an Arg changed in later statements. required_if_eq and required_unless_present count with any arguments. The _any and _all forms count only when their argument is a nonempty array literal, so the lint misses conditions passed through variables.

Example

use clap::Arg;

fn config_arg() -> Arg {
    Arg::new("config")
        .long("config")
        .required(true)
        .required_if_eq("mode", "custom")
}

Use instead

use clap::Arg;

fn config_arg() -> Arg {
    Arg::new("config")
        .long("config")
        .required_if_eq("mode", "custom")
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clap::Arg method chain that calls trailing_var_arg(true) but never calls num_args.

Why is this bad?

trailing_var_arg makes the last positional argument capture the rest of the command line, so the argument must accept multiple values. Without num_args, the argument takes one value by default, and clap panics in debug builds when code builds the command.

Known problems

The lint checks only one method chain. It warns when a later statement calls num_args on the same Arg. It does not warn when the final action call is ArgAction::Append, because that action already lets the argument take multiple values. It checks only the final trailing_var_arg call, and only when its argument is the literal true.

Example

use clap::Arg;

fn command_args() -> Arg {
    Arg::new("args").trailing_var_arg(true)
}

Use instead

use clap::Arg;

fn command_args() -> Arg {
    Arg::new("args").trailing_var_arg(true).num_args(1..)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for exported functions and methods whose final expression is an Iterator::collect call and whose return type is Vec, HashMap, HashSet, BTreeMap, BTreeSet, or Box<[T]>, including through type aliases.

Why is this bad?

The function allocates and fills a collection on every call, and the signature makes that allocation part of the public API. A caller that only iterates, takes the first match, or collects into another type still pays for the collection. Returning impl Iterator lets each caller decide whether to collect.

Known problems

  • The lint cannot tell when the caller needs an owned collection, for example for indexing, len, or several passes. Keep the collection in those cases and allow the lint.
  • An iterator that borrows an argument ties the return value to that borrow, which some callers cannot accept.
  • Only a direct final .collect() triggers. The lint ignores Ok(iter.collect()), a collected local returned later, and VecDeque return types.
  • The lint ignores functions outside the crate's exported API. This includes pub functions in private modules and methods of private types.
  • The lint ignores trait impl methods because the trait fixes their return type. It also ignores functions generated by macros.

Example

pub fn even_ids(ids: &[u64]) -> Vec<u64> {
    ids.iter().copied().filter(|id| id % 2 == 0).collect()
}

Use instead

pub fn even_ids(ids: &[u64]) -> impl Iterator<Item = u64> + '_ {
    ids.iter().copied().filter(|id| id % 2 == 0)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks function return types, including the output of an async fn, for a two-element tuple of a collection and a bool, in either order. The tuple can also be inside Result or Option. The collections are arrays and the standard Vec, VecDeque, HashMap, HashSet, BTreeMap, and BTreeSet types.

Why is this bad?

The type does not say what the bool means or which combinations can occur. A caller must guess whether (vec![], true) is valid. An enum names each outcome and makes invalid combinations impossible to return.

Known problems

Some flags are independent of the collection, such as a truncated marker next to a list of lines. The lint warns in that case too.

The lint misses tuples with more than two elements, structs with a collection field and a bool field, references such as (&[T], bool), and collections from other crates.

Example

struct Action;

fn pending_actions() -> Option<(Vec<Action>, bool)> {
    None
}

Use instead

struct Action;

enum PendingActions {
    Ready(Vec<Action>),
    Waiting(Vec<Action>),
}

fn pending_actions() -> Option<PendingActions> {
    None
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for bool expressions with too much inline work in if and while conditions and match guards.

Scoring

The lint splits the condition at top-level && and || and scores each term. A method or function call adds 2 plus its receiver or callee chain and boolean argument scores. An index in a receiver or comparison operand adds 2 plus its receiver chain score. A comparison adds 1 plus the scores of its operands. A field access adds nothing on a variable or on a field of a variable, such as self.config.is_enabled. On any other value, such as order.customer().address, it adds 1.

A !, cast, or reference adds 1. An if or match adds 4, a block 2, and an if let pattern 2. A match inside a call chain or comparison operand adds 3. A ? or .await adds nothing, and the lint scores the expression before it as part of the chain. A named bool variable scores 0.

Each inline closure argument adds its work minus 2. Work counts 2 per call, 4 per branch, match, loop, or nested closure, and 1 per statement, binary operator, or assignment, up to 8. A closure with one call or one comparison adds nothing, and one closure adds at most 6.

A term with a score of 6 or more is complex. The lint warns when two terms are complex, or when one term is complex and the total score reaches 9. A condition without && or || warns when its score reaches 9.

Why is this bad?

A dense condition puts data access, calls, and the branch decision in one expression. The reader must evaluate all of it to learn what the branch tests. Named bool bindings state each part of the decision and give each part a place for a comment.

Known problems

  • The score is a heuristic. It can warn on a fluent API call chain that reads well, and it misses work hidden inside helpers.
  • Non-bool call arguments other than inline closures add nothing to the score.
  • The lint ignores a condition that contains a macro call outside a closure body. It also skips code that cfg removes.
  • The lint skips let initializers and assignments because the binding already names the result. It also skips const and static initializers and return expressions.
  • Moving every term into a binding before the if evaluates all of them. That can change short-circuit behavior, side effects, borrows, .await points, or ? propagation. In a while condition, a binding computed once is not re-evaluated on each iteration. Keep dependent terms inside the branch, or move them into a named helper. The lint emits help without an automatic fix.

Example

fn accepts(values: &[i32], expected: usize) -> bool {
    if values.iter().filter(|value| **value > 0).count() == expected
        && values.iter().all(|value| value.abs() < 100 && *value % 2 == 0)
    {
        return true;
    }
    false
}

Use instead

fn accepts(values: &[i32], expected: usize) -> bool {
    let has_expected_count = values.iter().filter(|value| **value > 0).count() == expected;
    let all_values_fit = || {
        values.iter().all(|value| value.abs() < 100 && *value % 2 == 0)
    };
    has_expected_count && all_values_fit()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for two adjacent for loops that iterate over local bindings or a direct borrow of one. The loops must bind patterns of the same shape and item type and have structurally equal bodies. A use of the first loop's binding must match a use of the second loop's binding at the same pattern position.

Why is this bad?

The code repeats the same body. A later change to one copy can miss the other, and the two loops then do different work by accident. Iterator::chain states one ordered sequence and keeps one copy of the body.

Known problems

  • Sources other than a local binding, &name, or &mut name never trigger. Field accesses such as self.items and calls such as list.iter() are ignored.
  • The body comparison supports common expression and statement forms. A body that uses another form, such as a labeled block, a cast, a struct literal, or a while loop. The lint ignores that body.
  • The lint ignores a body that contains break, because after chaining the break would also skip the second sequence.
  • chain converts the second source into an iterator before the first loop runs. The lint only emits help and does not offer an automatic fix.

Example

fn report(first: &[i32], second: &[i32]) {
    for value in first {
        consume(value);
    }
    for value in second {
        consume(value);
    }
}

Use instead

fn report(first: &[i32], second: &[i32]) {
    for value in first.iter().chain(second) {
        consume(value);
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for map.contains_key("literal") || [..].iter().any(|key| map.contains_key(*key)), where the array holds only string literals and both calls use the same local receiver and the same resolved contains_key method. The lint ignores a differently named method such as has_key.

Why is this bad?

The code checks the first key in a separate call that repeats the receiver and the method. A reader must compare both halves to see that they ask the same question. Adding the first key to the array keeps all keys in one list.

Known problems

  • The separate call must be the direct left operand of ||. In a || b || [..].iter().any(..), the left operand is a || b, so the lint ignores the expression.
  • The receiver must be a local binding. The lint ignores field receivers such as self.map.contains_key(..).
  • The lint ignores computed keys, closures with extra conditions, and sources other than an inline array with .iter().any(..).
  • The machine-applicable fix moves the separate key to the front of the array, so the keys keep their search order. A disjunction produced by a macro gets help without a fix.

Example

use std::collections::HashMap;

fn has_identity(document: &HashMap<String, String>) -> bool {
    document.contains_key("address")
        || ["full_name", "date_of_birth"]
            .iter()
            .any(|key| document.contains_key(*key))
}

Use instead

use std::collections::HashMap;

fn has_identity(document: &HashMap<String, String>) -> bool {
    ["address", "full_name", "date_of_birth"]
        .iter()
        .any(|key| document.contains_key(*key))
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for named fields whose type is String or &str and whose name marks a country code. The names are country, country_code, iso_country, iso_country_code, residence_country, nationality_country, and any name ending in _country or _country_code, compared without case.

Why is this bad?

A string field accepts any text, so invalid or inconsistently cased codes such as "usa" or "Us" move through the program unchecked. A country-code type validates the value once, at the boundary.

Known problems

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. After peeling, it checks only String and &str, including type aliases and use renames. It does not inspect Vec<String>, Box<str>, or Cow<'_, str>.

It can warn on a field such as home_country that holds a display name instead of a code. It does not flag other names, such as country_name or nation.

Example

struct Profile<'a> {
    country: String,
    residence_country: &'a str,
}

Use instead

struct CountryCode(String);

struct Profile {
    country: CountryCode,
    residence_country: CountryCode,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Warns when measured function complexity and executable-line coverage produce a CRAP score above the configured threshold. CRAP means Change Risk Anti-Patterns: it is a review heuristic that highlights complex functions with little measured coverage.

Why is this bad?

Uncovered decisions need tests and can increase change risk. The score ranks candidates for review; it does not predict defects. Coverage records execution, not whether a test checks the right result.

Known problems

Coverage must come from a representative run matching the analyzed source revision. Without configured coverage, this lint is inactive.

Example

A function with five independent decisions has complexity 6. With 0% measured coverage, its score is 42, which exceeds the default threshold of 30:

fn decisions(values: [bool; 5]) -> usize {
    let mut count = 0;
    if values[0] { count += 1; }
    if values[1] { count += 1; }
    if values[2] { count += 1; }
    if values[3] { count += 1; }
    if values[4] { count += 1; }
    count
}

The UI test supplies a matching LCOV record with no covered lines and asserts this diagnostic.

Use instead

Reduce the decision structure and test the behavior. The example below has no explicit decisions, so its complexity is 1 and even 0% measured coverage scores 2:

fn count_enabled(values: [bool; 5]) -> usize {
    values.iter().filter(|enabled| **enabled).count()
}

The UI test includes this function with zero covered lines and asserts that it stays below the threshold. Review assertions as well as coverage.

Coverage profile and configuration

The formula is complexity² × (1 − coverage_fraction)³ + complexity. Complexity is a dimensionless decision count from the same HIR profile as cyclomatic_complexity. Coverage fraction is covered executable lines divided by measured executable lines. Multiplying this fraction by 100 converts it to the diagnostic's percentage. The default threshold is 30; equality passes.

The original Java metric used basis-path coverage. This Rust profile uses executable-line coverage and is not numerically interchangeable with that original profile. Line hits prove execution; they do not prove assertion quality.

Configure [crap-score] in dylint.toml: optional coverage_path is a local LCOV path; threshold is a finite nonnegative number. Relative report paths and SF paths resolve from the compiler working directory. Unknown configuration keys fail. Omitted coverage_path disables scoring.

An unreadable report or malformed DA record fails compilation. An unresolvable SF path fails compilation. An analyzed file absent from the report or a function with no measured executable lines receives no score. Threshold validation also runs when coverage_path is omitted.

The parser accepts LLVM 22.1.8 SF:<path> and DA:<positive-line>,<unsigned-hits>[,<checksum>] records. It ignores checksums and unrelated record kinds, including function and branch counters. end_of_record clears the active source. A DA record with extra fields fails.

Canonical filesystem paths merge source aliases; duplicate line records retain the maximum hit count. The callable's body start and end lines define an inclusive range in one file. Enclosing body ranges include nested closure lines. Functions sharing one source line share that line’s hit count.

Closures also have their own HIR body; macro-generated callables are excluded. Reports must match the analyzed source revision and relevant test scope; this lint cannot authenticate that match.

Sources: original formula and cautions, original threshold guidance, and LLVM 22.1.8 LCOV exporter.

A score of 1260

The observed function had cyclomatic complexity 35 and 0% coverage in the selected coverage run:

CRAP = 35^2 * (1 - 0/100)^3 + 35
     = 1225 + 35
     = 1260

Cyclomatic complexity 35 means the analyzer found a large decision structure. Under the original definition, cyclomatic complexity is one plus the method's unique decisions. Exact counting differs between language analyzers. Compare the Rust value with other values from the same cargo-crap version.

The result is extreme because the formula squares uncovered complexity. 1260 is numerically 42 times the threshold. It does not mean 42 times as many defects, maintenance effort, or risk. The scale is nonlinear. The authors proposed it as an experimental ranking heuristic.

For the same complexity, coverage changes the score as follows:

CoverageCRAP
0%1260.000
50%188.125
75%54.141
90%36.225
100%35.000

At 100% coverage, CRAP equals cyclomatic complexity. A function with complexity 35 therefore cannot reach 30 through coverage alone. It must reduce complexity to pass the absolute threshold.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for local structs, enums, and type aliases named after a well-known semantic type. The names are StatusCode, HttpStatusCode, Method, HttpMethod, Url, URL, Uri, URI, Uuid, UUID, Email, EmailAddress, Path, PathBuf, Duration, Instant, Timestamp, and DateTime.

Why is this bad?

A local Url or Duration looks like the established type but parses, validates, and converts differently. Readers assume the familiar behavior, and code that crosses crate boundaries needs adapters between two types for one concept.

Known problems

The lint matches exact names, so PaymentMethod or RouteMethod do not warn. It warns on every declaration with a listed name, including a compatibility type that mirrors an external API on purpose.

A type alias does not warn when it resolves to the established type, such as type Duration = std::time::Duration or type Uri = http::Uri. An alias named Email, EmailAddress, or Timestamp always warns, because the lint knows no established type for those names.

Example

struct StatusCode(u16);

enum HttpMethod {
    Get,
    Post,
}

type Url = String;

Use instead

use http::{Method, StatusCode};
use url::Url;
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the Cyclomatic Complexity of each function, method, and closure, and warns when the score is 10 or more.

A callable starts at 1. Each if, while, for, &&, ||, ?, match guard, and match arm after the first adds 1. A plain loop and .await add nothing. A closure gets its own score and does not add to the enclosing function. The limit of 9 follows the NIST structured testing guide and PMD's Cyclomatic Complexity rule.

Why is this bad?

Each decision adds an independent path that a reviewer must follow and a test must cover. A function with many paths can reach high line coverage while an untested path still hides a bug. A change to one branch can also affect paths that look unrelated in the source.

Known problems

The score counts decisions without judging them. Ten flat validation checks can be easier to read than three interleaved state changes, and both can score the same. Paths that can never run together still count.

The lint skips code a macro generates but counts expressions written as macro arguments, so assert!(a && b) adds 1 for the &&. Decisions inside called functions count only toward those functions. The lint does not check a function a macro generates.

Example

fn score(a: bool, b: bool, c: bool, d: bool, e: bool, f: bool) -> usize {
    let mut score = 0;
    if a {
        score += 1;
    }
    if b {
        score += 1;
    }
    if c {
        score += 1;
    }
    if d {
        score += 1;
    }
    if e {
        score += 1;
    }
    if f {
        score += 1;
    }
    if a && b && c && d && e {
        score += 1;
    }
    score
}

The seven if expressions and four && operators give a score of 12.

Use instead

Move the independent checks into a separate function or replace them with data.

fn count_set(flags: [bool; 6]) -> usize {
    flags.into_iter().filter(|&flag| flag).count()
}

fn score(a: bool, b: bool, c: bool, d: bool, e: bool, f: bool) -> usize {
    let mut score = count_set([a, b, c, d, e, f]);
    if a && b && c && d && e {
        score += 1;
    }
    score
}

Interpretation and sources

Cyclomatic Complexity measures the number of linearly independent paths through a control-flow graph. McCabe defined it as v(G) = E - N + 2P, where E is the edge count, N is the node count, and P is the number of connected components. For one structured function, tools commonly calculate an equivalent form of one plus the number of decisions.

It estimates the minimum basis-path testing burden. It does not count every possible execution path, and it does not distinguish a flat decision table from deep nesting. Counting details for match, guards, ?, boolean operators, closures, macros, and generated code differ between analyzers. Pin the analyzer version and test its Rust grammar before making the value a gate.

McCabe described 10 as a reasonable starting bound. This lint's documented limit is local policy. McCabe's 1976 paper.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for named fields whose type is String or &str and whose name marks a date. The names are birthdate, names containing the word dob, birth_date, or date_of_birth, and any name ending in _date, compared without case.

Why is this bad?

A string field accepts any text, so a value such as "2024-02-30" or "03/04/2024" moves through the program unchecked. Each reader must parse it again and may read the parts in a different order. A date type parses once, at the boundary, and compares dates correctly.

Known problems

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. After peeling, it checks only String and &str, including type aliases and use renames. It does not inspect Vec<String>, Box<str>, or Cow<'_, str>.

It can warn on a field that holds a formatted date for display. It does not flag other date names, such as created_on or date_label.

Example

struct Profile<'a> {
    date_of_birth: String,
    renewal_date: &'a str,
}

Use instead

use chrono::NaiveDate;

struct Profile {
    date_of_birth: NaiveDate,
    renewal_date: NaiveDate,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for named fields whose type is a primitive integer and whose name contains a timestamp word. The words are timestamp, timestamps, epoch, ts, date, and datetime, matched between underscores and with case. The word unix counts only after at, as in created_at_unix, or before time, secs, seconds, ms, millis, or nanos, so unix_mode does not warn.

Why is this bad?

An integer timestamp does not record its epoch, unit, or time zone. Code that reads seconds where another part wrote milliseconds still compiles. A datetime type fixes the unit and offers correct comparison and formatting.

Known problems

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. After peeling, it checks only primitive integer types, including type aliases and use renames; integer newtypes remain opaque.

It can warn where a wire format or database column requires an integer epoch. It does not flag other timestamp names, such as created_at or modified.

Example

struct Session {
    created_at_ts: i64,
    expires_epoch: u64,
}

Use instead

use chrono::{DateTime, Utc};

struct Session {
    created_at: DateTime<Utc>,
    expires_at: DateTime<Utc>,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the nearest Cargo.toml above the crate root file for dependency versions that omit the patch part, such as "1" or "0.1". It checks [dependencies], [dev-dependencies], [build-dependencies], the same tables under [target.'...'], and [workspace.dependencies]. The warning points at the version string and suggests the full version.

Why is this bad?

Cargo reads "1" as ^1.0.0, so it accepts every 1.x release, including releases older than the code needs. Reviewers cannot see which minimum version the crate uses in tests.

Known problems

The lint only flags plain numeric versions with one or two parts. Versions with operators, wildcards, or pre-release tags, such as ">=1" or "1.*". The lint skips those versions.

The lint skips a virtual workspace manifest because Cargo compiles no crate from it.

The lint marks the suggestion as possibly incorrect, so cargo fix does not apply it.

Example

[dependencies]
anyhow = { version = "1.2" }
serde = "1"

Use instead

[dependencies]
anyhow = { version = "1.2.0" }
serde = "1.0.0"
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the nearest Cargo.toml above the crate root file for dependency entries that do not follow alphabetical order. It checks [dependencies], [dev-dependencies], [build-dependencies], [workspace.dependencies], and their [target.'...'] variants. The warning points at the out-of-order key.

Why is this bad?

In an unsorted table, a reader must scan every line to find a dependency, and duplicate or misplaced entries are easy to miss during updates. Sorted tables also give new entries one correct position, which reduces merge conflicts.

Known problems

The lint compares only adjacent entries and ignores ASCII case. Blank lines and comment lines start a new block, which the lint compares on its own. This lets you keep sorted groups, but a stray blank line also hides an ordering mistake across it.

The lint does not order dependency subtables such as [dependencies.serde].

Example

[dependencies]
serde = "1.0.0"
anyhow = "1.0.0"

Use instead

[dependencies]
anyhow = "1.0.0"
serde = "1.0.0"
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks #[doc = "..."] and #![doc = "..."] attributes whose value is a one-line string literal, and suggests the equivalent /// or //! comment.

Why is this bad?

Attribute syntax adds quotes, brackets, and escapes around plain prose, which makes documentation harder to read and edit. Line doc comments are the usual form, and settings such as #[doc(hidden)] stay visible as attributes.

Known problems

The lint skips values that start or end with whitespace, so The lint does not report #[doc = " Text"], the form that /// Text expands to. It also skips multi-line strings, values built by macros such as concat!, attributes inside cfg_attr, and attributes generated by macros. When code follows the attribute on the same line, the lint gives help without a fix, because a line comment would absorb that code.

Example

#[doc = "Parses a port number."]
pub fn parse_port(raw: &str) -> Option<u16> {
    raw.parse().ok()
}

Use instead

/// Parses a port number.
pub fn parse_port(raw: &str) -> Option<u16> {
    raw.parse().ok()
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for named fields whose type is a primitive integer and whose name contains a time-unit word. The words cover nanoseconds through weeks, such as ns, ms, millis, sec, seconds, mins, hr, hours, days, and weeks, matched between underscores and with case. The singular words second, minute, hour, day, and week, which usually name a calendar component, and min, which usually means minimum, do not count.

Why is this bad?

The unit lives only in the field name, so code can pass milliseconds where callers expect seconds and still compile. std::time::Duration stores one value and converts units explicitly.

Known problems

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. After peeling, it checks only primitive integer types, including type aliases and use renames; integer newtypes such as struct BusinessDays(u16) remain opaque.

It warns on a raw integer count that uses a unit word but is not an elapsed time, such as business_days: u16. It does not flag duration names without a unit word, such as timeout or ttl.

Example

struct RetryConfig {
    retry_delay_seconds: u64,
    timeout_ms: usize,
}

Use instead

use std::time::Duration;

struct RetryConfig {
    retry_delay: Duration,
    timeout: Duration,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks inherent methods named as_* for work in their body or in closures inside it. It reports:

  • .clone() on a value that is neither Copy nor an Rc or Arc;
  • .to_string() and .to_owned() on a non-Copy value;
  • format!, str::parse, a From or Into conversion that produces a String or Vec, a function or method named decode, and the ? operator.

The lint matches calls by their resolved definitions.

Why is this bad?

Rust API naming conventions treat as_* methods as cheap borrowed views. A caller who sees as_string() expects no allocation or failure, so hidden clones and parses end up in loops and hot paths. A to_*, try_*, or parse_* name shows the cost at the call site.

Known problems

The lint misses work done through helper functions with other names and other expensive operations, such as collecting an iterator. A function named decode counts as work even when it is cheap.

The lint skips trait methods, trait impl methods, and methods generated by macros.

Example

struct Token {
    raw: String,
}

impl Token {
    fn as_string(&self) -> String {
        self.raw.clone()
    }

    fn as_number(&self) -> Result<u64, std::num::ParseIntError> {
        self.raw.parse()
    }
}

Use instead

struct Token {
    raw: String,
}

impl Token {
    fn to_raw_string(&self) -> String {
        self.raw.clone()
    }

    fn parse_number(&self) -> Result<u64, std::num::ParseIntError> {
        self.raw.parse()
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the standard stable slice sort_by_key method for closures that call recognized methods that return an owned String: Unicode and ASCII case conversion plus owned String cloning. Calls must resolve to standard-library methods, and the key closure must only project a field from its argument before applying the recognized transformation.

Why is this bad?

sort_by_key may call its key closure repeatedly while comparing elements. When the key operation allocates, repeated comparisons repeat allocation and conversion work. sort_by_cached_key evaluates the key at most once per element and preserves stable ordering.

Known problems

The lint skips arrays with a statically known length of at most two. For two elements, sorting evaluates each key once, so caching does not reduce key calls. It does not inspect runtime Vec or slice lengths, or unresolved const-generic array lengths, so it can still diagnose empty or small inputs when their length is unknown during linting. Case-conversion calls through user-defined Deref<Target = str> receivers are skipped; only actual str and standard String receivers qualify. The lint also skips arbitrary helpers, custom methods, closure blocks with additional statements, and sort_unstable_by_key. It does not report side-effecting or non-deterministic callbacks.

Caching changes callback count and order, so no automatic fix is offered. The cached method uses temporary storage, and the lint does not measure whether a particular collection is large enough to benefit.

Example

fn sort_names(names: &mut [String]) {
    names.sort_by_key(|name| name.to_lowercase());
}

Use instead

fn sort_names(names: &mut [String]) {
    names.sort_by_cached_key(|name| name.to_lowercase());
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks whether the inherent methods of a local type split into separate groups that use separate fields. It warns when there are at least two such groups.

Two methods join the same group when they use a common self.field or one calls the other through self. The lint counts only methods with a self receiver that use a field or call another counted method, across all inherent impl blocks. It checks a type only when it has at least 6 such methods and 4 used fields. It warns when at least two groups each have 2 or more methods and 2 or more fields.

Why is this bad?

Methods that never share state usually implement separate responsibilities. Keeping them in one type makes each half harder to test alone, and every user of one half also depends on the other.

Known problems

Facades, adapters, and plain records can hold separate groups on purpose. The lint misses connections it cannot see: field uses generated by a macro body, destructuring with let Self { a, b } = self, calls through helper functions, async methods, and trait methods. Field uses in closures and in macro arguments such as format!("{}", self.name) count toward the enclosing method. The lint reports separation in the method graph, not an LCOM score, because published LCOM variants disagree with each other (Al Dallal, 2020).

Example

struct SplitState {
    left_a: usize,
    left_b: usize,
    right_a: usize,
    right_b: usize,
}

impl SplitState {
    fn read_left(&self) -> usize {
        self.left_a + self.left_b
    }
    fn write_left(&mut self, value: usize) {
        self.left_a = value;
        self.left_b = value;
    }
    fn reset_left(&mut self) {
        self.left_a = 0;
        self.left_b = 0;
    }
    fn read_right(&self) -> usize {
        self.right_a + self.right_b
    }
    fn write_right(&mut self, value: usize) {
        self.right_a = value;
        self.right_b = value;
    }
    fn reset_right(&mut self) {
        self.right_a = 0;
        self.right_b = 0;
    }
}

The left_* methods and the right_* methods never share a field.

Use instead

Extract each group into its own type when it has its own invariant.

struct Pair {
    a: usize,
    b: usize,
}

impl Pair {
    fn read(&self) -> usize {
        self.a + self.b
    }
    fn write(&mut self, value: usize) {
        self.a = value;
        self.b = value;
    }
    fn reset(&mut self) {
        self.write(0);
    }
}

struct SplitState {
    left: Pair,
    right: Pair,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for string literals that hold a machine-specific filesystem path. It recognizes Unix absolute paths under host root directories. Examples include /home/alice/cache and /Users/alice, home paths such as ~/cache or ~alice/cache, Windows drive paths such as C:\Users, and UNC paths such as \\server\share.

Why is this bad?

The code hardcodes a location on one machine. Builds, tests, and tools that use it fail on other machines, in CI, and in containers.

Known problems

The lint reads only the literal's text, not how code uses the value. A Unix path warns only when its first component is a standard top-level directory: bin, boot, dev, etc, home, lib, lib64, media, mnt, nix, opt, private, proc, root, run, sbin, snap, srv, sys, tmp, usr, var, Applications, Library, System, Users, or Volumes. It therefore skips URL routes such as "/api/users" and "/users/:id" and comment text such as "// note", but it also skips a real path under another root, such as /data/cache. It warns on fixed system paths such as /dev/null.

It does not flag relative paths such as assets/icon.png or ../src/lib.rs, URLs such as https://example.com/assets, byte strings, or paths built at runtime.

Example

fn cache_dir() -> &'static str {
    "/home/alice/.cache/example"
}

Use instead

fn cache_dir() -> &'static str {
    "target/example-cache"
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks fields, function parameters, and function return types that use String or &str when the field or parameter has the name http_method or request_method. The lint checks a return type when the function has one of those names. It also checks the generic names method and verb when the crate depends on an HTTP library: actix_web, axum, http, hyper, isahc, poem, reqwest, rocket, surf, tide, ureq, or warp.

Why is this bad?

A string accepts any text, so a typo such as "PSOT" or a lowercase "get" reaches the HTTP client before it fails. http::Method parses the verb once and compares it without string matching.

Known problems

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. After peeling, it checks only String and &str, including type aliases and use renames. It does not inspect Box<str> or closure parameters.

It matches only exact names, so it does not flag http_verb or method_str. In a crate with an HTTP library, it still warns on a method that names something else, such as a builder method. It skips destructured parameters, functions with other names that return a method string, and methods of trait impls, whose signature comes from the trait.

Example

struct Route<'a> {
    method: String,
    http_method: &'a str,
}

fn request_method(verb: &'static str) -> &'static str {
    verb
}

Use instead

use http::Method;

struct Route {
    method: Method,
    http_method: Method,
}

fn request_method(verb: Method) -> Method {
    verb
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Counts the inherent methods of each local struct, enum, or union across all of its impl blocks, and warns when there are more than 20. Associated functions without self count as methods. Methods in trait implementations do not count. The limit of 20 follows the Rust nom = 20 override in the big-code-analysis threshold guide.

Why is this bad?

Each inherent method is part of the type's interface. A type with many methods usually owns several responsibilities. A change to one method can break callers of another. Readers must scan a long list to find the method they need.

Known problems

A cohesive domain type or a deliberate facade can need more than 20 methods. Constructors and other associated functions count the same as methods. Methods generated by a macro do not count. The lint does not check impl blocks for trait objects or other types that are not a local struct, enum, or union.

Example

struct LargeType;

impl LargeType {
    fn m01(&self) {} fn m02(&self) {} fn m03(&self) {} fn m04(&self) {}
    fn m05(&self) {} fn m06(&self) {} fn m07(&self) {} fn m08(&self) {}
    fn m09(&self) {} fn m10(&self) {} fn m11(&self) {}
}

impl LargeType {
    fn m12(&self) {} fn m13(&self) {} fn m14(&self) {} fn m15(&self) {}
    fn m16(&self) {} fn m17(&self) {} fn m18(&self) {} fn m19(&self) {}
    fn m20(&self) {} fn m21(&self) {}
}

The two impl blocks give LargeType 21 inherent methods.

Use instead

Move a group of methods with its own state and invariants to a separate type.

struct Reader;

impl Reader {
    fn m01(&self) {} fn m02(&self) {} fn m03(&self) {} fn m04(&self) {}
    fn m05(&self) {} fn m06(&self) {} fn m07(&self) {} fn m08(&self) {}
    fn m09(&self) {} fn m10(&self) {} fn m11(&self) {}
}

struct Writer;

impl Writer {
    fn m12(&self) {} fn m13(&self) {} fn m14(&self) {} fn m15(&self) {}
    fn m16(&self) {} fn m17(&self) {} fn m18(&self) {} fn m19(&self) {}
    fn m20(&self) {} fn m21(&self) {}
}

struct LargeType {
    reader: Reader,
    writer: Writer,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::set_allow_empty_glob(true) calls.

Why is this bad?

By default, insta::glob! fails the test when its pattern matches no files. That catches a misspelled pattern or a moved fixture directory. With the setting enabled, a glob that matches nothing runs no assertions and the test still passes.

Known problems

The lint warns even when an empty fixture set is valid, such as fixtures that exist only on some platforms. It does not check a call whose argument is not the literal true.

Example

use insta::Settings;

fn fixture_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_allow_empty_glob(true);
    settings
}

Use instead

Keep the default, so an empty glob fails the test.

use insta::Settings;

fn fixture_settings() -> Settings {
    Settings::clone_current()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::assert_binary_snapshot! calls whose name is a string literal with no ., such as "response".

Why is this bad?

Insta splits a binary snapshot name at the first . to get the file extension. A name with no . makes the macro panic when the test runs, so the macro never records or compares the snapshot.

Known problems

The lint follows string literals for at most eight steps through simple immutable local bindings, same-crate constants, and same-crate inherent associated constants. Trait-associated constants remain unknown, even with literal defaults, because an implementation may override the default. Mutable, destructured, or uninitialized bindings; external constants, statics, and associated constants; calls; and other computed expressions remain unknown.

Example

fn snapshot_response(bytes: Vec<u8>) {
    insta::assert_binary_snapshot!("response", bytes);
}

Use instead

Add an extension to the name. A name that is only an extension, such as ".bin", uses the default snapshot name.

fn snapshot_response(bytes: Vec<u8>) {
    insta::assert_binary_snapshot!("response.bin", bytes);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::bind_to_scope calls inside an async function, async block, or async closure. It warns when the returned guard may remain live while the coroutine returns Poll::Pending at a suspension point.

Why is this bad?

bind_to_scope binds the settings to the current thread until Rust drops the returned guard. An async task can move to another thread at an .await, and other tasks can run on the same thread. Snapshots in the task can then miss the settings, and snapshots in other tasks can pick them up. Settings::bind_async binds the settings to one future instead.

Known problems

Calls inside plain closures nested in async code and calls inside synchronous functions called by async code are not analyzed.

Mutable collection tracking is conservative. After drop(guards.pop()), the lint can still warn at a later .await because MIR does not identify which element the collection removed.

The standard PhantomData<T> carries T only in type information and does not retain a guard. The lint recognizes PhantomData by the compiler-resolved standard item, not by its source spelling. Values stored in Option<T>, Box<T>, struct fields, or nested futures can retain the guard. Opaque helper calls can still produce false positives when a return type mentions the guard but MIR cannot prove that the value stores no guard. The lint follows aggregate field paths through a helper only when MIR proves that the helper returns its sole argument unchanged.

Example

use insta::Settings;

async fn fetch() -> String {
    String::from("body")
}

async fn snapshot_response() {
    let mut settings = Settings::clone_current();
    settings.set_snapshot_suffix("v2");
    let _guard = settings.bind_to_scope();
    insta::assert_snapshot!(fetch().await);
}

Use instead

use insta::Settings;

async fn fetch() -> String {
    String::from("body")
}

async fn snapshot_response() {
    let mut settings = Settings::clone_current();
    settings.set_snapshot_suffix("v2");
    settings
        .bind_async(async {
            insta::assert_snapshot!(fetch().await);
        })
        .await;
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::assert_compact_json_snapshot! calls.

Why is this bad?

Compact JSON puts a small value on one line, so a change to any field changes the whole line in the diff. When the value grows past the size limit for one line, the whole snapshot switches to multiple lines. Insta's documentation notes that this macro has worse diff behavior.

Known problems

The lint warns even when one-line JSON is the output under test.

Example

fn snapshot_ids(ids: Vec<u32>) {
    insta::assert_compact_json_snapshot!(ids);
}

Use instead

fn snapshot_ids(ids: Vec<u32>) {
    insta::assert_yaml_snapshot!(ids);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a match on a value of type insta::internals::Content, unless the matched value is the result of Content::resolve_inner().

Why is this bad?

Content can wrap a value in internal variants, such as the variants for Option and newtype structs. A match on the outer value misses a string or number stored inside such a wrapper. This often breaks dynamic redaction callbacks. The Content::as_* accessors and resolve_inner() remove the wrappers first.

Known problems

The lint warns even when the code means to inspect the outer wrapper.

Example

use insta::internals::Content;

fn is_text(content: &Content) -> bool {
    match content {
        Content::String(_) => true,
        _ => false,
    }
}

Use instead

Use an as_* accessor, or match on content.resolve_inner().

use insta::internals::Content;

fn is_text(content: &Content) -> bool {
    content.as_str().is_some()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::assert_display_snapshot! calls.

Why is this bad?

Insta deprecated assert_display_snapshot!. assert_snapshot! takes the same Display values and writes the same snapshots. A future major release can remove the deprecated macro, which breaks the test build.

Known problems

The lint offers a machine-applicable fix only for calls written as insta::assert_display_snapshot! or ::insta::assert_display_snapshot!. A call through an imported name gets help without a fix, because the import can fail to cover assert_snapshot!.

Example

fn snapshot_body(body: &str) {
    insta::assert_display_snapshot!(body);
}

Use instead

fn snapshot_body(body: &str) {
    insta::assert_snapshot!(body);
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::set_description calls whose argument is the empty string literal "".

Why is this bad?

Insta shows the description next to the snapshot during review. An empty description shows nothing, and it often means the author left out a value by mistake.

Known problems

The lint follows string literals for at most eight steps through simple immutable local bindings, same-crate constants, and same-crate inherent associated constants. Trait-associated constants remain unknown, even with literal defaults, because an implementation may override the default. Mutable, destructured, or uninitialized bindings; external constants, statics, and associated constants; calls; and other computed expressions remain unknown. The lint does not check descriptions that contain only whitespace.

Example

use insta::Settings;

fn expired_token_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_description("");
    settings
}

Use instead

Describe the case, or remove the call.

use insta::Settings;

fn expired_token_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_description("response when the token has expired");
    settings
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::add_filter calls whose pattern is the empty string literal "".

Why is this bad?

Insta applies each filter as a regular expression replacement on the snapshot text. An empty pattern matches at every position, so the replacement inserts text between every character and the snapshot no longer shows the value under test.

Known problems

The lint follows string literals for at most eight steps through simple immutable local bindings, same-crate constants, and same-crate inherent associated constants. Trait-associated constants remain unknown, even with literal defaults, because an implementation may override the default. Mutable, destructured, or uninitialized bindings; external constants, statics, and associated constants; calls; and other computed expressions remain unknown. The lint does not check other patterns that can match empty text, such as "a*".

Example

use insta::Settings;

fn id_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.add_filter("", "[id]");
    settings
}

Use instead

use insta::Settings;

fn id_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.add_filter(r"\b[[:xdigit:]]{32}\b", "[id]");
    settings
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::set_input_file calls whose argument is the empty string literal "".

Why is this bad?

Insta stores the input file path with the snapshot so reviewers can find the input that produced it. An empty path points to no file, and it often means the author left out a value by mistake.

Known problems

The lint follows string literals for at most eight steps through simple immutable local bindings, same-crate constants, and same-crate inherent associated constants. Trait-associated constants remain unknown, even with literal defaults, because an implementation may override the default. Mutable, destructured, or uninitialized bindings; external constants, statics, and associated constants; calls; and other computed expressions remain unknown.

Example

use insta::Settings;

fn user_fixture_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_input_file("");
    settings
}

Use instead

Name the input file, or remove the call.

use insta::Settings;

fn user_fixture_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_input_file("fixtures/user.json");
    settings
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::set_snapshot_path calls whose argument is the empty string literal "".

Why is this bad?

Insta resolves a relative snapshot path from the directory of the test file. An empty path makes Insta write snapshot files into the source directory itself instead of the default snapshots directory.

Known problems

The lint follows string literals for at most eight steps through simple immutable local bindings, same-crate constants, and same-crate inherent associated constants. Trait-associated constants remain unknown, even with literal defaults, because an implementation may override the default. Mutable, destructured, or uninitialized bindings; external constants, statics, and associated constants; calls; and other computed expressions remain unknown.

Example

use insta::Settings;

fn api_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_snapshot_path("");
    settings
}

Use instead

Name the snapshot directory, or remove the call to keep snapshots.

use insta::Settings;

fn api_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_snapshot_path("snapshots/api");
    settings
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::set_snapshot_suffix calls whose argument is the empty string literal "".

Why is this bad?

Insta appends @ and the suffix to each snapshot name. An empty suffix renames every snapshot to end in a bare @, such as name@.snap, without telling cases apart.

Known problems

The lint follows string literals for at most eight steps through simple immutable local bindings, same-crate constants, and same-crate inherent associated constants. Trait-associated constants remain unknown, even with literal defaults, because an implementation may override the default. Mutable, destructured, or uninitialized bindings; external constants, statics, and associated constants; calls; and other computed expressions remain unknown. The lint does not check suffixes that contain only whitespace.

Example

use insta::Settings;

fn compact_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_snapshot_suffix("");
    settings
}

Use instead

Name the case, or remove the call.

use insta::Settings;

fn compact_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_snapshot_suffix("compact");
    settings
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for two-argument insta::glob! calls whose pattern is a string literal equal to .. or starting with ../.

Why is this bad?

Insta does not support parent directory traversal in the two-argument form of glob!, so the glob does not find the intended files. The three-argument form takes a base directory, which can be a parent directory.

Known problems

The lint checks only a string literal passed directly as the pattern. It does not check patterns with .. after the first segment, such as fixtures/../other/*.txt.

Example

fn snapshot_fixtures() {
    insta::glob!("../fixtures/*.txt", |path| {
        insta::assert_snapshot!(std::fs::read_to_string(path).unwrap());
    });
}

Use instead

fn snapshot_fixtures() {
    insta::glob!("..", "fixtures/*.txt", |path| {
        insta::assert_snapshot!(std::fs::read_to_string(path).unwrap());
    });
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::assert_json_snapshot! calls.

Why is this bad?

JSON snapshots add braces, quotes, and commas that make diffs harder to read. Adding a field after the last one also changes the line before it, because that line gains a comma. Insta's serializer guide recommends YAML over JSON for most values.

Known problems

The lint warns even when the exact JSON output is what the test checks, such as the body of a JSON API.

Example

fn snapshot_ids(ids: Vec<u32>) {
    insta::assert_json_snapshot!(ids);
}

Use instead

fn snapshot_ids(ids: Vec<u32>) {
    insta::assert_yaml_snapshot!(ids);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::set_prepend_module_to_snapshot(false) calls.

Why is this bad?

By default, Insta names a snapshot file <module>__<name>.snap. Turning off the prefix changes the file name to <name>.snap. Tests with the same name in different modules of one directory then write to the same snapshot file.

Known problems

The lint warns even when a project keeps snapshot names unique by other means. It does not check a call whose argument is not the literal false.

Example

use insta::Settings;

fn short_name_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_prepend_module_to_snapshot(false);
    settings
}

Use instead

Keep the default module prefix.

use insta::Settings;

fn short_name_settings() -> Settings {
    Settings::clone_current()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::add_filter calls whose pattern and replacement are the same nonempty string literal with no regular expression special characters, such as add_filter("token", "token").

Why is this bad?

The filter replaces the matched text with the same text, so it does not change the snapshot. The test looks protected against unstable data that it still records.

Known problems

The lint follows string literals for at most eight steps through simple immutable local bindings, same-crate constants, and same-crate inherent associated constants. Trait-associated constants remain unknown, even with literal defaults, because an implementation may override the default. Mutable, destructured, or uninitialized bindings; external constants, statics, and associated constants; calls; and other computed expressions remain unknown. It skips patterns that contain any of .^$*+?()[]{}|\\, even when the filter still has no effect. The machine-applicable fix removes the call only when it is a whole statement. The fix leaves local declarations and other statements unchanged.

Example

use insta::Settings;

fn token_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.add_filter("token", "token");
    settings
}

Use instead

Replace the match with a stable value, or remove the filter.

use insta::Settings;

fn token_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.add_filter("token", "[token]");
    settings
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a Content::as_* accessor called directly on the result of Content::resolve_inner(), such as content.resolve_inner().as_str().

Why is this bad?

The Content::as_* accessors already remove Insta's internal wrappers. The extra resolve_inner() call does nothing and suggests that the accessors need it.

Known problems

The lint checks only a direct method chain. It does not check resolve_inner() stored in a variable and passed to an accessor later.

Example

use insta::internals::Content;

fn text(content: &Content) -> Option<&str> {
    content.resolve_inner().as_str()
}

Use instead

use insta::internals::Content;

fn text(content: &Content) -> Option<&str> {
    content.as_str()
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::bind calls whose callable returns a Future, including futures returned by function calls or stored closures.

Why is this bad?

Settings::bind applies the settings only while its callable runs. If the caller polls the returned future, that happens after the scope ends, so snapshots inside it do not see the settings.

Known problems

The lint detects any callable whose result implements Future, but its machine-applicable fix remains limited to a parameterless synchronous closure that returns a direct async block. That fix renames bind to bind_async and removes the closure head. A move closure needs an async move block to preserve captures. Async closures, function calls, stored closures, and other callables receive help without a fix. For those cases, review captures and side effects because constructing the future directly can change when callable code runs.

Example

use insta::Settings;

async fn work() {}

async fn snapshot_work() {
    let settings = Settings::clone_current();
    let future = settings.bind(|| async { work().await });
    future.await;
}

Use instead

use insta::Settings;

async fn work() {}

async fn snapshot_work() {
    let settings = Settings::clone_current();
    settings.bind_async(async { work().await }).await;
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::new(), Settings::default(), and Default::default() calls that create insta::Settings.

Why is this bad?

Settings::new() starts from Insta's defaults and drops the settings bound in the current scope, such as redactions, filters, and the snapshot path. Snapshots taken with the new settings can then differ from the rest of the test suite. Settings::clone_current() keeps the current settings and lets the code change only what it needs.

Known problems

The lint warns even when a test must start from the defaults on purpose. The machine-applicable fix renames the constructor only when the call names the type, as in Settings::new(). A trait path such as Default::default() gets help text only.

Example

use insta::Settings;

fn compact_settings() -> Settings {
    let mut settings = Settings::new();
    settings.set_snapshot_suffix("compact");
    settings
}

Use instead

use insta::Settings;

fn compact_settings() -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_snapshot_suffix("compact");
    settings
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for insta::Settings::set_raw_info calls.

Why is this bad?

Insta applies the redactions from Settings::add_redaction to metadata set with set_info, but not to metadata set with set_raw_info. Values that the snapshot body hides, such as tokens or timestamps, can then appear in the stored snapshot metadata.

Known problems

The lint warns even when the value is already safe to store. set_info requires Insta's serde feature, so code without that feature has no replacement.

Example

use insta::{internals::Content, Settings};

fn user_settings(user: &str) -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_raw_info(&Content::String(user.to_owned()));
    settings
}

Use instead

use insta::Settings;

fn user_settings(user: &str) -> Settings {
    let mut settings = Settings::clone_current();
    settings.set_info(&user);
    settings
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for Insta snapshot assertion macros, such as assert_snapshot! and assert_debug_snapshot!, inside a for, while, or loop body without insta::allow_duplicates!. The lint skips an assertion with a computed snapshot name.

Why is this bad?

Each pass through the loop runs the same assertion. For a file snapshot with no explicit name, Insta stores a new numbered snapshot on each pass, tied to the iteration order. An inline snapshot repeated in a loop fails. Inside allow_duplicates!, Insta checks that every pass produces the same snapshot.

Known problems

The lint skips a computed snapshot name, such as format!("case_{i}"), because each pass can use a different name. It warns for a literal or a name that resolves in at most eight steps through simple immutable local bindings, same-crate constants, and same-crate inherent associated constants, because those values repeat. Trait-associated constants remain unknown, even with literal defaults, because an implementation may override the default. Mutable, destructured, or uninitialized bindings; external constants, statics, and associated constants; calls; and other computed names remain unknown. The lint does not check assertions inside closures passed to iterator methods such as for_each, or inside functions called from a loop.

Example

fn trims_whitespace() {
    for input in [" a", "a ", " a "] {
        insta::assert_snapshot!(input.trim());
    }
}

Use instead

fn trims_whitespace() {
    insta::allow_duplicates! {
        for input in [" a", "a ", " a "] {
            insta::assert_snapshot!(input.trim());
        }
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks doc comments on definitions that other crates can reach, including through re-exports. Crate and module docs need at least 40 prose words. Other items, methods, trait items, fields, and variants need at least 20 prose words. A word is any whitespace-separated token with a letter or digit. Inline code and code blocks do not count.

Why is this bad?

A one-line doc such as "Read state." repeats the item name. Callers of another crate cannot see the source, so they guess at units, error cases, side effects, and panics, and they guess wrong.

Known problems

Word count does not measure accuracy or usefulness, so padded text passes. The lint does not check items without docs; Rust's missing_docs lint covers those. It skips items generated by macros, items in trait impls, and items that are #[doc(hidden)] or inside a hidden module or crate. It also skips crates whose name ends in _support or _fixture.

Example

use std::sync::atomic::{AtomicU64, Ordering};

/// Read the counter.
pub fn read_counter(counter: &AtomicU64) -> u64 {
    counter.load(Ordering::Acquire)
}

Use instead

use std::sync::atomic::{AtomicU64, Ordering};

/// Return the current value of `counter` with `Ordering::Acquire`. The value
/// can be stale as soon as it returns, because other threads may increment the
/// counter at any time. This function never blocks and never panics.
pub fn read_counter(counter: &AtomicU64) -> u64 {
    counter.load(Ordering::Acquire)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Warns when a non-documentation attribute appears between documentation attributes on the same item.

Why is this bad?

Interleaved attributes interrupt the documentation block. Put item attributes after the documentation block.

Known problems

The lint checks AST attribute order because its subject is source layout. Macro-generated attributes are excluded. A machine-applicable ordering fix is offered only for one interleaved must_use, inline or cold attribute when every attribute is documentation or one of those built-ins. Procedural macros can inspect attribute order, so other cases receive help for manual review.

Example

/// Return the input.
#[must_use]
/// The caller retains ownership.
fn identity(value: i32) -> i32 { value }

Use instead

/// Return the input.
/// The caller retains ownership.
#[must_use]
fn identity(value: i32) -> i32 { value }
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks use items whose path starts with a module declared in the same module as the use, without a self:: prefix, such as use helpers::Thing; next to mod helpers.

Why is this bad?

A bare path such as helpers::Thing looks the same as an import from an external crate named helpers. The self:: prefix shows at the import site that the path is local.

Known problems

The lint does not check use items inside function bodies. For grouped imports such as use helpers::{One, nested::Two};, the lint reports each name but cannot suggest a fix. The lint does not check paths that start with a module brought into scope by another use item.

Example

mod helpers {
    pub struct Thing;
}

use helpers::Thing;

Use instead

mod helpers {
    pub struct Thing;
}

use self::helpers::Thing;
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks the message argument in macro calls whose final path segment is trace, debug, info, warn, error, or event. It reports unescaped { placeholders in cooked or raw string messages and literal concat! messages. It checks unqualified and qualified concat! calls, including absolute paths such as ::std::concat!, by final path segment. It accepts parenthesis, bracket, and brace delimiters.

It recognizes strings, raw strings, characters, booleans, integers, floats, and negative numeric literals. It reads the log key/value ; form and the positional level in tracing::event!, while skipping recognized options and structured fields. Named format arguments interpolate into the message text. Rust literal escapes are decoded before the resulting message text is checked.

Why is this bad?

A formatted value becomes part of free text. Log search and aggregation tools cannot group records by that value. A structured field keeps the value under its own key while the message stays constant.

Known problems

The lint reads macro spelling before expansion and does not resolve macro identity. A local macro whose final path segment matches a logging name can warn. An alias or wrapper with a different final segment is not recognized. The concat! parser also selects by final path segment and applies the built-in literal rules, so a local macro with that name can behave differently.

Only a direct string literal or literal concat! expression in the parsed message position is checked. String values in recognized macro options and structured fields are ignored. Computed message expressions, strings produced by other macros, and strings added during logging-macro expansion are not inspected. Formatted output such as println! and write! is not logging and is not checked.

Example

use tracing::info;

fn record_login(user_id: u64) {
    info!("user {user_id} logged in");
}

Use instead

use tracing::info;

fn record_login(user_id: u64) {
    info!(user_id, "user logged in");
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the combined line count of the Rust source files compiled into a crate. It warns when the crate has more than 20,000 non-test lines or more than 40,000 total lines. Blank and comment lines count, and test lines count only toward the total limit.

Set the limits in the workspace's dylint.toml:

[large-rust-crate]
non_test_line_limit = 20000
total_line_limit = 40000

Why is this bad?

A crate is the unit of compilation, so a change to any file recompiles the whole crate. A large crate also tends to mix responsibilities that could have separate owners, dependencies, and public APIs.

Known problems

The lint counts readable .rs files compiled into the crate that sit under the package directory, the nearest directory above the crate root with a Cargo.toml. It skips files from inactive cfg modules and files outside the package directory.

A line is a test line only when it belongs to an item marked with a test attribute, such as #[test], or a #[cfg(...)] that requires test. If syn cannot parse a file, every line counts as non-test. When code exceeds both limits, the lint reports only the total-line violation.

Example

The abbreviated snippets require their separate module files and omitted implementation.

// src/lib.rs of a crate whose modules total 25,000 non-test lines
pub mod billing;
pub mod reporting;
pub mod storage;

Use instead

Move independent modules into their own workspace crates and re-export them:

// src/lib.rs
pub use billing;
pub use reporting;
pub use storage;
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks each Rust source file compiled into the crate. It warns when a file has 1,500 or more non-test lines or 2,000 or more total lines. Blank and comment lines count. When a file reaches both limits, the lint reports only the total-line violation.

A line is a test line when it belongs to an item marked #[test], or to an attribute whose last path segment is test, such as #[tokio::test]. It also qualifies when a #[cfg(...)] requires test, such as #[cfg(test)] or #[cfg(all(test, unix))]. Test lines count only toward the total limit.

Why is this bad?

A file of this size usually holds several unrelated responsibilities. Reviewers and editors must scroll through code that does not concern the change, and the file becomes a frequent merge-conflict site.

Known problems

The lint reads the crate's own source files from disk under the compiler's current directory. It skips files outside that directory, files from inactive cfg modules, and files that other crates contribute, such as macro definitions and the standard library.

If syn cannot parse a file, every line counts as non-test. Test helpers without a test attribute or cfg(test), such as a tests.rs module loaded with a plain mod tests;, also count as non-test.

Example

The abbreviated snippets require their separate module files and omitted implementation.

// src/main.rs, 1,800 lines long
fn main() {
    let config = load_config();
    run_commands(&config);
}

fn load_config() -> Config {
    // 600 lines of configuration loading
}

fn run_commands(config: &Config) {
    // 1,100 lines of command handling
}

Use instead

// src/main.rs
mod commands;
mod config;

fn main() {
    let config = config::load();
    commands::run(&config);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for let Some(..) = option else { return Err(error) }; inside a function or closure that returns Result, when the else block holds only that return and error already has the function's error type.

Why is this bad?

The let-else spends three lines on a standard conversion. ok_or_else turns the Option into a Result, and ? returns the error, in one line.

Known problems

  • The machine-applicable fix always uses ok_or_else and wraps the initializer in parentheses, for example (find_user(id)).ok_or_else(|| error)?.
  • The lint gives help without a fix when the rewrite could fail to compile or change behavior. This covers a refutable pattern inside Some(..), such as Some(0), and an error value that uses a local variable or contains a closure, return, ?, or .await. It also covers a type annotation on the let, a statement inside a macro, and an initializer that is a reference to an Option. A place initializer such as holder.name gets a fix only when its type is Copy and the pattern has no ref binding.
  • The fix also requires an error value whose type does not come from the expected type, because ? converts the closure's error through From. An error such as "x".into(), Default::default(), an unsuffixed number, None, or Box::new(..) coerced to Box<dyn Error> gets help without a fix. Literals with a fixed type, constants, and struct literals keep the fix. If a direct function, method, or constructor call has no type parameter in its declared return type, it keeps the fix. Function-item error values, calls through function pointers, and block expressions get help without a fix.
  • The lint ignores an else block with any other statement, a return Err(..) whose error type differs from the function's error type, and an Err value built through From.

Example

fn user_name(id: u64) -> Result<&'static str, Error> {
    let Some(user) = find_user(id) else {
        return Err(Error::Missing);
    };
    Ok(user)
}

Use instead

fn user_name(id: u64) -> Result<&'static str, Error> {
    let user = find_user(id).ok_or(Error::Missing)?;
    Ok(user)
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks methods in From, TryFrom, and FromStr impls, including closures inside them, for logging calls. Logging calls are eprintln!, the debug!, error!, event!, info!, log!, trace!, and warn! macros of the log and tracing crates, and functions such as warn or log_error that resolve to a kslog, log, or tracing crate or module. A local macro that expands to one of these logging macros also counts.

Why is this bad?

Conversions run wherever code parses or converts a value, including in loops and in code that expects failure. A log line there repeats on every call. It can write raw input such as user data to logs and takes the choice of log level and wording away from the caller.

Known problems

The lint reports only the first logging call in each method. It misses logging done through helper functions, logging macros from other crates, and logging in impls of other traits, such as Into. The lint skips conversion impls generated by a macro because developers cannot edit generated code where the lint would point.

Example

struct UserId(String);

impl From<String> for UserId {
    fn from(raw: String) -> Self {
        eprintln!("converting user id: {raw}");
        UserId(raw)
    }
}

Use instead

struct UserId(String);

impl From<String> for UserId {
    fn from(raw: String) -> Self {
        UserId(raw)
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks functions, methods, and async fns that return Result<T, E> for an if let Err(..) branch or a match Err(..) arm that only logs the error and then continues. The caught error must have the same type E as the function's error.

Why is this bad?

The function can return the error, but the branch drops it after logging. The caller sees Ok and cannot retry, report, or handle the failure.

Known problems

The branch counts as logging only when every statement is a logging call. Logging calls include eprintln!, the debug!, error!, event!, info!, log!, trace!, and warn! macros from the log or tracing crates. A local macro that expands to one of those calls also counts. A function such as warn or log_error counts when it resolves to a kslog, log, or tracing crate or module. println! and logging through helper functions do not count.

The lint misses:

  • caught errors of a different type that ? could convert with From;
  • branches with any statement besides logging, including let;
  • match arms with a guard;
  • closures and async blocks.

Example

fn reload() -> Result<(), std::io::Error> {
    if let Err(error) = std::fs::remove_file("cache.bin") {
        eprintln!("cache cleanup failed: {error}");
    }
    Ok(())
}

Use instead

fn reload() -> Result<(), std::io::Error> {
    std::fs::remove_file("cache.bin")?;
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a for index in 0..values.len().saturating_sub(1) loop over a slice, array, or Vec binding whose body uses index only in values[index] and values[index + 1]. It suggests for window in values.windows(2) with window[0] and window[1].

Why is this bad?

The loop rebuilds adjacent pairs with index arithmetic. Each index operation carries a bounds check, and an off-by-one change to the range or an index can panic or skip a pair. slice::windows(2) yields each adjacent pair directly.

Known problems

  • Only the saturating_sub(1) bound triggers. The lint ignores a loop over 0..values.len() - 1.
  • The sequence must be a built-in slice, array, or Vec reached from an immutable local binding through only built-in references. The lint ignores custom Deref receivers and fields such as self.values.
  • The lint ignores a body that uses index in any other way, reads another element of the slice, or already uses the name window.

Example

fn print_pairs(values: &[i32]) {
    for index in 0..values.len().saturating_sub(1) {
        let current = values[index];
        let next = values[index + 1];
        println!("{current} {next}");
    }
}

Use instead

fn print_pairs(values: &[i32]) {
    for window in values.windows(2) {
        let current = window[0];
        let next = window[1];
        println!("{current} {next}");
    }
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to std::env::args and std::env::args_os, including calls through use imports and renamed imports.

Why is this bad?

Code that reads env::args() directly must handle help text, unknown flags, missing values, and error messages itself, and those cases are easy to get wrong. A parser such as clap derives that handling from one type definition.

Known problems

  • The lint exempts an args or args_os call only when its iterator result reaches resolved std::process::Command::args or Iterator::count through at most four standard skip or take adapters. Longer chains remain linted.
  • The lint still flags stored iterators, filtered or mixed streams, other consumers, and parsing operations such as next, nth, and collect.
  • Methods named args or count on other types do not qualify for suppression.
  • Small throwaway binaries that do not need a parser also trigger.
  • Argument reads hidden behind a wrapper function in another crate are not detected.
  • The lint cannot resolve calls through function-pointer bindings back to std::env::args or std::env::args_os.

Example

fn main() {
    let _command = std::env::args().nth(1);
}

Use instead

use clap::Parser;

#[derive(Parser)]
struct Cli {
    command: Option<String>,
}

fn main() {
    let _command = Cli::parse().command;
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks hand-written Debug implementations for local structs without type or const parameters. Their fmt body must be one debug_struct or debug_tuple chain. The chain must use the struct's name, show every field in declaration order as &self.field, and end with .finish(). That chain prints the same text as #[derive(Debug)].

When a struct has fields, its builder must match its field syntax. Named structs must use debug_struct with field labels. Tuple structs must use debug_tuple with positional fields. A different builder prints different text, so the lint skips it.

The machine-applicable fix adds #[derive(Debug)] to the struct and removes the implementation. The lint offers it only when neither item comes from a macro and the implementation has no attributes or doc comments.

Why is this bad?

Adding, removing, or renaming a field requires an update to a hand-written Debug implementation. #[derive(Debug)] stays in sync with the type definition and produces the same output.

Known problems

The lint misses implementations that write the output with write! or f.write_str, enums, and generic structs, where the derive adds a Debug bound to every type parameter. Implementations that leave out, reorder, rename, or redact a field, or that end with .finish_non_exhaustive(), print different text, so the lint skips them.

Example

use std::fmt;

struct User {
    id: u64,
}

impl fmt::Debug for User {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("User").field("id", &self.id).finish()
    }
}

Use instead

#[derive(Debug)]
struct User {
    id: u64,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks hand-written Default implementations for local structs without type or const parameters. The default method must return one struct expression, such as Self { ... }, Self(...), or Self, with every field set to its type's default value. A default value is a call that resolves to Default::default, such as Default::default(), u8::default(), or <Vec<T>>::default(), or false, the integer 0, or None.

The machine-applicable fix adds #[derive(Default)] to the struct and removes the implementation. The lint offers it only when neither item comes from a macro and the implementation has no attributes or doc comments.

Why is this bad?

Adding, removing, or renaming a field requires an update to a hand-written Default implementation. #[derive(Default)] stays in sync with the type definition and produces the same value.

Known problems

The lint misses fields set with constructors such as Vec::new() or String::new(), bodies with statements, and enums, where a derive needs a #[default] variant. The lint skips generic structs because the derive adds a Default bound to every type parameter. Clippy's derivable_impls lint reports many of the same implementations.

Example

struct Config {
    retries: u8,
    labels: Vec<String>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            retries: Default::default(),
            labels: <Vec<String>>::default(),
        }
    }
}

Use instead

#[derive(Default)]
struct Config {
    retries: u8,
    labels: Vec<String>,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a two-arm match on a HashMap or BTreeMap Entry where the Occupied arm updates the value through one get_mut() or into_mut() call and the Vacant arm only calls insert. The match must produce ().

Why is this bad?

The match spends several lines on an update-or-insert step. The reader must check both arms to see that they only change or add one value. and_modify(...).or_insert(...) states the same single-lookup update in one chain.

Known problems

  • Each arm must use its entry binding once. The lint ignores an Occupied arm that also calls get() or remove(), or a Vacant arm that reads key().
  • The lint ignores an arm with a guard, a wildcard arm, or an arm that contains return, break, continue, ?, or .await, because the arm body moves into a closure.
  • The lint ignores if let Entry::Occupied(..) = ... without a Vacant branch.

Example

use std::collections::{HashMap, hash_map::Entry};

fn count(map: &mut HashMap<String, i32>, key: String) {
    match map.entry(key) {
        Entry::Occupied(mut entry) => *entry.get_mut() += 1,
        Entry::Vacant(entry) => {
            entry.insert(1);
        }
    }
}

Use instead

use std::collections::HashMap;

fn count(map: &mut HashMap<String, i32>, key: String) {
    map.entry(key).and_modify(|value| *value += 1).or_insert(1);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks types that have both a hand-written Display implementation whose fmt body is one write!(...) expression and an empty Error implementation.

Why is this bad?

The message lives in a separate Display implementation, away from the type, and developers must keep both boilerplate implementations synchronized by hand. #[derive(thiserror::Error)] with an #[error("...")] attribute keeps the message on the type and generates both implementations.

Known problems

The suggestion adds a dependency on the thiserror crate. The lint resolves the standard Display, Error, and write! APIs, pairs implementations by the local type definition, and skips generic types. It also skips Error implementations with methods such as source, Display bodies that use more than one statement, and display implementations expanded from another macro. It does not recognize equivalent formatting written with f.write_str.

Example

use std::{error::Error, fmt};

#[derive(Debug)]
struct MissingUser {
    user_id: u64,
}

impl fmt::Display for MissingUser {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "missing user {}", self.user_id)
    }
}

impl Error for MissingUser {}

Use instead

#[derive(Debug, thiserror::Error)]
#[error("missing user {user_id}")]
struct MissingUser {
    user_id: u64,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a for loop whose body is only one Vec::push or VecDeque::push_back call on a local variable, a field, or a dereference of one. When the loop pushes its unchanged item from a Vec, VecDeque, array, or slice, the lint suggests replacing the loop with one extend call. Other source types receive help without a machine-applicable fix because their size_hint can have side effects.

Why is this bad?

The loop grows the collection one item at a time. Extend::extend adds the items in one call and can reserve capacity from the iterator size hint. When code transforms the pushed value, extend with map states the transformation in one place. When size_hint has side effects, calling it can change later yielded values. The lint limits machine-applicable fixes to standard source types whose size_hint has no side effects.

Known problems

  • Only the standard Vec::push and VecDeque::push_back trigger. The lint ignores other collections, such as HashSet::insert, and local types with a push method.
  • The target must be a built-in Vec or VecDeque reached from a local place through only built-in references. The lint ignores targets reached through custom Deref or DerefMut receivers.
  • The lint skips bodies that contain ?, .await, break, continue, or return, and pushed values that read the target collection.
  • The lint ignores a source expression that reads the target collection because extend holds the target borrow while it evaluates the source.
  • A target produced by a call or an index, such as target().push(value), is ignored because the loop evaluates it once per item.
  • The automatic fix applies only when the loop pushes its variable without coercion from a Vec, VecDeque, array, or slice, as a statement or block tail. Other source types, including iterator adapters, get help without a machine-applicable fix because their size_hint can have side effects. Other loops, such as loops in match arms, get help without a fix. Loops with line or block comments retain the diagnostic and its help text but get no automatic fix. Comment markers inside string literals do not suppress fixes.

Example

fn append_all(output: &mut Vec<i32>, values: Vec<i32>) {
    for value in values {
        output.push(value);
    }
}

Use instead

fn append_all(output: &mut Vec<i32>, values: Vec<i32>) {
    output.extend(values);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a for loop that pushes or inserts one ? expression. The loop must target an empty mutable collection declared just before it. The block must return that collection as Ok(collection) or Some(collection).

Why is this bad?

The mutable accumulator, the loop, and the wrapped return spread one operation over several statements. collect into Result<Vec<_>, _> or Option<Vec<_>> builds the collection and stops at the first failure in one expression.

Known problems

  • The collection must be a standard Vec, VecDeque, HashSet, BTreeSet, HashMap, or BTreeMap created by an argument-free new or default call. The constructor must resolve to that collection's own method or its standard Default implementation. The lint ignores lookalike associated functions, vec![], and Vec::with_capacity(n).
  • The loop body must be one push, push_back, or single-argument insert call. The lint ignores a body with any other statement or a two-argument HashMap::insert.
  • The inserted expression must contain exactly one ? operator outside a closure, and no return, break, continue, or .await. For Result, the failing expression must already have the function's error type. The lint ignores a ? that converts the error through From.
  • collect can need a type annotation, so the lint emits help without an automatic fix.

Example

use std::num::ParseIntError;

fn parse_all(values: &[&str]) -> Result<Vec<i32>, ParseIntError> {
    let mut output = Vec::new();
    for value in values {
        output.push(value.parse()?);
    }
    Ok(output)
}

Use instead

use std::num::ParseIntError;

fn parse_all(values: &[&str]) -> Result<Vec<i32>, ParseIntError> {
    values.iter().map(|value| value.parse()).collect()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a for loop whose body is only an if without else or let, where the if branch holds one call, method call, or assignment.

Why is this bad?

Nested blocks handle item selection and the action inside the loop, which adds two levels of indentation for one condition. filter(...).for_each(...) names the selection and the action as separate steps.

Known problems

  • The lint skips bodies that contain ?, .await, break, continue, or return outside a closure.
  • Compound assignments such as total += value do not count as an action, so the lint ignores those loops.
  • The lint ignores conditions with if let or a let chain.
  • Closure arguments can need extra dereferences, such as **value for a slice iterator. The lint emits help without an automatic fix.

Example

fn consume_positive(values: &[i32]) {
    for value in values {
        if *value > 0 {
            consume(*value);
        }
    }
}

Use instead

fn consume_positive(values: &[i32]) {
    values
        .iter()
        .filter(|value| **value > 0)
        .for_each(|value| consume(*value));
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a for loop whose body is only if let Some(..) = expr without else, where expr is an Option and the branch holds one call, method call, or assignment.

Why is this bad?

The loop mixes the conversion, the skip of None, and the action in nested blocks. filter_map(...).for_each(...) names the conversion and the action as separate steps.

Known problems

  • The lint skips bodies that contain ?, .await, break, continue, or return outside a closure.
  • The lint skips if let Some(..) = result.ok(), which reads better as if let Ok(..) = result.
  • The Some payload pattern must be _ or a binding without a subpattern. The lint ignores tuple patterns, x @ _, and partial patterns such as Some(0). filter_map would run the action for all Some values.
  • Compound assignments such as total += value do not count as an action. The lint ignores those loops.
  • The lint emits help without an automatic fix because closure arguments can need adjustment.

Example

fn consume_numbers(values: &[&str]) {
    for value in values {
        if let Some(number) = parse(value) {
            consume(number);
        }
    }
}

Use instead

fn consume_numbers(values: &[&str]) {
    values
        .iter()
        .filter_map(|value| parse(value))
        .for_each(consume);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a mutable accumulator declared just before a for loop that fills it in one of four ways:

  • Vec::new() followed by a body that is only out.push(..).
  • An integer 0 followed by one conditional increment using count += 1, count = count + 1, or count = 1 + count.
  • false followed by a body that is only if condition { found = true }.
  • true followed by a body that is only if condition { all_ok = false }.

Why is this bad?

The mutable binding, the loop, and the update spread one result over several lines. The boolean loops also keep iterating after they find the answer. map(...).collect(), filter(...).count(), any(...), and all(...) compute the same result in one expression, and any and all stop at the first decisive item.

Known problems

  • The lint requires the accumulator declaration directly before the loop. It ignores a loop separated from its accumulator by another statement.
  • The loop body must be the single push or the single if shown above. The lint ignores a push inside an if, a body with extra statements, and a pushed value or condition that contains break, continue, return, ?, .await, a loop, or an assignment.
  • The lint ignores a pushed value or condition that reads or captures the accumulator. The lint accepts a nested closure that shadows the accumulator with a separate binding.
  • any and all stop at the first decisive item. If the condition or the iterator has side effects, the rewrite runs them fewer times.
  • A custom iterator's size_hint() can have side effects or change later next() values. Collection adapters can call size_hint() during source iteration, so use .collect() only when the source iterator's size_hint() has no side effects.
  • The lint ignores an accumulator declared inside a macro expansion.
  • Only Vec::new() starts a collection. The lint ignores vec![] and Vec::with_capacity(n).
  • A Vec push loop can also trigger manual_extend_loop.

Example

fn count_even(values: &[i32]) -> usize {
    let mut count = 0;
    for value in values {
        if *value % 2 == 0 {
            count += 1;
        }
    }
    count
}

Use instead

fn count_even(values: &[i32]) -> usize {
    values.iter().filter(|value| **value % 2 == 0).count()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for if option.as_ref().is_some_and(predicate) { option.take() } else { None } on a standard Option, where both calls use the same local place: a local binding, a field of one, or a * projection of one.

Why is this bad?

The code writes the receiver twice. A later edit can test one Option and take another. Option::take_if tests and takes the value in one call.

Known problems

  • Only the as_ref().is_some_and(..) condition and a literal None in the else branch trigger. Conditions such as matches!(option, Some(..)) are ignored.
  • The lint ignores a receiver outside a local place, such as holder().slot.
  • The lint ignores a receiver that uses an overloaded Deref or DerefMut adjustment. Repeating that receiver can run user code twice.
  • The take_if predicate receives &mut T instead of &T, so the closure can need changes. The lint emits help without an automatic fix.

Example

fn take_positive(option: &mut Option<i32>) -> Option<i32> {
    if option.as_ref().is_some_and(|value| *value > 0) {
        option.take()
    } else {
        None
    }
}

Use instead

fn take_positive(option: &mut Option<i32>) -> Option<i32> {
    option.take_if(|value| *value > 0)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for two empty mutable collections of the same type declared just before a for loop. The loop body must be one if/else that inserts the loop item into the first or second collection. The block must return (first, second).

Why is this bad?

The split takes two mutable bindings, a loop, and two branches that repeat the item. A later edit can change the item in one branch only. Iterator::partition states the two-way split in one call.

Known problems

  • The collections must be standard Vec, VecDeque, HashSet, or BTreeSet values created with the collection's own new function (including vec![]) or with Default::default. The lint ignores Vec::with_capacity(n) and maps because their insertion takes a key and a value.
  • Each branch must call push, push_back, or insert with the loop binding itself. The lint ignores a loop pattern that destructures the item.
  • The condition must not use either collection, and must not contain return, break, continue, ?, or .await, because it moves into a closure.
  • The block must end with the tuple of both collections in declaration order. The lint ignores a loop whose results code uses another way.
  • The predicate closure receives &T, so it can need a dereference. The lint emits help without an automatic fix.

Example

fn split_sign(values: Vec<i32>) -> (Vec<i32>, Vec<i32>) {
    let mut positive = Vec::new();
    let mut other = Vec::new();
    for value in values {
        if value > 0 {
            positive.push(value);
        } else {
            other.push(value);
        }
    }
    (positive, other)
}

Use instead

fn split_sign(values: Vec<i32>) -> (Vec<i32>, Vec<i32>) {
    values.into_iter().partition(|value| *value > 0)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a block that binds an Option or Result, observes it with if let Some(..), if let Ok(..), or if let Err(..) = &binding, and then returns the binding unchanged.

Why is this bad?

The binding exists only to look at the value before returning it. The three statements hide that the value passes through unchanged. inspect and inspect_err state the side effect and the unchanged return in one chain.

Known problems

  • The block must contain exactly the let, the if let, and the returned binding. The lint ignores a block with any other statement.
  • The observation must borrow the binding as &name, have no else, and hold one action. The lint skips it when it contains ?, .await, break, continue, or return outside a closure.
  • The variant payload pattern must be _ or a binding without a subpattern. The lint ignores tuple patterns, x @ _, and partial patterns such as Some(0). inspect would observe all Some values.
  • With inspect, temporaries in the initial value can drop at a different time. The lint emits help without an automatic fix.

Example

fn load(ok: bool) -> Result<i32, String> {
    let result = operation(ok);
    if let Err(error) = &result {
        println!("{error}");
    }
    result
}

Use instead

fn load(ok: bool) -> Result<i32, String> {
    operation(ok).inspect_err(|error| println!("{error}"))
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for test functions that loop over a literal list of cases with for or Iterator::for_each. A case list is an array, slice, repeated array, or vec![...] literal. It can also be a local variable, const, or static in the same crate that holds one, including after calls such as .iter() or .into_iter().

Why is this bad?

All cases share one test result. The first failing case stops the loop, so later cases do not run, and the failure report does not name the case. test-case turns each case into its own named test.

Known problems

A function counts as a test when it has #[test] or its name starts with test_. The lint skips functions that already use #[test_case(...)] or #[test_case::test_case(...)].

It warns on any loop over a literal list in a test, including setup loops that are not cases. For example, setup code pushes fixed bytes into a buffer. It does not flag case lists returned by a helper, taken from another crate, or iterated with while let. It does not flag test attributes from other crates, such as #[tokio::test], unless the name starts with test_. It reports only the first loop in each test.

Example

#[test]
fn parses_status() {
    for (raw, expected) in [("ok", true), ("no", false)] {
        assert_eq!(parse(raw), expected);
    }
}

Use instead

use test_case::test_case;

#[test_case("ok", true)]
#[test_case("no", false)]
fn parses_status(raw: &str, expected: bool) {
    assert_eq!(parse(raw), expected);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a for loop whose body is one expression with one ?. The loop must be the last statement of a function, closure, or async body, and the body must end with Ok(()), Some(()), or ControlFlow::Continue(()).

Why is this bad?

The loop, the ?, and the separate success tail spread one fallible pass over several lines. Iterator::try_for_each applies the action, stops at the first failure, and returns the result in one expression.

Known problems

  • The ? operand must already have the tail's error, None, or break type. The lint ignores a ? that converts the error through From.
  • The lint ignores a loop body with more than one statement or more than one ?. It also ignores a body that contains break, continue, return, or .await, because those would apply to the closure.
  • The lint ignores a loop followed by another statement before the tail. It also ignores a loop in a nested block, because its ? returns from the enclosing body.
  • The lint ignores a tail produced by a macro.
  • The closure can need type annotations, so the lint emits help without an automatic fix.

Example

use std::io;

fn write_all(values: &[i32]) -> io::Result<()> {
    for value in values {
        write_value(value)?;
    }
    Ok(())
}

Use instead

use std::io;

fn write_all(values: &[i32]) -> io::Result<()> {
    values.iter().try_for_each(write_value)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for two empty mutable collections declared just before a loop. The loop must use the pattern for (first, second) in .... Its body must push or insert each binding into the matching collection. The block must return both collections as a tuple.

Why is this bad?

The split takes two mutable bindings and a loop to do what one call does. A later edit can swap the targets or drop one push. Iterator::unzip states the pair split in one call.

Known problems

  • The collections must be standard Vec, VecDeque, HashSet, BTreeSet, HashMap, or BTreeMap values created by an argument-free new or default call owned by that collection. The lint ignores lookalike associated functions on other types, vec![], and Vec::with_capacity(n).
  • The loop pattern must be a two-name tuple, and the body must insert the names unchanged in pattern order with push, push_back, or insert. The lint ignores swapped or transformed items.
  • The block must end with the tuple of both collections in declaration order. The lint ignores a loop when another expression consumes its results.
  • The lint emits help without an automatic fix.

Example

fn split_pairs(pairs: Vec<(i32, String)>) -> (Vec<i32>, Vec<String>) {
    let mut ids = Vec::new();
    let mut names = Vec::new();
    for (id, name) in pairs {
        ids.push(id);
        names.push(name);
    }
    (ids, names)
}

Use instead

fn split_pairs(pairs: Vec<(i32, String)>) -> (Vec<i32>, Vec<String>) {
    pairs.into_iter().unzip()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for #[test] functions that contain four or more calls to the standard assert, assert_eq, assert_ne, debug_assert, debug_assert_eq, or debug_assert_ne macros, including through use renames.

Why is this bad?

A test with many assertions often checks one structured output piece by piece. The first failing assertion stops the test, so the report shows one difference at a time. When the output changes on purpose, each assertion needs a manual edit. A snapshot assertion with insta shows the full difference and updates with one command.

Known problems

It warns when the assertions check independent behavior that a snapshot would not describe well. It counts each macro call once, so an assertion inside a loop counts once. It does not count assertions in helper functions, local macros named assert_eq, or other assertion macros such as assert_matches. It skips test attributes from other crates, such as #[tokio::test].

Example

#[test]
fn renders_report() {
    let report = render_report();
    assert!(report.contains("Summary"));
    assert!(report.contains("Total"));
    assert!(report.contains("Details"));
    assert!(report.contains("Footer"));
}

Use instead

#[test]
fn renders_report() {
    let report = render_report();
    insta::assert_snapshot!(report);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Counts the return expressions and ? operators in each function, method, and closure, and warns when there are more than 4. Reaching the end of the body does not count. The limit of 4 follows the agent-feedback threshold in the big-code-analysis threshold guide.

Why is this bad?

Each exit is a place where the function can stop with a different result. Many exits often mean that several independent checks share one function. A reader must trace every exit to know which state the function leaves behind.

Known problems

Guard clauses can make a function easier to read, and the lint counts them the same as other exits. The lint does not count exits a macro generates, such as the return in anyhow::bail!. It counts a ? written as a macro argument. It ignores panic! and break. A return or ? inside a closure counts toward the closure, not the enclosing function. The lint does not check a function a macro generates.

Example

fn pick(values: [Option<u8>; 3]) -> Option<u8> {
    let first = values[0]?;
    let second = values[1]?;
    let third = values[2]?;
    if first == 0 {
        return None;
    }
    if second == 0 {
        return None;
    }
    Some(third)
}

The three ? operators and two return expressions give five exits.

Use instead

Move a repeated check into a separate fallible function.

fn nonzero(value: Option<u8>) -> Option<u8> {
    value.filter(|&value| value != 0)
}

fn pick(values: [Option<u8>; 3]) -> Option<u8> {
    nonzero(values[0])?;
    nonzero(values[1])?;
    values[2]
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a clippy.toml or .clippy.toml file in Clippy's configuration search path. Clippy chooses CLIPPY_CONF_DIR when set, then CARGO_MANIFEST_DIR when set, then the current directory. It searches the chosen directory and each parent through the filesystem root. Clippy checks .clippy.toml before clippy.toml in each directory. It does not fall back to a lower-priority start directory if the chosen directory has no configuration file.

This lint checks only whether the file exists. Clippy validates its contents.

The warning points at the package manifest's [package] header.

Why is this bad?

Without a checked-in clippy.toml, Clippy uses its defaults or a configuration file outside the repository. Different machines and CI can then report different Clippy results for the same code.

Known problems

The lint skips crates without a package manifest, such as files compiled directly with rustc.

Example

my-crate/
├── Cargo.toml
└── src/lib.rs

Use instead

my-crate/
├── Cargo.toml
├── clippy.toml
└── src/lib.rs
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks functions, inherent methods, and trait method declarations that other crates can reach. The lint warns when their doc comment lacks a # Examples section with a non-empty Rust code block. Items without any doc comment also warn.

To check every function and method, including private ones, set the scope in the workspace's dylint.toml:

[missing-doctest-examples]
scope = "all"

Why is this bad?

Without an example, readers must work out the call pattern from the signature. An example in # Examples runs as a doctest, so cargo test fails when the API changes and the example no longer compiles or passes.

Known problems

The heading must be a level-one heading with the exact text Examples, so # Example and ## Examples do not count. An indented code block counts, as does a block whose fence info string holds only rustdoc attributes such as rust, no_run, should_panic, or ignore. An ignore block counts even though it never runs. The lint does not judge whether the example is useful.

It skips functions generated by macros, methods in trait impls, items that are #[doc(hidden)] or inside a hidden module, the crate's main entry point, and #[test] functions. With scope = "all", it also warns on small private helpers.

Example

/// Add one to a value.
pub fn increment(value: u64) -> u64 {
    value + 1
}

Use instead

/// Add one to a value.
///
/// # Examples
///
/// ```
/// assert_eq!(my_crate::increment(1), 2);
/// ```
pub fn increment(value: u64) -> u64 {
    value + 1
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for functions and methods whose body has five or more statements and fewer // comments than one per five statements, rounded up. A body with 11 statements needs 3 comments. Trailing expressions of blocks count as statements, including those in nested if, match, and loop blocks. A macro call counts as one statement no matter how many statements it expands to, and statements written inside macro arguments count normally.

Why is this bad?

A long body without comments shows what the code does but not why. Reviewers must work out the reason for the ordering, the invariants, and the error handling from the code alone. A later edit can break an assumption that the code never records.

Known problems

The lint lexes the body's source text and counts the lines that hold a // line comment, including doc comments, commented-out code, and comments after code. A // inside a string literal does not count, and block comments do not count. The lint does not judge comment quality or check how authors spread comments across the body.

The lint does not count statements inside closures toward the enclosing function, and it does not check closures on their own.

Example

fn apply(value: u64) -> u64 {
    let doubled = value * 2;
    let adjusted = doubled + 1;
    let bounded = adjusted.min(100);
    let shifted = bounded + 2;
    let restored = shifted - 2;
    restored.saturating_sub(3)
}

Use instead

fn apply(value: u64) -> u64 {
    // Normalize before applying the overflow policy.
    let doubled = value * 2;
    let adjusted = doubled + 1;
    let bounded = adjusted.min(100);
    let shifted = bounded + 2;
    let restored = shifted - 2;
    // Keep saturation last.
    restored.saturating_sub(3)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a rust-toolchain.toml file in the package directory and its ancestors through the workspace root. The workspace root is the package manifest itself when it has a [workspace] table. Otherwise, an explicit package.workspace path, resolved relative to the package manifest directory, selects the root. When that key is absent, the root is the nearest ancestor Cargo.toml with a [workspace] table that does not exclude the package. If the explicit workspace root is not an ancestor, the lint checks the package directory and workspace root separately. The warning points at the [package] header of the manifest.

Why is this bad?

Without a pinned toolchain, each contributor and CI job builds with whatever Rust version the machine provides. Compiler, Clippy, rustfmt, and Dylint results can then differ between machines.

Known problems

Only the exact name rust-toolchain.toml counts. A bare rust-toolchain file does not satisfy the lint; rust_toolchain_toml reports that file. The lint ignores a rust-toolchain.toml above the workspace root.

The lint skips crates without a package manifest, such as files compiled directly with rustc.

Example

my-crate/
├── Cargo.toml
└── src/lib.rs

Use instead

my-crate/
├── Cargo.toml
├── rust-toolchain.toml
└── src/lib.rs
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for top-level local modules that depend on each other in a cycle, and emits one warning per cycle that names every module in it.

A dependency is a resolved path or method call to a definition in another module of the same crate. Nested modules count as part of their top-level ancestor. The crate root is not part of the graph, because it declares and re-exports every module. The lint accepts the chain input -> policy -> output. Adding output -> input makes it report all three modules.

Why is this bad?

A cycle removes the dependency direction between modules. Every module in the cycle depends on the others for reading, testing, and moving, and a change in one can ripple around the whole cycle. Shared types tend to drift toward whichever module is easiest to import, which adds more reverse edges over time.

Known problems

The lint does not report a cycle that passes through items defined in the crate root. It does not report cycles between nested modules under one top-level module, so moving two cyclic modules under one parent silences the lint. The lint does not count references inside macro invocations or dependencies created at run time, such as trait objects or callbacks.

Example

mod parser {
    pub fn parse() {
        crate::model::validate();
    }

    pub fn token_limit() -> usize {
        64
    }
}

mod model {
    pub fn validate() {
        let _limit = crate::parser::token_limit();
    }
}

parser and model depend on each other.

Use instead

Move the shared definition to a module that both sides can depend on.

mod limits {
    pub fn token_limit() -> usize {
        64
    }
}

mod parser {
    pub fn parse() {
        crate::model::validate();
    }
}

mod model {
    pub fn validate() {
        let _limit = crate::limits::token_limit();
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Counts the distinct top-level local modules that each top-level module refers to, and warns when there are more than 7.

A reference is a resolved path or method call to a definition in another module of the same crate. Nested modules count as part of their top-level ancestor. Items defined directly in the crate root count as one more module. Repeated references to the same module count once. References to other crates do not count. The limit of 7 is a local policy.

Why is this bad?

Each outgoing dependency is one more reason for a module to change. A module that calls into parsing, storage, networking, formatting, and telemetry breaks when any of them changes. Its tests need setup for all of them.

Known problems

A composition root or main module can legitimately coordinate many modules. A pub use facade does not lower the count, because paths resolve to the original definition. A method call counts toward the module that defines the method, which for a trait method is the module that defines the trait. The lint does not count references inside macro invocations or dependencies created at run time, such as trait objects or registration. The lint does not measure coupling between nested modules under one top-level module.

Example

mod coordinator {
    pub fn run() {
        crate::input::read();
        crate::policy::check();
        crate::storage::load();
        crate::auth::authorize();
        crate::schedule::plan();
        crate::output::write();
        crate::telemetry::record();
        crate::recovery::checkpoint();
    }
}

coordinator refers to 8 top-level modules.

Use instead

Move related steps behind modules that each own one stage.

mod intake {
    pub fn run() {
        crate::input::read();
        crate::policy::check();
        crate::storage::load();
        crate::auth::authorize();
    }
}

mod delivery {
    pub fn run() {
        crate::schedule::plan();
        crate::output::write();
        crate::telemetry::record();
        crate::recovery::checkpoint();
    }
}

mod coordinator {
    pub fn run() {
        crate::intake::run();
        crate::delivery::run();
    }
}

Interpretation and sources

Module fan-out counts distinct outgoing module dependencies. A high value can identify an orchestration module, while generated adapters and facades need review. Compare changes with accepted code before interpreting a threshold as architectural evidence. Code Maat supplies separate history-based coupling measurements; this compiler lint measures static dependencies.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks modules that contain a struct, enum, union, or type alias whose name is the PascalCase form of the module name, such as UserProfile in mod user_profile. It warns when that type is not the first item after the module's leading use and extern crate items.

Why is this bad?

A module named after a type exists to define that type. When helpers, constants, or impls come first, a reader must scroll past them to find the definition that explains the rest of the module.

Known problems

The name match is exact, so mod api_client does not match APIClient, and the lint never treats traits as the module's type. It does not check the crate root. It ignores macro invocations and items that macros generate, both as the module's type and as items placed before it.

Example

mod user_profile {
    use std::fmt;

    fn normalize_name(raw: &str) -> String {
        raw.trim().to_owned()
    }

    struct UserProfile {
        name: String,
    }
}

Use instead

mod user_profile {
    use std::fmt;

    struct UserProfile {
        name: String,
    }

    fn normalize_name(raw: &str) -> String {
        raw.trim().to_owned()
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Estimates the number of acyclic paths through each function, method, and closure, and warns when the estimate exceeds 200.

Sequential decisions multiply: two if expressions in a row give 2 × 2 = 4 paths. Match arms add because only one arm runs, and a guarded arm adds one path for the case where the guard fails. A loop counts the paths through its body plus one exit path. A ? doubles the paths of its operand. && and || add the paths where evaluation stops early. A closure gets its own estimate.

The limit of 200 follows PMD's NPath Complexity rule.

Why is this bad?

Each path is a combination of branch outcomes that can reach the end of the function. Tests can cover every branch once and still miss the combination that produces the wrong final state. Flat code can hide this: nine sequential if expressions have little nesting but 512 paths.

Known problems

The estimate comes from syntax. It counts combinations that earlier checks make impossible, and equivalent rewrites can produce different estimates. A loop counts one pass through its body, not every possible iteration count.

Code generated by a macro adds no paths of its own, while expressions written as macro arguments count like other source code. A macro used as an if condition counts as one true and one false path. .await adds no paths. The lint excludes branches inside called functions, callbacks, and trait objects. The lint does not check a function a macro generates.

Example

fn count_enabled(flags: [bool; 9]) -> usize {
    let mut enabled = 0;
    if flags[0] { enabled += 1; }
    if flags[1] { enabled += 1; }
    if flags[2] { enabled += 1; }
    if flags[3] { enabled += 1; }
    if flags[4] { enabled += 1; }
    if flags[5] { enabled += 1; }
    if flags[6] { enabled += 1; }
    if flags[7] { enabled += 1; }
    if flags[8] { enabled += 1; }
    enabled
}

The nine sequential if expressions give 2⁹ = 512 paths.

Use instead

Replace the repeated independent decisions with data, or split them into stages that each return a result.

fn count_enabled(flags: [bool; 9]) -> usize {
    flags.into_iter().filter(|&flag| flag).count()
}

Interpretation and sources

NPath estimates acyclic execution paths. Sequential independent decisions multiply their path counts, so five independent two-way decisions can produce 32 paths despite a cyclomatic complexity of six. This estimates combinations to examine, not defect probability. The local Rust profile and fixtures control counting for match, guards, ?, short-circuit operators, loops, closures and macros. Compare only values from that profile. Nejmeh's original paper and ACPATH's analysis of limitations.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an immutable bool binding with a generic predicate name, such as is_empty or has_items. Its only use must be as the condition or negated condition of the if in the next statement.

Why is this bad?

The binding repeats what the expression already says, and the reader must look one line up to see what the if tests. Writing the expression in the if keeps the condition where the branch happens.

Known problems

  • The name check is a fixed word list. A name triggers only when it starts with is, has, have, can, should, contains, or matches and every following word is generic, such as empty, valid, ready, items, or value. The lint treats a name such as can_release_funds or is_over_limit as a domain concept and ignores it, even when it adds no meaning.
  • The initializer must be a comparison or a call to a function or method whose name starts with is_, has_, can_, or should_ (or is contains, matches, starts_with, ends_with, and similar). It can also be a !, &&, or || combination of those.
  • The machine-applicable fix removes the let statement and writes the initializer into the condition. It adds parentheses after ! for a binary operator and around any struct literal. An initializer written by a macro call gets help without a fix, and the lint ignores a let produced by a macro.
  • The lint ignores while conditions because inlining would re-evaluate the expression on every iteration.

Example

fn report(values: &[i32]) {
    let is_empty = values.is_empty();
    if is_empty {
        println!("no values");
    }
}

Use instead

fn report(values: &[i32]) {
    if values.is_empty() {
        println!("no values");
    }
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a free function visible only in its own module. Its body must be one expression without statements, and its only crate reference must be one direct call from another function, closure, or constant. The lint tokenizes each source file once and reuses its Rust identifier counts for other candidate helpers. Identifier counts use the same Unicode NFC normalization as rustc.

Why is this bad?

The reader must jump to another item to see one expression that runs in one place. Writing the expression at the call site keeps the behavior where callers use it. A helper still earns its place when its name states a rule, as covered by the exclusions below.

Known problems

  • Code removed by cfg, such as a #[cfg(test)] module in a library build, does not enter the compiled crate. To preserve helpers called by such code, the lint requires the helper name to appear exactly twice as a Rust identifier in its source file. Comments and literal contents are excluded. A matching identifier in an unrelated item can still suppress the warning.
  • The lint ignores a helper whose only call comes from a macro expansion.
  • Attributes other than doc comments, #[inline], #[must_use], and lint level attributes such as #[allow] prevent the lint. So do type or const parameters and an async or unsafe prefix. A pub(crate) function in the crate root has the same visibility as a private one and can trigger.
  • A body whose expression is an if, match, loop, closure, or block is ignored.
  • The lint ignores names containing a word such as validate, check, ensure, assert, test, fixture, build, make, hook, rule, policy, or invariant, or starting with can, should, is, has, or must.

Example

fn adjusted_total(amount: u64) -> u64 {
    amount.saturating_add(1)
}

fn checkout(amount: u64) -> u64 {
    adjusted_total(amount)
}

Use instead

fn checkout(amount: u64) -> u64 {
    amount.saturating_add(1)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks functions whose tail expression is a struct literal with fields cloned from a parameter, such as id: input.id.clone(). Clones of Copy fields do not count. It warns when:

  • an owned struct parameter without a Drop impl has two or more fields cloned and no field moved out of it, or
  • a borrowed parameter other than self has two or more fields cloned, in a function whose signature no trait fixes.

The borrowed rule skips &self receivers, because a method usually cannot consume its receiver, and trait methods such as From<&T>::from, because the trait fixes the parameter type.

For the owned rule, the lint suggests moving each cloned field instead. The suggestion is machine applicable only when the function body is the struct literal. The function must clone each field once directly from the parameter, and no other field expression uses the parameter.

Why is this bad?

When the function owns the input, each clone allocates a copy of data. The function could move those fields instead, then drop the original. When the function borrows the input but always needs owned fields, the signature hides that cost from callers. Those callers may already have an owned value to give up.

Known problems

The borrowed rule cannot inspect its callers. It still warns when every caller needs to keep the input after the call.

Cheap clones such as Arc count toward the total.

The lint checks only a struct literal in tail position. It skips a struct inside Ok(..), a return statement, or a struct built with ..base syntax. It also skips destructured parameters, ref parameters, and functions generated by macros.

Example

struct Input {
    id: String,
    name: String,
}

struct Output {
    id: String,
    name: String,
}

fn convert(input: Input) -> Output {
    Output {
        id: input.id.clone(),
        name: input.name.clone(),
    }
}

Use instead

struct Input {
    id: String,
    name: String,
}

struct Output {
    id: String,
    name: String,
}

fn convert(input: Input) -> Output {
    Output {
        id: input.id,
        name: input.name,
    }
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks pub functions and methods for by-value String or Vec<T> parameters, including aliases, when the body only reads them. The lint skips a parameter when the body moves it by returning or storing it, iterating it by value, or capturing it in a move closure. It also skips a mut binding.

Why is this bad?

A by-value String or Vec<T> forces a caller that only has a borrow to allocate a copy, even when the function only reads the value. &str and &[T] accept both owned and borrowed data without a copy.

Known problems

The lint skips functions with pub(crate) or narrower visibility. It checks a pub function inside a private module.

The lint skips trait methods, trait impl methods, async fns, destructured parameters, and functions generated by macros.

Example

pub fn count_errors(lines: Vec<String>) -> usize {
    lines.iter().filter(|line| line.contains("ERROR")).count()
}

Use instead

pub fn count_errors(lines: &[String]) -> usize {
    lines.iter().filter(|line| line.contains("ERROR")).count()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the nearest Cargo.toml above the crate root file. If that manifest has a [package] table but no top-level lints key, the lint warns at the [package] header. A [lints] or [lints.*] table, a dotted key such as lints.workspace = true, and an inline table such as lints = { workspace = true } all satisfy the lint.

Why is this bad?

A package without a [lints] table does not inherit [workspace.lints] and uses rustc and Clippy defaults. Lint policy then differs between crates without any visible setting in the manifest.

Known problems

An empty [lints] table satisfies the lint even though it configures nothing.

Example

[package]
name = "example"
version = "0.1.0"
edition = "2024"

Use instead

[package]
name = "example"
version = "0.1.0"
edition = "2024"

[lints]
workspace = true
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the crate's entry main function, when it does not return Result, for calls to the standard panic!, todo!, unimplemented!, unreachable!, assert!, assert_eq!, and assert_ne! macros, and to Option or Result .unwrap() and .expect(..).

Why is this bad?

A panic in main prints a panic message and a backtrace hint instead of an error message, and it exits with code 101. Returning Result from main lets ? report startup failures such as a missing file as ordinary errors.

Known problems

The lint matches macros and methods by their resolved definitions, so it skips local macros or methods with the same names. The lint also skips other panicking operations, such as indexing, slicing, .unwrap_err(), or debug_assert!.

A main that returns a non-Result type, such as std::process::ExitCode, is infallible for this lint, so the lint checks it.

The lint does not look inside closures, async blocks, or functions that main calls. It skips main in build.rs files and functions marked #[test].

Example

fn main() {
    let config = std::fs::read_to_string("config.toml").expect("config exists");
    println!("{config}");
}

Use instead

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = std::fs::read_to_string("config.toml")?;
    println!("{config}");
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for #[path = "..."] attributes, including those produced by cfg_attr, in any file other than the crate root file that rustc compiles, such as src/lib.rs, src/main.rs, build.rs, or src/bin/tool.rs.

Why is this bad?

A path attribute overrides where Rust looks for a module file. Readers who follow the standard layout look for src/parser/parser_support.rs and do not find the module. Outside a crate root, the override is hard to spot.

Known problems

The lint compares the attribute's file with the crate root file. It allows every crate root, including examples/demo.rs and tests/api.rs, and it warns in a nested module file even when its name is lib.rs or main.rs. It skips files that the compiler cannot map to a real path.

Example

The abbreviated snippets require their separate module files and omitted implementation.

// src/parser.rs
#[path = "shared/parser_support.rs"]
mod parser_support;

Use instead

// src/parser.rs
mod parser_support;

Place parser_support in src/parser/parser_support.rs, or declare the path override from main.rs, lib.rs, or build.rs when it is necessary.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for named fields whose type is String or &str and whose name contains a filesystem word. The words are path, paths, dir, dirs, directory, directories, folder, folders, file, filename, and filepath, matched between underscores and with case.

Why is this bad?

A string cannot hold every path the operating system allows, and it invites joining paths with format! and /. PathBuf and Path keep non-UTF-8 paths and provide join, parent, and extension.

Known problems

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. After peeling, it checks only String and &str, including type aliases and use renames. It does not inspect Vec<String>, Box<str>, or Cow<'_, str>.

It warns on any field that contains one of the words, including display text such as display_path_label or file_label. It skips a field whose name also contains a word for a non-filesystem path: api, crate, def, endpoint, http, import, item, json, key, mod, module, public, query, route, symbol, type, uri, url, web, or xpath. So module_path and url_path do not warn, and neither does a public_file that is a real file. It does not flag other path names, such as location or cachedir.

Example

struct CacheConfig<'a> {
    cache_dir: String,
    manifest_path: &'a str,
}

Use instead

use std::path::{Path, PathBuf};

struct CacheConfig<'a> {
    cache_dir: PathBuf,
    manifest_path: &'a Path,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks structs and enums that other crates can reach, including through re-exports. The lint warns when they implement serde Serialize or Deserialize but not schemars JsonSchema, in crates that use schemars. A crate uses schemars when it has any JsonSchema impl or when Cargo passes schemars to the compiled target as a direct dependency.

Why is this bad?

A public serde type usually describes JSON that other programs read or write. Without a JsonSchema impl, the type is missing from the generated schema, so clients and API documentation fall out of step with the Rust type.

Known problems

It warns on types with handwritten serde impls. A schemars entry under [dev-dependencies] makes test, example, and bench targets count as using schemars, but not the library itself. It does not check pub types inside private modules, pub(crate) types, types generated by macros, or schema traits other than schemars::JsonSchema.

Example

use serde::Serialize;

#[derive(Serialize)]
pub struct UserResponse {
    id: String,
}

Use instead

use schemars::JsonSchema;
use serde::Serialize;

#[derive(JsonSchema, Serialize)]
pub struct UserResponse {
    id: String,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Counts the public names that each reachable module exposes outside the crate, and warns when there are more than 25. Functions, types, traits, constants, nested modules, primitive aliases, and names from pub use re-exports count. Resolved glob re-exports add every exported name. Re-export aliases with different names count separately, even when they point to one definition. The lint counts a tuple or variant constructor once with its parent definition. Compiler-generated test-marker constants for #[test] functions do not count.

Items inside impl blocks do not count.

This local policy sets the limit at 25, below the nom = 30 default in the big-code-analysis threshold guide. This lint counts module names, not class methods.

Why is this bad?

Every public name is part of the crate's compatibility contract. A module with many names is hard to browse in documentation, and each name is one more thing that cannot change without a breaking release.

Known problems

The count gives a central type and a small function that supports it the same weight, even when their maintenance impact differs. Public items generated by a macro count. The lint skips crates whose name ends in _support or _fixture and crates marked #![doc(hidden)].

Example

pub fn f01() {} pub fn f02() {} pub fn f03() {} pub fn f04() {} pub fn f05() {}
pub fn f06() {} pub fn f07() {} pub fn f08() {} pub fn f09() {} pub fn f10() {}
pub fn f11() {} pub fn f12() {} pub fn f13() {} pub fn f14() {} pub fn f15() {}
pub fn f16() {} pub fn f17() {} pub fn f18() {} pub fn f19() {} pub fn f20() {}
pub fn f21() {} pub fn f22() {} pub fn f23() {} pub fn f24() {} pub fn f25() {}
pub fn f26() {}

The crate root exposes 26 public functions.

Use instead

Group related names into public modules, or keep helpers private.

pub mod read {
    pub fn f01() {} pub fn f02() {} pub fn f03() {} pub fn f04() {} pub fn f05() {}
    pub fn f06() {} pub fn f07() {} pub fn f08() {} pub fn f09() {} pub fn f10() {}
    pub fn f11() {} pub fn f12() {} pub fn f13() {}
}

pub mod write {
    pub fn f14() {} pub fn f15() {} pub fn f16() {} pub fn f17() {} pub fn f18() {}
    pub fn f19() {} pub fn f20() {} pub fn f21() {} pub fn f22() {} pub fn f23() {}
    pub fn f24() {} pub fn f25() {} pub fn f26() {}
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for #[cfg(...)] predicates that appear more than three times on items across a crate. It warns once per predicate, at the first occurrence. The lint does not count gates on test-only items or anything inside them.

Why is this bad?

Each gate is a place where the code differs between builds. When one predicate appears across many items and files, readers cannot see the full set of code the feature adds. A change to the feature must then touch every gate. A single gated module keeps that code in one place.

Known problems

The lint compares predicates by structure, so all(unix, feature = "abc") and all(feature = "abc", unix) count as the same predicate. The lint counts gates on items, impl and trait items, enum variants, and fields. It does not count cfg_attr, gates on statements or expressions, or gates produced by macros.

An item is test-only when it has an attribute whose last path segment is test, or a cfg that requires test. The lint reads only files under the package directory and skips files that syn cannot parse. The parser does not load module files when cfg disables the modules, so the lint does not count their gates.

Example

#[cfg(feature = "abc")]
fn first() {}

#[cfg(feature = "abc")]
fn second() {}

#[cfg(feature = "abc")]
fn third() {}

#[cfg(feature = "abc")]
fn fourth() {}

Use instead

#[cfg(feature = "abc")]
mod abc {
    pub fn first() {}
    pub fn second() {}
    pub fn third() {}
    pub fn fourth() {}
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to reqwest::blocking functions and methods inside an async function, async block, or async closure.

Why is this bad?

The blocking API runs its own runtime and waits for it on the current thread. Inside an async body, that wait stalls the executor thread, so other futures on it stop making progress. Tokio can also panic when code creates or drops a blocking client inside a Tokio runtime.

Known problems

The lint only looks at the nearest enclosing closure or async body. It misses a blocking call inside a synchronous closure that runs in the async body, such as an iterator adapter. This same rule keeps calls inside a tokio::task::spawn_blocking closure from triggering the lint.

The lint does not follow function calls. A synchronous function that uses reqwest::blocking does not trigger the lint when async code calls it.

Example

async fn fetch() -> Result<(), reqwest::Error> {
    let _response = reqwest::blocking::get("https://example.com")?;
    Ok(())
}

Use instead

async fn fetch() -> Result<(), reqwest::Error> {
    let _response = reqwest::get("https://example.com").await?;
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for Client::new, Client::builder, and ClientBuilder::new calls inside a loop, while, or for body. It covers both the async client and reqwest::blocking::Client.

Why is this bad?

Each Reqwest client owns its own connection pool. A new client on every iteration throws away the idle connections from the previous one. Each network call then pays for a new DNS lookup, TCP connection, and TLS handshake.

Known problems

The lint only finds direct constructor calls inside a loop expression. It misses a constructor inside a function that the loop calls, and a constructor inside an iterator closure such as for_each.

A loop that needs a separate client per iteration, for example to isolate cookies or proxies, also triggers the lint.

Example

async fn fetch_all(urls: &[&str]) -> Result<(), reqwest::Error> {
    for url in urls {
        let client = reqwest::Client::new();
        client.get(*url).send().await?;
    }
    Ok(())
}

Use instead

async fn fetch_all(urls: &[&str]) -> Result<(), reqwest::Error> {
    let client = reqwest::Client::new();
    for url in urls {
        client.get(*url).send().await?;
    }
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an async reqwest::Client passed directly to Arc::new or Rc::new.

Why is this bad?

Client already holds its state in an Arc, and cloning it shares the same connection pool. The outer Arc or Rc adds an allocation, a second reference count, and an extra pointer hop on every use, with no benefit.

Known problems

The lint only checks the Arc::new and Rc::new calls themselves. It misses a client wrapped through Arc::from, Into, or another function. It does not check reqwest::blocking::Client.

Example

use std::sync::Arc;

fn shared_client() -> Arc<reqwest::Client> {
    let client = reqwest::Client::new();
    Arc::new(client)
}

Use instead

fn shared_client() -> reqwest::Client {
    let client = reqwest::Client::new();
    client.clone()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for the reqwest::get and reqwest::blocking::get shortcut functions inside a loop, while, or for body.

Why is this bad?

Each shortcut call builds a new Client with its own connection pool. The loop reuses no connection, so every network call pays for a new DNS lookup, TCP connection, and TLS handshake.

Known problems

The lint only finds direct calls inside a loop expression. It misses a shortcut inside a function that the loop calls, and a shortcut inside an iterator closure such as for_each.

Example

async fn fetch_all(urls: &[&str]) -> Result<(), reqwest::Error> {
    for url in urls {
        let _response = reqwest::get(*url).await?;
    }
    Ok(())
}

Use instead

async fn fetch_all(urls: &[&str]) -> Result<(), reqwest::Error> {
    let client = reqwest::Client::new();
    for url in urls {
        let _response = client.get(*url).send().await?;
    }
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a RequestBuilder method chain that calls .multipart(form) and also sets the Content-Type header with .header(...), in either order.

Why is this bad?

.multipart sets Content-Type to multipart/form-data with the boundary string that separates the form parts. A manual value can drop that boundary or replace the generated header, so the server cannot split the body into parts.

Known problems

The lint only follows one method chain. It misses a header set in a separate statement or through .headers(map). It recognizes the header name only as the CONTENT_TYPE constant or a string literal equal to content-type, ignoring case.

Example

fn upload(client: &reqwest::Client, form: reqwest::multipart::Form) -> reqwest::RequestBuilder {
    client
        .post("https://example.com")
        .header(reqwest::header::CONTENT_TYPE, "multipart/form-data")
        .multipart(form)
}

Use instead

fn upload(client: &reqwest::Client, form: reqwest::multipart::Form) -> reqwest::RequestBuilder {
    client.post("https://example.com").multipart(form)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks reqwest::retry::Builder::max_extra_load calls whose statically known f32 value is below 0.0, above 1000.0, NaN, or infinite. It resolves local constants and supported f32 arithmetic.

Why is this bad?

max_extra_load panics when its argument is outside 0.0..=1000.0. The program crashes when it builds the retry policy, even though the source contains the bad value.

Known problems

The lint resolves local non-trait f32 constants, unary negation, and built-in +, -, *, /, or % expressions through 16 nested steps. It also recognizes f32::NAN, f32::INFINITY, and f32::NEG_INFINITY. The evaluator uses f32 precision, so it follows the value after f32 rounding. Runtime values, function calls, casts, statics, trait or other external constants, overloaded operators, control flow, and unsupported operators remain unknown.

Example

fn retry_policy() -> reqwest::retry::Builder {
    reqwest::retry::for_host("example.com").max_extra_load(-0.1)
}

Use instead

fn retry_policy() -> reqwest::retry::Builder {
    reqwest::retry::for_host("example.com").max_extra_load(0.2)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to reqwest::retry::Builder::no_budget.

Why is this bad?

no_budget removes the limit on retry traffic, which Reqwest documents as not recommended. When a service starts failing, every client retries without limit and adds load to the service that is already failing. This can turn a short outage into a retry storm.

Known problems

The lint flags every no_budget call, including ones in tests or benchmarks that need unlimited retries.

Example

fn retry_policy() -> reqwest::retry::Builder {
    reqwest::retry::for_host("example.com").no_budget()
}

Use instead

fn retry_policy() -> reqwest::retry::Builder {
    reqwest::retry::for_host("example.com").max_extra_load(0.2)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks tls_danger_accept_invalid_certs and deprecated danger_accept_invalid_certs on a Reqwest ClientBuilder. It reports true, local boolean constants set to true, and bounded !, &&, and || expressions that resolve to true.

Why is this bad?

The client then trusts any certificate for any site, including expired and self-signed ones. An attacker on the network can present their own certificate and read or change the traffic.

Known problems

Unknown runtime values remain unknown unless a known left operand of && or || short-circuits to a result without inspecting the right operand. Associated and external constants, comparisons, and expressions over 16 visited nodes remain unknown. It can still report test code that intentionally accepts a self-signed certificate from a local server.

Example

fn build_client() -> Result<reqwest::Client, reqwest::Error> {
    reqwest::Client::builder()
        .tls_danger_accept_invalid_certs(true)
        .build()
}

Use instead

Trust the specific certificate the server uses:

fn build_client(cert: reqwest::Certificate) -> Result<reqwest::Client, reqwest::Error> {
    reqwest::Client::builder()
        .tls_certs_merge([cert])
        .build()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks tls_danger_accept_invalid_hostnames and deprecated danger_accept_invalid_hostnames on a Reqwest ClientBuilder. It reports true, local boolean constants set to true, and bounded !, &&, and || expressions that resolve to true.

Why is this bad?

The client then accepts a valid certificate issued for any host. An attacker with a certificate for their own domain can impersonate the server and read or change the traffic.

Known problems

Unknown runtime values remain unknown unless a known left operand of && or || short-circuits to a result without inspecting the right operand. Associated and external constants, comparisons, and expressions over 16 visited nodes remain unknown. It can still report test code that intentionally accepts a mismatched certificate name from a local server.

Example

fn build_client() -> Result<reqwest::Client, reqwest::Error> {
    reqwest::Client::builder()
        .tls_danger_accept_invalid_hostnames(true)
        .build()
}

Use instead

Keep hostname verification enabled and use a certificate whose names match the URL host:

fn build_client() -> Result<reqwest::Client, reqwest::Error> {
    reqwest::Client::builder().build()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Runs the rumdl Markdown rules on doc comments attached to the crate, items, associated items, fields, and enum variants. It reports the first rule violation in each doc comment block.

The rules come from the nearest project configuration that rumdl check would find: .rumdl.toml, rumdl.toml, .config/rumdl.toml, pyproject.toml with [tool.rumdl], or a markdownlint file. The search starts in the directory of the documented source file and stops at the repository root, marked by .git. Without a project configuration, rumdl's defaults apply.

Why is this bad?

Rustdoc renders doc comments as Markdown. Broken emphasis, missing blank lines around headings, and similar mistakes render incorrectly or inconsistently, and nothing else in a normal build reports them.

Known problems

The lint does not read the user-level rumdl configuration, and a project configuration that fails to load falls back to rumdl's defaults without a warning. The default line-length rule (MD013) reports doc lines longer than 80 characters. The lint reports only the first violation in each block, so fixing one can reveal the next. It offers a machine-applicable fix only for /// and //! comments with LF line endings. Block doc comments, #[doc = "..."] attributes, and generated docs receive a warning on the whole item without a fix.

Example

/// Do not write * emphasis * with spaces.
pub fn render_docs() {}

Use instead

/// Do not write *emphasis* with spaces.
pub fn render_docs() {}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks calls to std::env::var, std::env::var_os, std::env::vars, and std::env::vars_os outside configuration, startup, build-script, and test code. It matches calls by their standard-library definitions, so imported aliases remain covered.

Why is this bad?

A function that reads the environment has a hidden input that its signature does not show. Callers cannot pass a different value, and tests that set environment variables affect each other because all tests share one process environment. Parsing and default rules for the variable spread to every read site.

Known problems

The lint allows a function when any of these conditions applies:

  • The function's name is main.
  • Its name or the name of any enclosing module, type, or impl contains one of these words: cli, config, configuration, bootstrap, settings, setting, env, or environment.
  • Its path contains the word test, tests, or testing.
  • It is a #[test] function, or it or an enclosing item has a cfg that requires test, such as #[cfg(test)] or #[cfg(all(test, unix))].
  • It is in build.rs, or the file name or its parent directory contains one of the allowed words, such as config.rs or settings/mod.rs.

The lint compares ASCII letters without regard to case. ASCII letters and digits form tokens. Lower-to-upper CamelCase and acronym-to-word transitions split tokens. Other non-alphanumeric ASCII characters separate tokens. Each non-ASCII Unicode scalar acts as a separator. This allows load_config, AppConfig::load, and APIConfig::load.

The lint judges a read inside a closure, such as a LazyLock initializer, by the item that owns the closure. It does not follow reads through wrapper functions.

Example

fn handle_request(send: impl FnOnce(&str)) {
    let endpoint = std::env::var("API_ENDPOINT").unwrap();
    send(&endpoint);
}

Use instead

struct AppConfig {
    endpoint: String,
}

fn load_config() -> AppConfig {
    AppConfig {
        endpoint: std::env::var("API_ENDPOINT").unwrap(),
    }
}

fn handle_request(config: &AppConfig, send: impl FnOnce(&str)) {
    send(&config.endpoint);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a file named rust-toolchain, without the .toml extension, in the crate root file's directory or any parent directory. The warning points at the start of that file.

Why is this bad?

The bare rust-toolchain name is the legacy rustup form. The name does not show whether the file holds a one-line channel or TOML, and editors and other tools do not treat it as TOML.

Known problems

The search continues to the file system root, so a rust-toolchain file outside the repository also triggers the lint. The lint warns even when a rust-toolchain.toml file exists next to the bare file.

When the file is not valid UTF-8, the warning points at the source file at the crate root instead.

Example

my-crate/
├── Cargo.toml
├── rust-toolchain
└── src/lib.rs

Use instead

my-crate/
├── Cargo.toml
├── rust-toolchain.toml
└── src/lib.rs
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a manual JsonSchema::json_schema implementation that returns a schema built with Schema::new_ref.

Why is this bad?

Schemars advises implementations against returning a $ref schema from json_schema. The generator decides when to emit a $ref and registers the target definition. A hand-written $ref can point to a definition that the generator never adds, so the output schema has a dangling reference.

Known problems

The lint only finds Schema::new_ref as the tail expression, as a return value, or as the result of an if or match branch. The code can store a $ref in a variable before returning it, convert a $ref with .into(), or write a $ref with json_schema! without triggering it.

Example

use schemars::{JsonSchema, Schema, SchemaGenerator};

struct Wrapper;

impl JsonSchema for Wrapper {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "Wrapper".into()
    }

    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
        Schema::new_ref("#/$defs/Other".to_owned())
    }
}

Use instead

Let the generator create the reference with subschema_for:

use schemars::{JsonSchema, Schema, SchemaGenerator};

struct Wrapper;

impl JsonSchema for Wrapper {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "Wrapper".into()
    }

    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
        generator.subschema_for::<Other>()
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a struct or enum that contains itself and whose JsonSchema implementation returns true from inline_schema, either through #[schemars(inline)] or a manual implementation.

Why is this bad?

Schemars documents that inline_schema must return false for recursive types. Schemars expands an inlined schema in place at every use, so a recursive type expands without end while Schemars generates the schema.

Known problems

The lint only finds a type that contains itself directly, possibly through generic wrappers such as Box, Option, and Vec, or through arrays, tuples, and references. It misses mutual recursion through another type, such as A containing B and B containing A. It also misses an inline_schema body that computes its result instead of returning the literal true.

Example

#[derive(schemars::JsonSchema)]
#[schemars(inline)]
struct Node {
    next: Option<Box<Node>>,
}

Use instead

#[derive(schemars::JsonSchema)]
struct Node {
    next: Option<Box<Node>>,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[schemars(default)] attribute next to the same #[serde(default)] attribute on the same struct, enum, or field.

Why is this bad?

Schemars already reads #[serde(default)] when it derives JsonSchema. The duplicate attribute from Schemars changes nothing in the schema. If someone later removes the Serde attribute and keeps the Schemars copy, the schema shows a default that the deserializer does not apply.

Known problems

The lint compares parsed Serde and Schemars entries on the same AST node. Keys must match exactly, and string values compare after Rust decodes literal escapes. The lint visits loaded modules, local items, variants, and fields. Unsupported or malformed keys and value forms do not trigger the lint.

A fix removes the whole Schemars attribute when its only entry is redundant and comment-free. For one redundant entry in a mixed attribute, the fix deletes only that entry and one adjacent comma, including a legal trailing comma. Comments outside the entry remain. The lint warns without a fix when the matching entry contains a comment, multiple redundant entries share one attribute, or the matching Schemars attribute comes from macro expansion.

A different default function or a bare default versus default = "path" remains an override and does not trigger the lint.

Example

#[derive(serde::Deserialize, schemars::JsonSchema)]
struct Request {
    #[serde(default)]
    #[schemars(default)]
    id: String,
}

Use instead

#[derive(serde::Deserialize, schemars::JsonSchema)]
struct Request {
    #[serde(default)]
    id: String,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[schemars(deny_unknown_fields)] attribute next to the same #[serde(deny_unknown_fields)] attribute on the same struct or enum.

Why is this bad?

Schemars already reads #[serde(deny_unknown_fields)] when it derives JsonSchema. The duplicate attribute from Schemars changes nothing in the schema. If someone later removes the Serde attribute and keeps the Schemars copy, the schema rejects fields that the deserializer accepts.

Known problems

The lint compares parsed Serde and Schemars entries on the same AST node. Keys must match exactly, and string values compare after Rust decodes literal escapes. The lint visits loaded modules, local items, variants, and fields. Unsupported or malformed keys and value forms do not trigger the lint.

A fix removes the whole Schemars attribute when its only entry is redundant and comment-free. For one redundant entry in a mixed attribute, the fix deletes only that entry and one adjacent comma, including a legal trailing comma. Comments outside the entry remain. The lint warns without a fix when the matching entry contains a comment, multiple redundant entries share one attribute, or the matching Schemars attribute comes from macro expansion.

Example

#[derive(serde::Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
struct Request {
    id: String,
}

Use instead

#[derive(serde::Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
struct Request {
    id: String,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[schemars(rename = "...")] attribute next to the same #[serde(rename = "...")] attribute on the same item, field, or variant.

Why is this bad?

Schemars already reads #[serde(rename)] when it derives JsonSchema. The duplicate attribute from Schemars changes nothing in the schema. When someone later renames one copy and not the other, the schema documents a name the serializer does not use.

Known problems

The lint compares parsed Serde and Schemars entries on the same AST node. Keys must match exactly, and string values compare after Rust decodes literal escapes. The lint visits loaded modules, local items, variants, and fields. Unsupported or malformed keys and value forms do not trigger the lint.

For rename, a direct name applies to both serialization and deserialization. Direction-specific names compare by direction.

A fix removes the whole Schemars attribute when its only entry is redundant and comment-free. For one redundant entry in a mixed attribute, the fix deletes only that entry and one adjacent comma, including a legal trailing comma. Comments outside the entry remain. The lint warns without a fix when the matching entry contains a comment, multiple redundant entries share one attribute, or the matching Schemars attribute comes from macro expansion.

A different name in the Schemars attribute is an intended override and does not trigger the lint. A duplicated rename_all attribute triggers schemars-redundant-serde-rename-all instead.

Example

#[derive(serde::Serialize, schemars::JsonSchema)]
struct Response {
    #[serde(rename = "requestId")]
    #[schemars(rename = "requestId")]
    request_id: String,
}

Use instead

#[derive(serde::Serialize, schemars::JsonSchema)]
struct Response {
    #[serde(rename = "requestId")]
    request_id: String,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[schemars(rename_all = "...")] attribute next to the same #[serde(rename_all = "...")] attribute on the same struct or enum.

Why is this bad?

Schemars already reads #[serde(rename_all)] when it derives JsonSchema. The duplicate attribute from Schemars changes nothing in the schema. When someone later changes one copy and not the other, the schema documents names the serializer does not use.

Known problems

The lint compares parsed Serde and Schemars entries on the same AST node. Keys must match exactly, and string values compare after Rust decodes literal escapes. The lint visits loaded modules, local items, variants, and fields. Unsupported or malformed keys and value forms do not trigger the lint.

For rename_all, a direct rule applies to both serialization and deserialization. Direction-specific rules compare by direction.

A fix removes the whole Schemars attribute when its only entry is redundant and comment-free. For one redundant entry in a mixed attribute, the fix deletes only that entry and one adjacent comma, including a legal trailing comma. Comments outside the entry remain. The lint warns without a fix when the matching entry contains a comment, multiple redundant entries share one attribute, or the matching Schemars attribute comes from macro expansion.

A different case convention in the Schemars attribute is an intended override and does not trigger the lint.

Example

#[derive(serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(rename_all = "camelCase")]
struct Response {
    request_id: String,
}

Use instead

#[derive(serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
struct Response {
    request_id: String,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[schemars(skip)] attribute next to the same #[serde(skip)] attribute on the same field or variant. The lint also checks the skip_serializing, skip_deserializing, and skip_serializing_if keys, which Schemars also reads from Serde.

Why is this bad?

Schemars already reads #[serde(skip)] and the other skip keys when it derives JsonSchema. The duplicate attribute from Schemars changes nothing in the schema. If someone later removes the Serde attribute and keeps the Schemars copy, the schema omits a field that the serializer writes.

Known problems

The lint compares parsed Serde and Schemars entries on the same AST node. Keys must match exactly, and string values compare after Rust decodes literal escapes. The lint visits loaded modules, local items, variants, and fields. Unsupported or malformed keys and value forms do not trigger the lint.

A fix removes the whole Schemars attribute when its only entry is redundant and comment-free. For one redundant entry in a mixed attribute, the fix deletes only that entry and one adjacent comma, including a legal trailing comma. Comments outside the entry remain. The lint warns without a fix when the matching entry contains a comment, multiple redundant entries share one attribute, or the matching Schemars attribute comes from macro expansion.

Example

#[derive(serde::Serialize, schemars::JsonSchema)]
struct Record {
    #[serde(skip)]
    #[schemars(skip)]
    internal_id: String,
}

Use instead

#[derive(serde::Serialize, schemars::JsonSchema)]
struct Record {
    #[serde(skip)]
    internal_id: String,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[schemars(tag = "...")] attribute next to the same #[serde(tag = "...")] attribute on the same enum.

Why is this bad?

Schemars already reads #[serde(tag)] when it derives JsonSchema. The duplicate attribute from Schemars changes nothing in the schema. When someone later renames one copy and not the other, the schema documents a tag field the serializer does not write.

Known problems

The lint compares parsed Serde and Schemars entries on the same AST node. Keys must match exactly, and string values compare after Rust decodes literal escapes. The lint visits loaded modules, local items, variants, and fields. Unsupported or malformed keys and value forms do not trigger the lint.

A fix removes the whole Schemars attribute when its only entry is redundant and comment-free. For one redundant entry in a mixed attribute, the fix deletes only that entry and one adjacent comma, including a legal trailing comma. Comments outside the entry remain. The lint warns without a fix when the matching entry contains a comment, multiple redundant entries share one attribute, or the matching Schemars attribute comes from macro expansion.

A different tag name in the Schemars attribute is an intended override and does not trigger the lint.

Example

#[derive(serde::Serialize, schemars::JsonSchema)]
#[serde(tag = "kind")]
#[schemars(tag = "kind")]
enum Event {
    Created,
}

Use instead

#[derive(serde::Serialize, schemars::JsonSchema)]
#[serde(tag = "kind")]
enum Event {
    Created,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[schemars(transparent)] attribute next to the same #[serde(transparent)] attribute on the same struct.

Why is this bad?

Schemars already reads #[serde(transparent)] when it derives JsonSchema. The duplicate attribute from Schemars changes nothing in the schema. If someone later removes the Serde attribute and keeps the Schemars copy, the schema can describe a different shape than the serializer writes.

Known problems

The lint compares parsed Serde and Schemars entries on the same AST node. Keys must match exactly, and string values compare after Rust decodes literal escapes. The lint visits loaded modules, local items, variants, and fields. Unsupported or malformed keys and value forms do not trigger the lint.

A fix removes the whole Schemars attribute when its only entry is redundant and comment-free. For one redundant entry in a mixed attribute, the fix deletes only that entry and one adjacent comma, including a legal trailing comma. Comments outside the entry remain. The lint warns without a fix when the matching entry contains a comment, multiple redundant entries share one attribute, or the matching Schemars attribute comes from macro expansion.

Example

#[derive(serde::Serialize, schemars::JsonSchema)]
#[serde(transparent)]
#[schemars(transparent)]
struct Id(String);

Use instead

#[derive(serde::Serialize, schemars::JsonSchema)]
#[serde(transparent)]
struct Id(String);
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for schema_for_value!, SchemaGenerator::root_schema_for_value, and SchemaGenerator::into_root_schema_for_value called with a value whose type implements JsonSchema in the current crate.

Why is this bad?

A schema built from a value only describes that one value. For an enum, it covers only the variant the value holds, so the schema rejects valid data that uses another variant. Schemars documents that type-based generation gives a more precise schema.

Known problems

The lint only checks values whose type is a struct, enum, or union defined in the current crate, with or without references. It misses values of external types, primitives, and wrappers such as Vec<T>, even when they implement JsonSchema.

Example

#[derive(schemars::JsonSchema, serde::Serialize)]
enum Message {
    Text(String),
    Number(i64),
}

fn message_schema() -> schemars::Schema {
    schemars::schema_for_value!(Message::Number(7))
}

Use instead

#[derive(schemars::JsonSchema, serde::Serialize)]
enum Message {
    Text(String),
    Number(i64),
}

fn message_schema() -> schemars::Schema {
    schemars::schema_for!(Message)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for fields, function parameters, and let bindings whose type is a raw string or byte buffer and whose name marks a secret. A name marks a secret when it contains the word token, password, or secret, or the words api_key, or equals apikey, compared without case. The raw types are String, &str, Vec<u8>, Box<[u8]>, &[u8], [u8], and [u8; N].

Why is this bad?

A raw string or byte buffer prints in full through Debug, so a secret reaches logs through a derived Debug or a {:?} format. It also stays in memory after drop. A secret type such as secrecy::SecretString redacts Debug output and zeroes the memory on drop.

Known problems

It warns on values that only mention a secret word, such as a hashed password_hash, or a parser's next_token: String. It does not flag other secret names, such as private_key or credentials.

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. It does not inspect Box<str>, closure parameters, or destructured bindings. The lint skips trait impl method parameters because the trait supplies their types; it checks the trait declaration instead.

Example

struct Credentials<'a> {
    api_key: String,
    session_token: &'a str,
    password: Vec<u8>,
}

fn authenticate(access_token: String) {
    let refresh_token = "raw-token";
}

Use instead

use secrecy::{SecretBox, SecretString};

struct Credentials {
    api_key: SecretString,
    session_token: SecretString,
    password: SecretBox<[u8]>,
}

fn authenticate(access_token: SecretString) {
    let refresh_token = SecretString::from("raw-token");
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for fields, function parameters, and function return types that store a domain value as a primitive. It flags three cases by name:

  • Names ending in _id with an integer type.
  • Names equal to http_status or ending in _http_status with an integer type.
  • Names reason, reasons, reason_code, reason_codes, or ending in _reason_code or _reason_codes, with a String or str type.

It looks through references, slices, arrays, and Vec. The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. The lint checks a return type against the function name. It also flags a match on a string that has three or more string-literal arms and a catch-all arm.

Why is this bad?

Two u64 IDs from different tables have the same type, so passing a tenant ID where an account ID belongs still compiles. A string reason code accepts any text, and a string match with a catch-all arm silently accepts typos. A newtype or enum makes the compiler reject these mixups.

Known problems

It warns on a free-text field named reason that holds a human-readable message. It skips the string match inside a FromStr::from_str or TryFrom::try_from impl, where parsing strings into an enum is the intended fix. It also skips the methods of trait impls because the trait supplies their signature.

The match check counts arms whose pattern is only string literals, including "a" | "b", and needs an unguarded _ or binding arm such as other =>. It does not flag a bare id, a status field, closure parameters, or destructured parameters.

Example

struct Request {
    tenant_id: u64,
    reason_code: String,
}

fn state_slot(state: &str) -> usize {
    match state {
        "queued" => 0,
        "running" => 1,
        "complete" => 2,
        _ => 3,
    }
}

Use instead

struct TenantId(u64);

enum ReasonCode {
    Queued,
    Expired,
}

struct Request {
    tenant_id: TenantId,
    reason_code: ReasonCode,
}

enum State {
    Queued,
    Running,
    Complete,
}

fn state_slot(state: State) -> usize {
    match state {
        State::Queued => 0,
        State::Running => 1,
        State::Complete => 2,
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a struct that derives both Default and Deserialize, has at least two fields, and puts a bare #[serde(default)] on every field.

Why is this bad?

With a derived Default, a container-level #[serde(default)] fills each missing field with the same value as the field-level attributes. Repeating the attribute on every field adds noise, and a new field added without it becomes required by mistake.

Known problems

The lint skips a struct with a handwritten Default implementation, because its values can differ from the per-field defaults. It skips a struct where any field uses default = "path".

Example

#[derive(Default, serde::Deserialize)]
struct Settings {
    #[serde(default)]
    retries: u32,
    #[serde(default)]
    verbose: bool,
}

Use instead

#[derive(Default, serde::Deserialize)]
#[serde(default)]
struct Settings {
    retries: u32,
    verbose: bool,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for #[serde(borrow)] on a field of a type that derives Deserialize when the field type is &str, &[u8], or an Option of either.

Why is this bad?

Serde always borrows fields written as &str and &[u8], or Option of either, from the input. The attribute changes nothing on these types, and readers can mistake it for a needed setting.

Known problems

Serde decides implicit borrowing from the written type, so a type alias for &str still needs #[serde(borrow)] and the lint does not flag it.

The machine-applicable fix deletes the whole attribute, so the lint offers it only when borrow is the attribute's only entry. An attribute such as #[serde(borrow, rename = "name")] gets help without a fix.

Example

#[derive(serde::Deserialize)]
struct User<'a> {
    #[serde(borrow)]
    name: &'a str,
}

Use instead

#[derive(serde::Deserialize)]
struct User<'a> {
    name: &'a str,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a Cow<'a, str> or Cow<'a, [u8]> field without #[serde(borrow)] in a type that derives Deserialize. The lint skips fields with a 'static lifetime.

Why is this bad?

Serde only borrows a Cow field when it has #[serde(borrow)]. Without it, Serde always deserializes the field as Cow::Owned, so Serde allocates and copies every value even though the type has a lifetime for borrowing.

Known problems

Serde recognizes a borrowable Cow by its written name, so the lint checks only a field whose type path ends in Cow and resolves to the standard Cow. Serde does not borrow through a renamed import, a type alias, or a wrapper such as Option<Cow<'a, str>>, so the lint skips those fields.

A field intended to own its data also triggers the lint.

Example

use std::borrow::Cow;

#[derive(serde::Deserialize)]
struct Comment<'a> {
    body: Cow<'a, str>,
}

Use instead

use std::borrow::Cow;

#[derive(serde::Deserialize)]
struct Comment<'a> {
    #[serde(borrow)]
    body: Cow<'a, str>,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to serde::Deserializer::deserialize_any, written either as a method call or as a path call. The lint skips calls inside an implementation of Deserializer, because a format forwards to its own deserialize_any by design.

Why is this bad?

deserialize_any asks the input to say what type comes next. Only self-describing formats such as JSON can do that. Formats such as Postcard and Bincode return an error, so they cannot decode the type.

Known problems

Some types need dynamic input, such as a type that accepts either a string or a number. These calls also trigger the lint.

Example

use serde::de::{Deserializer, Visitor};

struct IdVisitor;

impl<'de> Visitor<'de> for IdVisitor {
    type Value = u64;

    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str("an id")
    }

    fn visit_u64<E>(self, value: u64) -> Result<u64, E> {
        Ok(value)
    }
}

fn deserialize_id<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
    deserializer.deserialize_any(IdVisitor)
}

Use instead

Call the deserialize_* method for the type the visitor expects:

use serde::de::Deserializer;

fn deserialize_id<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
    deserializer.deserialize_u64(IdVisitor)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[serde(expecting = "...")] message on a struct or enum that starts with a capitalized word or ends with a period.

Why is this bad?

Serde inserts the message into its own error text, which reads like "invalid type: integer 1, expected a user id". Serde documents the message as a completion of "This Visitor expects to receive ...". Start it with a lowercase letter and omit the final period.

Known problems

The lint keeps the case of a first word that looks like an acronym, such as "UUID string" or "I/O path". It still lowercases a single capital letter followed by a space. The fix edits the literal as written. When the source uses an escape for its first letter or final period, the lint gives help without a fix. Before emitting a fix, it validates the complete literal range and its UTF-8 boundaries before splitting the delimiters. If that validation fails, it gives help without a fix.

Example

#[derive(serde::Deserialize)]
#[serde(expecting = "A user id.")]
struct UserId(String);

Use instead

#[derive(serde::Deserialize)]
#[serde(expecting = "a user id")]
struct UserId(String);
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an internally or adjacently tagged enum that derives Deserialize and whose last variant is a unit variant named Other or Unknown without #[serde(other)].

Why is this bad?

Without #[serde(other)], the variant only matches the literal tag "Other" or "Unknown". Input with any new tag fails to deserialize, which defeats the fallback variant's intended purpose.

Known problems

A variant named Other or Unknown intended to match only its own tag also triggers the lint. The lint misses fallback variants with other names or in a position other than last.

Example

#[derive(serde::Deserialize)]
#[serde(tag = "kind")]
enum Event {
    Created,
    Deleted,
    Unknown,
}

Use instead

#[derive(serde::Deserialize)]
#[serde(tag = "kind")]
enum Event {
    Created,
    Deleted,
    #[serde(other)]
    Unknown,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[serde(flatten)] field in a struct that derives Deserialize. The lint reports it when the struct has #[serde(deny_unknown_fields)]. It also reports a field when its type, or the T of an Option<T> field, names a struct in the crate with that attribute.

Why is this bad?

Serde documents that flatten does not work with deny_unknown_fields on either the outer or the flattened struct. With this combination, deserialization can reject valid input because one struct treats the fields of the other as unknown.

Known problems

The lint reads deny_unknown_fields only from structs defined in the crate, so it misses a flattened struct from another crate.

Example

#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct User {
    id: String,
    #[serde(flatten)]
    extra: Extra,
}

#[derive(serde::Deserialize)]
struct Extra {
    trace_id: String,
}

Use instead

#[derive(serde::Deserialize)]
struct User {
    id: String,
    #[serde(flatten)]
    extra: Extra,
}

#[derive(serde::Deserialize)]
struct Extra {
    trace_id: String,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a Serde attribute that only affects one direction on a type that derives only the other direction. On a type that derives only Serialize, it checks alias, default, deserialize_with, borrow, and skip_deserializing. On a type that derives only Deserialize, it checks skip_serializing, skip_serializing_if, serialize_with, and getter.

Why is this bad?

The derive ignores the attribute, so it has no effect. Readers expect it to change behavior, and the author might have intended to derive the other direction.

Known problems

The lint reports inactive serialize and deserialize string values in rename, rename_all, rename_all_fields, and bound. If a plain #[serde(...)] source attribute has a unique supported direction entry and an exact deletion span without comments, the lint offers a machine edit that preserves active directional values. Otherwise, it gives help text without an edit.

Example

#[derive(serde::Serialize)]
struct Output {
    #[serde(alias = "old_value")]
    value: String,
}

Use instead

#[derive(serde::Serialize)]
struct Output {
    value: String,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a struct with at least two named fields where every field has #[serde(rename = "...")] and all names follow one rename_all convention. It checks each derived direction and also handles rename(serialize = "...", deserialize = "...").

Why is this bad?

A container-level rename_all states the convention once. Repeating the rename on every field adds noise, and a new field added without a rename breaks the convention.

Known problems

The lint only detects PascalCase, camelCase, SCREAMING_SNAKE_CASE, kebab-case, and SCREAMING-KEBAB-CASE. It skips a struct where any field has no rename, and it does not check enum variants or tuple structs.

The lint offers a machine-applicable fix only when each field attribute holds just the rename entry.

Example

#[derive(serde::Serialize, serde::Deserialize)]
struct User {
    #[serde(rename = "firstName")]
    first_name: String,
    #[serde(rename = "lastName")]
    last_name: String,
}

Use instead

#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct User {
    first_name: String,
    last_name: String,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for serializer.serialize_str(&value.to_string()), where to_string is ToString::to_string and the type of value implements Display.

Why is this bad?

to_string allocates a String only to pass it as a &str. Serializer::collect_str takes the Display value directly, and serializers such as serde_json write it without the extra allocation.

Known problems

The lint misses a to_string result stored in a variable first, and other ways to build the string, such as format!. Serde's default collect_str still allocates, so the change only saves memory with serializers that override it.

The lint skips a value whose own type lacks a Display implementation. Examples include a type with a manual ToString implementation and a type that only dereferences to a Display type. collect_str would not accept either type.

Example

fn serialize<S: serde::Serializer>(value: &u64, serializer: S) -> Result<S::Ok, S::Error> {
    serializer.serialize_str(&value.to_string())
}

Use instead

fn serialize<S: serde::Serializer>(value: &u64, serializer: S) -> Result<S::Ok, S::Error> {
    serializer.collect_str(value)
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[serde(skip_serializing)] field in a type that derives both Serialize and Deserialize when the field has no skip, skip_deserializing, or default, and the container has no default. An Option field is not checked unless it has with or deserialize_with, because Serde fills a missing Option field with None.

Why is this bad?

skip_serializing does not skip deserializing. The serialized output leaves the field out, but deserialization still requires it, so reading back your own output fails with a missing field error.

Known problems

A field that other input always supplies also triggers the lint.

Example

#[derive(serde::Serialize, serde::Deserialize)]
struct Resource {
    name: String,
    #[serde(skip_serializing)]
    hash: String,
}

Use instead

#[derive(serde::Serialize, serde::Deserialize)]
struct Resource {
    name: String,
    #[serde(skip_serializing, default)]
    hash: String,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an enum variant with #[serde(skip)] or #[serde(skip_serializing)] in an enum that derives Serialize.

Why is this bad?

Serde returns an error when code serializes a variant marked skip or skip_serializing. The enum compiles as serializable, but serializing that one variant fails at runtime.

Known problems

Code that never serializes the variant still triggers the lint.

Example

#[derive(serde::Serialize)]
enum Event {
    Sent,
    #[serde(skip_serializing)]
    Internal,
}

Use instead

Remove the attribute so the variant serializes:

#[derive(serde::Serialize)]
enum Event {
    Sent,
    Internal,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[serde(untagged)] enum that derives Deserialize and has no #[serde(expecting = "...")] message.

Why is this bad?

When no variant matches, an untagged enum fails with a generic error such as "data did not match any variant of untagged enum Value". The error does not describe accepted input. An expecting message replaces it with a description of valid input.

Known problems

The lint does not check #[serde(untagged)] on a single variant.

Example

#[derive(serde::Deserialize)]
#[serde(untagged)]
enum Value {
    Name(String),
    Id(u64),
}

Use instead

#[derive(serde::Deserialize)]
#[serde(untagged, expecting = "a name string or a numeric id")]
enum Value {
    Name(String),
    Id(u64),
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the Cognitive Complexity of each function, method, and closure, and warns when the score is 15 or more.

Each if, loop, and match adds 1 plus its nesting depth. An if at the top level adds 1, an if inside it adds 2, and deeper nesting adds its current depth. An else if continues the chain without extra nesting. Each && and || adds 1 with no nesting penalty. A closure gets its own score. The limit of 14 follows PMD's Cognitive Complexity rule, which is stricter than the Clippy default of 25.

Why is this bad?

Deep nesting forces a reader to keep several conditions in mind before reaching the code that does the work. Reviewers can miss which condition controls a side effect, and an else far from its if is easy to misread.

Known problems

The score is structural. It cannot tell whether a condition has an obvious meaning. A long flat else if chain stays below the limit even when a lookup table would read better.

An else adds nothing, and break or continue to a label adds nothing. Recursion lies outside the score. The score excludes control flow a macro generates but includes expressions written as macro arguments. .await adds nothing. Functions that a macro generates also lie outside the score.

Example

fn report(a: bool, b: bool, c: bool, d: bool, e: bool, f: bool) {
    if a {
        if b {
            if c {
                if d {
                    if e {
                        if f {
                            println!("all checks passed");
                        }
                    }
                }
            }
        }
    }
}

The six nested if expressions score 1 + 2 + 3 + 4 + 5 + 6 = 21.

Use instead

Combine the conditions or return early so the main path stays flat.

fn report(a: bool, b: bool, c: bool, d: bool, e: bool, f: bool) {
    if a && b && c && d && e && f {
        println!("all checks passed");
    }
}

Interpretation and sources

SonarSource designed Cognitive Complexity to approximate understandability. The algorithm increments for breaks in linear flow, increments again for nesting, and discounts some readable shorthand. A deeply nested branch scores more than a flat sequence with the same Cyclomatic Complexity.

This makes it useful for review burden, but it is still a syntax heuristic. A small score does not prove clear names, good domain boundaries, or simple data flow. Rust analyzer behavior must cover match, guards, ?, async blocks, closures, labeled control flow, macros, and iterator chains.

The documented local source profile controls the threshold. Clippy's cognitive_complexity is a separate heuristic. SonarSource's paper and Clippy's caveat.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for any construction of sqlx::AssertSqlSafe, whatever the wrapped SQL text is.

Why is this bad?

AssertSqlSafe tells SQLx to accept a dynamic SQL string as safe. SQLx does not check or escape that string. If any part of it comes from user input, the input can change the statement and cause SQL injection.

Known problems

The lint flags every construction, including SQL that code builds only from trusted values or that a reviewer has checked by hand. It misses constructions inside a macro expansion.

Example

fn count_rows(table: &str) -> sqlx::AssertSqlSafe<String> {
    sqlx::AssertSqlSafe(format!("SELECT count(*) FROM {table}"))
}

Use instead

Keep the SQL text static and bind values as parameters. If a table or column name must vary, pick it from a fixed list:

fn count_rows_query(table: &str) -> Option<&'static str> {
    match table {
        "users" => Some("SELECT count(*) FROM users"),
        "teams" => Some("SELECT count(*) FROM teams"),
        _ => None,
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an empty collection passed to QueryBuilder::push_tuples: an empty array [], Vec::new(), or vec![], optionally borrowed.

Why is this bad?

push_tuples writes the parentheses around the tuple list before it reads the items. With no items, the query contains an empty () list, which is invalid SQL. The error only appears when the database runs the query.

Known problems

The lint checks only an argument written as [], Vec::new(), or vec![], optionally borrowed. It misses a collection that is empty only at runtime.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

fn find_pairs(query: &mut sqlx::QueryBuilder<'_, sqlx::Postgres>) {
    query.push("SELECT * FROM pairs WHERE (a, b) IN ");
    query.push_tuples([] as [(i32, i32); 0], |mut tuple, (a, b)| {
        tuple.push_bind(a).push_bind(b);
    });
}

Use instead

Handle the empty case before building the query:

fn find_pairs(query: &mut sqlx::QueryBuilder<'_, sqlx::Postgres>, pairs: Vec<(i32, i32)>) {
    if pairs.is_empty() {
        return;
    }
    query.push("SELECT * FROM pairs WHERE (a, b) IN ");
    query.push_tuples(pairs, |mut tuple, (a, b)| {
        tuple.push_bind(a).push_bind(b);
    });
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an empty collection passed to QueryBuilder::push_values: an empty array [], Vec::new(), or vec![], optionally borrowed.

Why is this bad?

push_values writes the VALUES keyword before it reads the items. With no items, the query ends in VALUES with no rows, which is invalid SQL. The error only appears when the database runs the query.

Known problems

The lint checks only an argument written as [], Vec::new(), or vec![], optionally borrowed. It misses a collection that is empty only at runtime.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

fn insert_users(query: &mut sqlx::QueryBuilder<'_, sqlx::Postgres>) {
    query.push("INSERT INTO users (id) ");
    query.push_values([] as [i32; 0], |mut row, id| {
        row.push_bind(id);
    });
}

Use instead

Handle the empty case before building the query:

fn insert_users(query: &mut sqlx::QueryBuilder<'_, sqlx::Postgres>, ids: Vec<i32>) {
    if ids.is_empty() {
        return;
    }
    query.push("INSERT INTO users (id) ");
    query.push_values(ids, |mut row, id| {
        row.push_bind(id);
    });
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to sqlx::Row::get and sqlx::Row::get_unchecked.

Why is this bad?

Both methods panic when the column does not exist or when SQLx cannot decode its value into the target type. A renamed column or an unexpected NULL then crashes the program instead of returning an error the caller can handle.

Known problems

The lint flags calls even when the column exists and its value has the expected type. It misses calls inside a macro expansion.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

use sqlx::Row;

fn user_name(row: &sqlx::postgres::PgRow) -> String {
    row.get("name")
}

Use instead

use sqlx::Row;

fn user_name(row: &sqlx::postgres::PgRow) -> Result<String, sqlx::Error> {
    row.try_get("name")
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to sqlx::Statement::column and sqlx::Row::column.

Why is this bad?

column panics when the index or name does not match a column of the prepared statement or the row. A changed query then crashes the program instead of returning an error the caller can handle.

Known problems

The lint flags calls even when it knows the index is valid.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

use sqlx::Statement;

fn first_column(statement: &sqlx::postgres::PgStatement<'_>) -> &sqlx::postgres::PgColumn {
    statement.column(0)
}

Use instead

use sqlx::Statement;

fn first_column(
    statement: &sqlx::postgres::PgStatement<'_>,
) -> Result<&sqlx::postgres::PgColumn, sqlx::Error> {
    statement.try_column(0)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to sqlx::pool::PoolConnection::leak.

Why is this bad?

SQLx documents that leak treats the connection as checked out forever, so each call lowers the pool's maximum size by one. After enough calls, the pool has no connections left, and every later acquire waits until it times out.

Known problems

The lint flags every call, including one that keeps a connection out of the pool on purpose. It misses calls inside a macro expansion.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

fn take_connection(
    connection: sqlx::pool::PoolConnection<sqlx::Postgres>,
) -> sqlx::PgConnection {
    connection.leak()
}

Use instead

detach also returns the connection but lets the pool open a replacement:

fn take_connection(
    connection: sqlx::pool::PoolConnection<sqlx::Postgres>,
) -> sqlx::PgConnection {
    connection.detach()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a format!(...) call, optionally borrowed, passed directly to QueryBuilder::push. Qualified forms such as std::format! also match.

Why is this bad?

push adds text to the SQL statement as is. Formatting a value into that text skips bind parameters. If the value comes from user input, it can change the statement and cause SQL injection.

Known problems

The lint checks only an argument that is the direct result of format!, optionally borrowed. It misses a formatted string stored in a variable first. It also flags format! calls that only insert fixed SQL fragments.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

fn filter_by_id(query: &mut sqlx::QueryBuilder<'_, sqlx::Postgres>, id: i64) {
    query.push(format!("WHERE id = {id}"));
}

Use instead

fn filter_by_id(query: &mut sqlx::QueryBuilder<'_, sqlx::Postgres>, id: i64) {
    query.push("WHERE id = ").push_bind(id);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a format!(...) call, optionally borrowed, passed directly to Separated::push_unseparated. Qualified forms such as std::format! also match.

Why is this bad?

push_unseparated adds text to the SQL statement as is. Formatting a value into that text skips bind parameters. If the value comes from user input, it can change the statement and cause SQL injection.

Known problems

The lint checks only an argument that is the direct result of format!, optionally borrowed. It misses a formatted string stored in a variable first. It also flags format! calls that only insert fixed SQL fragments.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

fn filter_by_ids(query: &mut sqlx::QueryBuilder<'_, sqlx::Postgres>, first: i64, second: i64) {
    query.push("WHERE id IN (");
    let mut ids = query.separated(", ");
    ids.push_bind(first);
    ids.push_unseparated(format!(", {second})"));
}

Use instead

fn filter_by_ids(query: &mut sqlx::QueryBuilder<'_, sqlx::Postgres>, first: i64, second: i64) {
    query.push("WHERE id IN (");
    let mut ids = query.separated(", ");
    ids.push_bind(first);
    ids.push_bind(second);
    ids.push_unseparated(")");
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for the SQLx macros query_unchecked!, query_as_unchecked!, query_scalar_unchecked!, query_file_unchecked!, query_file_as_unchecked!, and query_file_scalar_unchecked!.

Why is this bad?

The unchecked macros still parse the SQL and count its parameters and columns at compile time. They skip type checks on bind parameters and result columns, so a type mismatch fails at runtime instead of compile time.

Known problems

The lint also flags unchecked macros used on purpose, for example for a database type that SQLx cannot map to a Rust type.

The checked macro can reject a query that the unchecked macro accepts, so cargo fix does not apply the suggested rename.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

async fn user_exists(pool: &sqlx::PgPool, id: i64) -> Result<bool, sqlx::Error> {
    let row = sqlx::query_unchecked!("SELECT id FROM users WHERE id = $1", id)
        .fetch_optional(pool)
        .await?;
    Ok(row.is_some())
}

Use instead

async fn user_exists(pool: &sqlx::PgPool, id: i64) -> Result<bool, sqlx::Error> {
    let row = sqlx::query!("SELECT id FROM users WHERE id = $1", id)
        .fetch_optional(pool)
        .await?;
    Ok(row.is_some())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks PoolOptions::max_connections calls whose limit is statically known to be zero, including values resolved from local constants and supported unsigned arithmetic.

Why is this bad?

A pool with a maximum of zero connections can never hand out a connection. Every acquire and every query through the pool waits until the acquire timeout and then fails.

Known problems

The lint resolves integer literals, local non-trait constants, and unsigned +, -, *, /, or % expressions through 16 nested steps. It reports only values it can prove are zero. Runtime configuration and other runtime locals remain unknown. Casts, statics, trait or external constants, unsupported operators, and arithmetic that overflows, underflows, or divides by zero also remain unknown.

Example

The snippets use SQLx's PostgreSQL API. UI tests use a local SQLx fixture that omits this API.

fn pool_options() -> sqlx::pool::PoolOptions<sqlx::Postgres> {
    sqlx::pool::PoolOptions::new().max_connections(0)
}

Use instead

fn pool_options() -> sqlx::pool::PoolOptions<sqlx::Postgres> {
    sqlx::pool::PoolOptions::new().max_connections(8)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks function, method, and async fn return types for Result<T, String>, including required trait method signatures, aliases of Result or String, and a Result nested in the type arguments of the return type.

Why is this bad?

A String error only carries text. Callers cannot match on the failure kind or reach the original error through source(), and they must parse the message to react to a specific failure.

Known problems

The lint also warns at program edges where a text error is enough, such as a small command-line tool.

It does not inspect closures or return types written as an associated type such as <Self as Trait>::Out. It also ignores error types other than String, including &str and Box<dyn Error>.

Example

fn parse_port(raw: &str) -> Result<u16, String> {
    raw.parse::<u16>().map_err(|error| error.to_string())
}

Use instead

fn parse_port(raw: &str) -> Result<u16, std::num::ParseIntError> {
    raw.parse::<u16>()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for struct literals whose update base is Default::default(), such as Request { timeout: 5, ..Default::default() }.

Why is this bad?

The literal does not show which fields take default values. When someone adds a field to the struct, every such literal still compiles and takes the default. Adding a field triggers no call-site review.

Known problems

When the base is the built-in #[derive(Default)] of a struct in the same crate, the lint suggests listing each remaining field as field: Default::default(). It makes this suggestion only when those fields have no default field values. That derive calls Default::default() for each field, so the values do not change. Other cases, such as a handwritten Default impl, get help text only. When a generic whole-struct bound does not identify the default implementation, the lint also offers help text only.

It warns on types from other crates that have many fields, where listing every field is long and the crate documents ..Default::default() as the intended style. It does not flag other update bases, such as ..base or ..Request::new().

Example

#[derive(Default)]
struct Request {
    timeout: u64,
    retries: u8,
}

fn build() -> Request {
    Request {
        timeout: 5,
        ..Default::default()
    }
}

Use instead

#[derive(Default)]
struct Request {
    timeout: u64,
    retries: u8,
}

fn build() -> Request {
    Request {
        timeout: 5,
        retries: 0,
    }
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks fieldless enums for two problems:

  • The enum has hand-written implementations of both Display and FromStr.
  • The enum derives Serde and Strum traits, and the two produce or accept different names for a variant. The lint compares output names when the enum derives Serialize and one of strum::Display, AsRefStr, IntoStaticStr, or VariantNames. It compares accepted input names when it derives Deserialize and strum::EnumString.

The comparison applies Serde's rename_all, rename, alias, skip, skip_serializing, skip_deserializing, and other attributes, and Strum's serialize_all, to_string, serialize, prefix, suffix, disabled, default, and ascii_case_insensitive attributes.

Why is this bad?

Hand-written Display and FromStr implementations repeat the same mapping twice, and the two can drift apart. Strum derives both from one declaration.

When Serde and Strum use different names, the enum serializes one spelling and displays or parses another. The same rule name does not guarantee the same result. With snake_case, Serde turns HTTPResponse into h_t_t_p_response, while Strum produces http_response.

Known problems

The lint does not read the bodies of the hand-written implementations. It also flags an enum whose Display output targets people and differs on purpose from the names that FromStr parses.

The lint reports only the first divergent variant of each enum. It skips variants with non-ASCII names.

Example

use std::{fmt, str::FromStr};

enum Status {
    Ready,
    Busy,
}

impl fmt::Display for Status {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Ready => formatter.write_str("ready"),
            Self::Busy => formatter.write_str("busy"),
        }
    }
}

impl FromStr for Status {
    type Err = ();

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value {
            "ready" => Ok(Self::Ready),
            "busy" => Ok(Self::Busy),
            _ => Err(()),
        }
    }
}

The lint also flags Serde and Strum names that differ:

#[derive(serde::Serialize, strum::Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
enum ResponseKind {
    HTTPResponse,
}

Use instead

#[derive(strum::Display, strum::EnumString)]
#[strum(serialize_all = "lowercase")]
enum Status {
    Ready,
    Busy,
}

Spell the variant so that both case rules give the same name:

#[derive(serde::Serialize, strum::Display)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
enum ResponseKind {
    HttpResponse,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] or #[test_matrix(...)] function whose generated test functions are still async, because no async runtime test attribute such as #[tokio::test] follows the test-case attributes.

Why is this bad?

test-case does not add #[test] to async functions. It copies the attributes written after it, such as #[tokio::test], onto every generated case. Without such an attribute, the generated cases are plain async functions. cargo test does not run them and reports no failure.

Known problems

A custom test framework that finds async functions without transforming them can trigger a false positive.

Example

use test_case::test_case;

async fn double(value: u8) -> u8 {
    value * 2
}

#[test_case(1; "one")]
async fn doubles(value: u8) {
    assert_eq!(double(value).await, value * 2);
}

Use instead

Place the runtime attribute after the test-case attributes.

use test_case::test_case;

async fn double(value: u8) -> u8 {
    value * 2
}

#[test_case(1; "one")]
#[tokio::test]
async fn doubles(value: u8) {
    assert_eq!(double(value).await, value * 2);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] or #[test_matrix(...)] output of the form matches PATTERN if GUARD where the whole guard is the literal true or false.

Why is this bad?

An if true guard adds no condition, so the case checks less than it appears to. An if false guard rejects every value, so the case always fails.

Known problems

The lint checks only a guard that is the literal true or false. A named constant, !false, (true), or a compound expression does not trigger it.

For if true, the fix removes the guard. An if false guard gets help text but no automatic fix, because the intended condition is unknown.

Example

use test_case::test_case;

#[test_case(1 => matches Some(_) if true; "some")]
fn wrap(value: u8) -> Option<u8> {
    Some(value)
}

Use instead

Remove a true guard. Replace a false guard with the intended condition.

use test_case::test_case;

#[test_case(1 => matches Some(value) if value > 0; "some")]
fn wrap(value: u8) -> Option<u8> {
    Some(value)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] attribute whose description after the ; is an empty string literal, such as #[test_case(1; "")].

Why is this bad?

test-case builds the generated test name from the description. An empty description produces an uninformative name. A failure report then cannot identify the input or behavior that the case covers.

Known problems

A description produced by another macro does not trigger the lint.

Example

use test_case::test_case;

#[test_case(1; "")]
fn accepts_positive(value: u8) {
    assert!(value > 0);
}

Use instead

use test_case::test_case;

#[test_case(1; "minimum positive value")]
fn accepts_positive(value: u8) {
    assert!(value > 0);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] or #[test_matrix(...)] attribute that uses an ignore[""] or inconclusive[""] modifier with an empty reason.

Why is this bad?

The test harness generates the case and skips it. An empty reason gives no explanation or condition for rerunning it, so the case can stay disabled indefinitely.

Known problems

A reason from a constant or macro does not trigger the lint. A whitespace-only reason triggers test-case-whitespace-ignore-reason instead.

Example

use test_case::test_case;

#[test_case(1 => ignore[""]; "one")]
fn check(value: u8) {
    assert_eq!(value, 1);
}

Use instead

use test_case::test_case;

#[test_case(1 => ignore["fails on Windows, see issue #123"]; "one")]
fn check(value: u8) {
    assert_eq!(value, 1);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_matrix(...)] function that generates no test cases because one input set, such as [] or the range 0..0, is empty.

Why is this bad?

The Cartesian product of the input sets is empty, so test-case generates no tests and the function body never runs. cargo test reports no failure.

Known problems

The lint counts the cases of each #[test_matrix(...)] attribute from its array, tuple, and integer-range literals, the same way test-case expands them. A range bound written as a constant fails to expand, so the lint does not report it.

Example

use test_case::test_matrix;

#[test_matrix([], [1, 2])]
fn parses(_input: &str, _expected: u8) {}

Use instead

use test_case::test_matrix;

#[test_matrix(["zero", "one"], [1, 2])]
fn parses(_input: &str, _expected: u8) {}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] or #[test_matrix(...)] attribute that uses panics "", an empty expected panic message.

Why is this bad?

test-case checks that the panic message contains the expected text. Every message contains the empty string, so the case passes on any panic. An unrelated unwrap, index, or overflow panic can pass the case even when the intended failure never happens.

Known problems

A message from a constant or macro does not trigger the lint. A whitespace-only message triggers test-case-whitespace-panic-message instead.

Example

use test_case::test_case;

#[test_case(0 => panics ""; "zero")]
fn reciprocal(value: u8) {
    let _ = 1 / value;
}

Use instead

use test_case::test_case;

#[test_case(0 => panics "attempt to divide by zero"; "zero")]
fn reciprocal(value: u8) {
    let _ = 1 / value;
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] or #[test_matrix(...)] attribute that uses the bare ignore modifier instead of ignore["reason"].

Why is this bad?

The test harness skips the case without recording a reason. Readers cannot tell why the harness skips it or what must change before it can run again, so the skip tends to become permanent.

Known problems

The lint checks only the ignore spelling. A bare inconclusive modifier triggers test-case-inconclusive-modifier instead.

Example

use test_case::test_case;

#[test_case(1 => ignore; "one")]
fn check(value: u8) {
    assert_eq!(value, 1);
}

Use instead

use test_case::test_case;

#[test_case(1 => ignore["blocked by issue #123"]; "one")]
fn check(value: u8) {
    assert_eq!(value, 1);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_matrix(...)] attribute with an ignore or inconclusive modifier in its output.

Why is this bad?

test-case applies the matrix output and its modifiers to every generated case, so the test harness skips every combination. The matrix looks like broad coverage while none of it runs.

Known problems

The lint also warns when a project intentionally skips the whole matrix, for example during a short migration. An ignore modifier without a reason also triggers test-case-ignore-without-reason, and inconclusive also triggers test-case-inconclusive-modifier.

Example

use test_case::test_matrix;

#[test_matrix([1, 2], [true, false] => ignore["issue #123"])]
fn validates(value: u8, strict: bool) {
    assert!(value > 0 || !strict);
}

Use instead

Remove the modifier. Move inputs that cannot run yet into separate #[test_case(...)] attributes with ignore["reason"].

use test_case::test_matrix;

#[test_matrix([1, 2], [true, false])]
fn validates(value: u8, strict: bool) {
    assert!(value > 0 || !strict);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] or #[test_matrix(...)] attribute that uses the inconclusive modifier.

Why is this bad?

test-case generates an ignored test for an inconclusive case. The test harness reports it as ignored, not as a separate inconclusive result. ignore["reason"] has the same effect and uses the name the test output shows.

Known problems

test-case still supports inconclusive, so a project might keep it on purpose. The fix renames the keyword to ignore and keeps any reason.

Example

use test_case::test_case;

#[test_case(1 => inconclusive["issue 123"]; "one")]
fn check(value: u8) {
    assert_eq!(value, 1);
}

Use instead

use test_case::test_case;

#[test_case(1 => ignore["issue 123"]; "one")]
fn check(value: u8) {
    assert_eq!(value, 1);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a function whose #[test_case(...)] and #[test_matrix(...)] attributes generate 64 or more tests in total. A matrix generates one test for each combination of its input sets.

Why is this bad?

Each generated test adds compile time, run time, and lines in the test output. A large Cartesian product often repeats the same behavior with many values, and the cases that matter become hard to find.

Known problems

The lint uses a fixed limit of 64. A suite whose combinations each cover different behavior still triggers the lint. The lint counts tests, not the time each test takes.

Example

use test_case::test_matrix;

#[test_matrix(0..8, 0..8)]
fn adds_commutatively(left: u8, right: u8) {
    assert_eq!(left + right, right + left);
}

Use instead

Keep boundary values that represent the input range. For broad generated input, use a property-testing crate.

use test_case::test_matrix;

#[test_matrix([0, 1, 7], [0, 1, 7])]
fn adds_commutatively(left: u8, right: u8) {
    assert_eq!(left + right, right + left);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] or #[test_matrix(...)] description that starts with the word inconclusive followed by a space, -, or :, such as "inconclusive - blocked by issue #123".

Why is this bad?

Before test-case 2.0, inconclusive in a description marked the case as ignored. Test-case 2.0 removed that behavior. The description is now only a name, so the case runs despite its skip-like name.

Known problems

The lint matches only lowercase inconclusive at the start of the description. A description such as "Inconclusive - ..." does not trigger it, although old test-case versions matched the word in any case.

Example

use test_case::test_case;

#[test_case("letters"; "inconclusive - parser not implemented")]
fn parses(input: &str) {
    assert!(input.parse::<u8>().is_ok());
}

Use instead

Skip the case with the ignore modifier and describe the behavior.

use test_case::test_case;

#[test_case("letters" => ignore["parser support tracked in issue #123"]; "parses letters")]
fn parses(input: &str) {
    assert!(input.parse::<u8>().is_ok());
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an is almost or is almost_equal_to matcher in a #[test_case(...)] or #[test_matrix(...)] output whose precision is a zero or negative numeric literal.

Why is this bad?

test-case checks (actual - expected).abs() < precision. An absolute difference is never less than zero, so the case fails for every finite value, even when the two values are equal.

Known problems

The lint checks only a numeric literal, with or without a leading - or a type suffix. A constant or an arithmetic expression does not trigger it.

Example

use test_case::test_case;

#[test_case(1.0 => is almost 1.0 precision 0.0; "same value")]
fn identity(value: f64) -> f64 {
    value
}

Use instead

Use a positive tolerance. If the values must be exactly equal, use the plain => expected form.

use test_case::test_case;

#[test_case(1.0 => is almost 1.0 precision 0.001; "same value")]
fn identity(value: f64) -> f64 {
    value
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] or #[test_matrix(...)] attribute that uses the bare panics modifier without an expected message.

Why is this bad?

The case passes on any panic. An unrelated unwrap, index, or overflow panic can pass the case even when the intended failure never happens.

Known problems

Some tests only need to check that a function panics, because the message is not stable. The lint warns on those tests too.

test-case 3.3 drops a bare panics that a description follows, as in 1 => panics ; "name", so the generated test no longer expects a panic. The lint reports this form too.

Example

use test_case::test_case;

#[test_case(0 => panics; "zero")]
fn reciprocal(value: u8) {
    let _ = 1 / value;
}

Use instead

use test_case::test_case;

#[test_case(0 => panics "attempt to divide by zero"; "zero")]
fn reciprocal(value: u8) {
    let _ = 1 / value;
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_matrix(...)] function that generates exactly one test, because every input set has one value.

Why is this bad?

test_matrix says that inputs vary. When the matrix generates one test, the inputs do not vary, and #[test_case(...)] states that shape directly.

Known problems

The lint counts the cases of each #[test_matrix(...)] attribute from its array, tuple, and integer-range literals, the same way test-case expands them.

The lint gives help text but no automatic fix.

Example

use test_case::test_matrix;

#[test_matrix(["input"], [1])]
fn parses(_input: &str, _expected: u8) {}

Use instead

use test_case::test_case;

#[test_case("input", 1)]
fn parses(_input: &str, _expected: u8) {}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a contains_in_order matcher in a #[test_case(...)] or #[test_matrix(...)] output whose expected array or tuple has exactly one element, such as contains_in_order [2].

Why is this bad?

Order has no meaning for one element. The case only checks that the element is present, which contains states directly. contains_in_order suggests an order check that does not happen.

Known problems

The lint checks only an array or tuple literal written in the attribute. A constant or variable with one element does not trigger it. The lint gives help text but no automatic fix.

Example

use test_case::test_case;

#[test_case(vec![1, 2] => it contains_in_order [2]; "contains two")]
fn values(input: Vec<u8>) -> Vec<u8> {
    input
}

Use instead

use test_case::test_case;

#[test_case(vec![1, 2] => it contains 2; "contains two")]
fn values(input: Vec<u8>) -> Vec<u8> {
    input
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_matrix(...)] attribute that generates multiple tests and contains an input set written as an array or tuple with exactly one value. For example, ["fixed"] appears in #[test_matrix(["fixed"], [0, 1, 2])].

Why is this bad?

A one-value collection makes a constant input look like a varying one. Readers cannot tell which inputs create combinations or whether someone removed values by mistake. test-case accepts a plain expression for a constant input and generates the same tests. test-case-single-case-matrix covers a matrix that generates only one test.

Known problems

The lint checks only array and tuple literals written in the attribute. A one-value range such as 0..1, a constant, or a macro does not trigger it.

The fix replaces the collection with its value. If that value is itself an array, tuple, or range, the replacement would create a new input set. The lint gives help text but no automatic fix.

Example

use test_case::test_matrix;

#[test_matrix(["fixed"], [0, 1, 2])]
fn parses(input: &str, mode: u8) {
    assert!(!input.is_empty() && mode < 3);
}

Use instead

use test_case::test_matrix;

#[test_matrix("fixed", [0, 1, 2])]
fn parses(input: &str, mode: u8) {
    assert!(!input.is_empty() && mode < 3);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a #[test_case(...)] attribute without a description after a ;, such as #[test_case(1)].

Why is this bad?

Without a description, test-case builds the test name from the argument expressions. These names can be long and unclear in test output, and they change whenever someone rewrites an input expression.

Known problems

A function with many generated cases gets one warning for each attribute.

Example

use test_case::test_case;

#[test_case(1)]
fn accepts_positive(value: u8) {
    assert!(value > 0);
}

Use instead

use test_case::test_case;

#[test_case(1; "minimum positive value")]
fn accepts_positive(value: u8) {
    assert!(value > 0);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an ignore["..."] or inconclusive["..."] modifier in a #[test_case(...)] or #[test_matrix(...)] output whose reason is not empty but contains only whitespace.

Why is this bad?

The test harness skips the case. A blank reason looks like a reason in the source but gives no explanation of the skip or condition for rerunning it. Reviewers can easily miss the blank content.

Known problems

A reason from a constant or macro does not trigger the lint. An empty reason triggers test-case-empty-ignore-reason instead.

Example

use test_case::test_case;

#[test_case(1 => ignore["  "]; "one")]
fn validates(value: u8) {
    assert_eq!(value, 1);
}

Use instead

use test_case::test_case;

#[test_case(1 => ignore["blocked by issue #123"]; "one")]
fn validates(value: u8) {
    assert_eq!(value, 1);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a panics "..." output in a #[test_case(...)] or #[test_matrix(...)] attribute whose expected message contains at least one whitespace character and no other character.

Why is this bad?

test-case checks that the panic message contains the expected text. Most panic messages contain a space, so an unrelated unwrap, index, or overflow panic can pass the case even when the intended failure never happens.

Known problems

A message from a constant or macro does not trigger the lint. An empty message triggers test-case-empty-panic-message instead.

Example

use test_case::test_case;

#[test_case(0 => panics " "; "zero")]
fn reciprocal(value: u8) {
    let _ = 1 / value;
}

Use instead

use test_case::test_case;

#[test_case(0 => panics "attempt to divide by zero"; "zero")]
fn reciprocal(value: u8) {
    let _ = 1 / value;
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a matches output in a #[test_case(...)] or #[test_matrix(...)] attribute whose whole pattern is _ or (_), with or without an if guard.

Why is this bad?

The _ pattern matches every value, so the case passes whatever the function returns. The case cannot detect a change in the result.

Known problems

The lint checks only a pattern that is _ or (_). Other patterns that match every value, such as a binding value or Ok(_) | Err(_), do not trigger it.

Example

use test_case::test_case;

#[test_case(1 => matches _; "one")]
fn wrap(value: u8) -> Option<u8> {
    Some(value)
}

Use instead

use test_case::test_case;

#[test_case(1 => matches Some(1); "one")]
fn wrap(value: u8) -> Option<u8> {
    Some(value)
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a with validator in a #[test_case(...)] or #[test_matrix(...)] output whose whole expression is a function path, such as with validate or with checks::validate.

Why is this bad?

test-case documents with for an inline closure and using for a named validation function. Both run the same check here. Using with for a named validation function makes the case look like it holds an inline assertion and hides the validator's reuse across cases.

Known problems

The lint checks only a path, including a generic path such as validate::<u8>. A call or a parenthesized expression does not trigger it. The fix renames with to using, which calls the same function.

Example

use test_case::test_case;

fn validate(actual: u8) {
    assert_eq!(actual, 4);
}

#[test_case(2 => with validate; "doubles")]
fn double(value: u8) -> u8 {
    value * 2
}

Use instead

use test_case::test_case;

fn validate(actual: u8) {
    assert_eq!(actual, 4);
}

#[test_case(2 => using validate; "doubles")]
fn double(value: u8) -> u8 {
    value * 2
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an #[error(...)] attribute on a type that derives thiserror::Error. It reports a {} placeholder when a positional argument contains only a field name, such as #[error("failed: {}", source)].

Why is this bad?

A positional placeholder hides which field supplies the displayed value. Adding or reordering format arguments can make a placeholder show the wrong field. Thiserror can capture a named field inside the format string, so the extra argument is redundant.

Known problems

The fix moves the field name into the placeholder and deletes the argument. It applies only when the message has exactly one positional placeholder and one positional argument, and the format string has no escape sequences. Messages with several positional placeholders get help text but no fix.

Example

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("failed: {}", source)]
    Operation { source: std::io::Error },
}

Use instead

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("failed: {source}")]
    Operation { source: std::io::Error },
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for an #[error(...)] format string that captures a Path or PathBuf field as {field} when the compiled package's Cargo.toml disables thiserror's default features without enabling its std feature.

Why is this bad?

Thiserror's Path and PathBuf display support requires its std feature. Without it, the derive fails with a missing Display implementation that does not mention the feature.

Known problems

The lint reads the manifest for the package Cargo compiles, including [dependencies.thiserror] tables, [target.*] tables, renamed dependencies, and workspace = true declarations. It still warns when another dependency enables thiserror's std feature through Cargo feature unification.

The lint runs before type checking, because the missing feature makes type checking fail. It therefore recognizes a field type by its written name, so a type alias for PathBuf does not trigger it. It checks only the exact {field} capture; a capture with a format spec needs Display in every configuration.

Example

#[derive(thiserror::Error, Debug)]
#[error("missing {path}")]
pub struct Error {
    path: std::path::PathBuf,
}

Use instead

Enable thiserror's std feature, or format the path with display().

#[derive(thiserror::Error, Debug)]
#[error("missing {}", path.display())]
pub struct Error {
    path: std::path::PathBuf,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a raw identifier placeholder, such as {r#type}, in the format string of an #[error(...)] attribute on a type that derives thiserror::Error.

Why is this bad?

Thiserror 2 rejects raw identifiers in format strings. Code written for thiserror 1 stops compiling after the upgrade. The unraw placeholder {type} refers to the same r#type field.

Known problems

With thiserror 2, an active item with this pattern already fails to compile with thiserror's own error. The lint is most useful on thiserror 1 code before a migration. The UI fixture uses thiserror 1 to preserve that pre-migration behavior.

thiserror 1 cannot format a keyword field such as r#type as {type}, so the lint offers that rewrite as help but does not apply it automatically. A rewrite of a non-keyword name such as {r#kind} to {kind} works with both versions, and the lint applies it automatically. A format string with escape sequences gets help text but no rewrite.

Example

#[derive(thiserror::Error, Debug)]
#[error("bad {r#type}")]
pub struct Error {
    r#type: String,
}

Use instead

After upgrading to thiserror 2, use the unraw placeholder.

#[derive(thiserror::Error, Debug)]
#[error("bad {type}")]
pub struct Error {
    r#type: String,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for #[backtrace] on a field that thiserror already treats as the backtrace. The field must be first in a thiserror::Error struct or variant, have the written type name Backtrace, and not be a source field.

Why is this bad?

Thiserror already uses a field with the written type name Backtrace as the error's backtrace. The attribute changes nothing, and readers might expect it to change behavior. On a source field, #[backtrace] forwards the source's backtrace, so the lint skips fields named source and fields marked #[source] or #[from].

Known problems

thiserror itself reads the written type name, so a type alias for Backtrace is not a backtrace field and does not trigger the lint.

Example

#[derive(thiserror::Error, Debug)]
#[error("failed")]
pub struct Error {
    #[backtrace]
    backtrace: std::backtrace::Backtrace,
}

Use instead

#[derive(thiserror::Error, Debug)]
#[error("failed")]
pub struct Error {
    backtrace: std::backtrace::Backtrace,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a field of a thiserror::Error type that has both #[from] and #[source].

Why is this bad?

Thiserror treats a #[from] field as the error source, so #[source] changes nothing. The extra attribute suggests that #[source] changes how thiserror treats the field, although #[from] already marks it as the source.

Known problems

None known.

Example

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("io")]
    Io(#[from] #[source] std::io::Error),
}

Use instead

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("io")]
    Io(#[from] std::io::Error),
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for #[source] on a field named source in a thiserror::Error struct or variant when no other field has #[source] or #[from].

Why is this bad?

Thiserror treats a field named source as the error source without an attribute, so #[source] changes nothing. The extra attribute suggests that the field would not be the source without it.

Known problems

A field written as the raw identifier r#source is ordinary data for thiserror, so #[source] on it is not redundant and the lint does not warn.

Example

#[derive(thiserror::Error, Debug)]
#[error("failed")]
pub struct Error {
    #[source]
    source: std::io::Error,
}

Use instead

#[derive(thiserror::Error, Debug)]
#[error("failed")]
pub struct Error {
    source: std::io::Error,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a Display placeholder that formats self, such as {self}, {self:>10}, or {} with a self argument, in the format string of an #[error(...)] attribute on a thiserror::Error type.

Why is this bad?

The #[error(...)] message becomes the type's Display implementation. A {self} placeholder makes that implementation call itself, so formatting the error overflows the stack.

Known problems

Rustc's unconditional_recursion lint can report the same attribute.

Example

#[derive(thiserror::Error, Debug)]
#[error("invalid error: {self}")]
pub struct Error;

Use instead

#[derive(thiserror::Error, Debug)]
#[error("invalid error")]
pub struct Error;
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a field named source in a type that derives thiserror::Error when no field has #[source] or #[from] and the field's written type is String, str, &str, char, bool, or a primitive number.

Why is this bad?

Thiserror treats a field named source as the error's source(). When the field holds ordinary data, the derive fails because the type does not implement std::error::Error. Thiserror 2 accepts the raw identifier r#source as an ordinary data field.

Known problems

The lint runs before type checking, because the field makes type checking fail. It therefore reads the written type name: a local type named String that implements Error triggers it, and a type alias for a primitive does not. It does not warn for other data types such as Option<String>.

Example

#[derive(thiserror::Error, Debug)]
#[error("{source} -> {destination}")]
pub struct RouteError {
    source: char,
    destination: char,
}

Use instead

#[derive(thiserror::Error, Debug)]
#[error("{source} -> {destination}")]
pub struct RouteError {
    r#source: char,
    destination: char,
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for a thiserror::Error struct or variant whose only field is its source and whose message only displays that field, such as #[error("{0}")] or #[error("{source}")]. The field counts as the source when it has #[from] or #[source], or when the field name is source.

Why is this bad?

The wrapper displays the inner error's message and also returns the inner error from source(). A reporter that prints the whole error chain then shows the same message twice. #[error(transparent)] forwards both Display and source() to the inner error.

Known problems

The suggested change alters source(). After the change, the wrapper's source() returns the inner error's source instead of the inner error. Code that downcasts the wrapper's source to the inner type stops matching, so the lint never applies the suggestion automatically.

thiserror rejects #[source] on the field of a transparent item, so a field with #[source] gets help without a suggestion. Remove #[source] by hand when you apply the change.

Example

#[derive(thiserror::Error, Debug)]
#[error("{source}")]
pub struct Error {
    #[from]
    source: std::io::Error,
}

Use instead

#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub struct Error {
    #[from]
    source: std::io::Error,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for an #[error(...)] attribute on a tuple struct or tuple variant that derives thiserror::Error. It reports a numeric placeholder such as {0} when an extra format argument has no name.

Why is this bad?

In a tuple type, {0} can mean the first tuple field or the first extra format argument. Thiserror 2 rejects this mix, so code written for thiserror 1 stops compiling after the upgrade. Naming the extra argument removes the ambiguity in both versions.

Known problems

With thiserror 2, an active item with this pattern already fails to compile with thiserror's own error. The lint is most useful on thiserror 1 code before a migration. The UI fixture uses thiserror 1 to preserve that pre-migration behavior. It gives help text but no automatic fix.

Example

fn expected() -> &'static str {
    "expected"
}

#[derive(thiserror::Error, Debug)]
#[error("bad {0} {}", expected())]
pub struct Error(String);

Use instead

fn expected() -> &'static str {
    "expected"
}

#[derive(thiserror::Error, Debug)]
#[error("bad {0} {expected}", expected = expected())]
pub struct Error(String);
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to Tokio's blocking_lock, blocking_lock_owned, blocking_read, blocking_read_owned, blocking_write, blocking_write_owned, blocking_send, and blocking_recv methods whose nearest enclosing function or closure is async.

Why is this bad?

Tokio documents that these methods panic when called within an asynchronous execution context. They exist for synchronous code that shares a Tokio primitive with async code.

Known problems

The lint treats every async body as running inside a Tokio runtime. It warns on a future that another executor polls, where the call does not panic.

The lint stops at the nearest closure. A call inside a synchronous closure that async code defines and calls does not trigger the lint. The call still runs in the async context. Calls inside a synchronous function also do not trigger the lint.

Example

async fn read_value(value: &tokio::sync::Mutex<u8>) -> u8 {
    *value.blocking_lock()
}

Use instead

Await the asynchronous method. If the whole operation is synchronous, move it into a tokio::task::spawn_blocking closure instead.

async fn read_value(value: &tokio::sync::Mutex<u8>) -> u8 {
    *value.lock().await
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to tokio::runtime::Handle::block_on whose nearest enclosing function or closure is async.

Why is this bad?

Tokio documents that Handle::block_on panics when called within an asynchronous execution context. Async code can await the future directly.

Known problems

The lint treats every async body as running inside a Tokio runtime. It warns on a future that another executor polls.

The lint stops at the nearest closure. A call inside a synchronous closure that async code defines and calls does not trigger the lint. Calls inside a synchronous function do not trigger the lint either.

Example

async fn load(handle: &tokio::runtime::Handle) -> u32 {
    handle.block_on(fetch())
}

Use instead

async fn load() -> u32 {
    fetch().await
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to tokio::runtime::Runtime::block_on whose nearest enclosing function or closure is async.

Why is this bad?

Tokio documents that Runtime::block_on panics when called within an asynchronous execution context. Async code can await the future directly.

Known problems

The lint treats every async body as running inside a Tokio runtime. It warns on a future that another executor polls.

The lint stops at the nearest closure. A call inside a synchronous closure that async code defines and calls does not trigger the lint. Calls inside a synchronous function do not trigger the lint either.

Example

async fn load(runtime: &tokio::runtime::Runtime) -> u32 {
    runtime.block_on(fetch())
}

Use instead

async fn load() -> u32 {
    fetch().await
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to tokio::runtime::Runtime::new whose nearest enclosing function or closure is async.

Why is this bad?

Tokio panics when code drops a runtime within an asynchronous execution context. A runtime created and dropped in async code therefore panics when it goes out of scope. Dropping a runtime also blocks until its worker threads stop. The code already runs on a runtime that it can use.

Known problems

The lint warns even when code moves the runtime out of the async body and drops it in synchronous code, where it does not panic.

The lint checks only Runtime::new. A runtime created with tokio::runtime::Builder::build, or returned by another function, does not trigger the lint. A synchronous closure that async code defines and calls does not trigger the lint either.

Example

async fn start() -> std::io::Result<()> {
    let runtime = tokio::runtime::Runtime::new()?;
    runtime.spawn(work());
    Ok(())
}

Use instead

async fn start() -> std::io::Result<()> {
    tokio::spawn(work());
    Ok(())
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to tokio::time::sleep inside the body of a loop, while, or for expression.

Why is this bad?

Each sleep measures its delay from the end of the previous work. Each cycle adds the work time to the delay, so a periodic schedule drifts. tokio::time::interval tracks fixed deadlines and lets you choose what to do with missed ticks.

Known problems

Retry backoff, rate-limit recovery, and polling loops often need a delay measured from the previous try. The lint cannot tell those loops from periodic ones and warns on both.

The lint warns on any sleep call that is lexically inside a loop, including one inside a closure or async block created in the loop. For example, a task spawned once per iteration that sleeps once still triggers the lint.

The lint checks only tokio::time::sleep. Calls to tokio::time::sleep_until do not trigger it.

Example

async fn run(period: std::time::Duration) {
    loop {
        work().await;
        tokio::time::sleep(period).await;
    }
}

Use instead

async fn run(period: std::time::Duration) {
    let mut ticker = tokio::time::interval(period);
    loop {
        ticker.tick().await;
        work().await;
    }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks calls to tokio::task::spawn_blocking when its function argument returns a type that implements Future.

Why is this bad?

Calling such a closure creates a future but does not poll it. spawn_blocking runs the closure on a blocking thread and returns the unpolled future as the task output. The async work never runs unless the caller awaits that returned future.

Known problems

The lint checks the resolved direct call and its inferred result type. It does not track whether the caller later awaits the JoinHandle and polls its future result.

Example

fn start() {
    let _task = tokio::task::spawn_blocking(|| async {
        work().await;
    });
}

Use instead

Spawn async work with tokio::spawn. If the work is blocking, keep the spawn_blocking closure synchronous.

fn start() {
    let _task = tokio::spawn(async {
        work().await;
    });
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for calls to tokio::sync::mpsc::unbounded_channel.

Why is this bad?

The sender of an unbounded channel never waits for capacity. When producers send faster than the consumer receives, queued messages grow until the process runs out of memory. A bounded channel makes producers wait instead.

Known problems

Other code bounds the message count for some channels, and some channels need a sender that can send from synchronous code without waiting. The lint cannot see those bounds and warns on every call.

Example

fn make_queue() {
    let (_tx, _rx) = tokio::sync::mpsc::unbounded_channel::<u8>();
}

Use instead

Use a bounded channel sized for the expected burst, and handle the waiting this adds to producers.

fn make_queue() {
    let (_tx, _rx) = tokio::sync::mpsc::channel::<u8>(64);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks tokio::sync::mpsc::channel and tokio::sync::broadcast::channel calls whose capacities are statically known to be zero. It follows local non-trait constants and same-type checked unsigned +, -, *, /, and % expressions through 16 nested steps.

Why is this bad?

Tokio documents that both constructors panic when the capacity is zero. The mistake is visible in the source but fails only at runtime.

Known problems

The evaluator reports only values it can prove are zero. Runtime locals and function calls remain unknown. Casts, statics, trait or external constants, unsupported operators, and arithmetic that overflows, underflows, divides by zero, or takes remainder by zero also remain unknown.

Example

fn make_queue() {
    let (_tx, _rx) = tokio::sync::mpsc::channel::<u8>(0);
}

Use instead

Use the smallest positive capacity that holds the expected burst.

fn make_queue() {
    let (_tx, _rx) = tokio::sync::mpsc::channel::<u8>(16);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a zero standard-library Duration passed to tokio::time::interval or tokio::time::interval_at. It recognizes Duration::ZERO, Default::default(), and local non-trait constants. It evaluates the standard constructors Duration::new, Duration::from_secs, Duration::from_millis, Duration::from_micros, Duration::from_nanos, Duration::from_secs_f32, and Duration::from_secs_f64. It also evaluates bounded Duration addition and subtraction, multiplication or division by a u32 scalar, and checked u32 or u64 arithmetic used by integer constructors. Float constructors use Duration's own conversion, including its exact nanosecond rounding.

Why is this bad?

Tokio documents that both constructors panic when the period is zero. The mistake is visible in the source but fails only at runtime.

Known problems

The lint leaves runtime variables and parameters, function calls, statics, external constants, trait constants, and unsupported expressions unknown. It also leaves arithmetic unknown when checked evaluation fails, operand types do not match, or evaluation reaches the 16-step limit. Float constructors whose standard conversion returns an error remain unknown. The lint recognizes the resolved standard Duration and Tokio interval functions, not user-defined items with the same names.

Example

fn make_ticker() {
    let _ticker = tokio::time::interval(std::time::Duration::from_secs_f64(1e-20));
}

Use instead

fn make_ticker() {
    let _ticker = tokio::time::interval(std::time::Duration::from_nanos(1));
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks tokio::runtime::Builder::worker_threads and tokio::runtime::Builder::max_blocking_threads calls whose thread count is statically known to be zero, including values resolved from local constants and supported unsigned arithmetic.

Why is this bad?

Tokio documents that both methods panic when the thread count is zero. The mistake is visible in the source but fails only at runtime.

Known problems

The lint resolves integer literals, local non-trait constants, and unsigned +, -, *, /, or % expressions through 16 nested steps. It reports only values it can prove are zero. Runtime locals and function calls remain unknown, as do casts, statics, trait or external constants, unsupported operators, and arithmetic that overflows, underflows, or divides by zero.

Example

fn build_runtime() -> std::io::Result<tokio::runtime::Runtime> {
    tokio::runtime::Builder::new_multi_thread()
        .worker_threads(0)
        .build()
}

Use instead

Use a positive thread count, or remove the call to keep Tokio's default.

fn build_runtime() -> std::io::Result<tokio::runtime::Runtime> {
    tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .build()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks items in the crate root and in modules, and warns when an item starts on the same line where the previous item ends. An item starts at its first outer attribute or doc comment. One line break is enough; a blank line is optional. The machine-applicable fix moves the item to a new line with the same indentation.

Why is this bad?

Two items on one line hide the boundary between them. Readers scanning the left edge of the file miss the second item, and diffs that touch either item show both.

Known problems

The lint skips items inside functions, traits, and impl blocks. It also skips items generated by macros and items removed by cfg.

Example

fn first() {} fn second() {}

Use instead

fn first() {}
fn second() {}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks whether Span::in_scope, tracing::subscriber::with_default, or tracing::dispatcher::with_default returns a Future, regardless of whether its callable argument is a closure, function item, or stored value.

Why is this bad?

These functions set the span or subscriber only while the callable runs. If the caller polls the returned future, that happens after the scope ends, so its events miss the span or subscriber.

Known problems

The lint flags any returned Future, but cannot determine whether or when the caller polls it or whether that context is intended for its events.

Example

async fn work() {}

async fn run(span: tracing::Span) {
    let future = span.in_scope(|| async { work().await });
    future.await;
}

Use instead

Attach the span with Instrument::instrument, or attach a subscriber with WithSubscriber::with_subscriber.

use tracing::Instrument;

async fn work() {}

async fn run(span: tracing::Span) {
    work().instrument(span).await;
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a tracing::span::Entered or tracing::span::EnteredSpan guard that a resolved Span::enter or Span::entered call returns and that an async future retains in its saved state at an .await. The lint follows owned moves through Option, Box::new, tuples, and user-defined struct fields. It preserves nested field paths when Box::new moves a wrapper such as Box<Option<EnteredSpan>>. It checks each suspension separately: releasing a guard after one .await does not erase that warning, and the release can keep later .await points quiet.

Why is this bad?

While the future waits at the .await, the span stays entered. Other tasks then run on the same thread inside that span, and tracing records their events under it. Tracing documents this pattern as producing incorrect traces.

Known problems

The analysis joins predecessor ownership and alias facts at each MIR block. It can report a guard when it cannot prove that every path released the guard before suspension.

When Option::take uses an alias that can refer to several wrappers, the analysis transfers a possible result and keeps each source as may-live. It can warn when branch conditions would prove that the guard was removed. An unresolved receiver alias also leaves the source as may-live; a single known source is cleared.

The analysis follows at most sixteen aggregate fields and sixteen recursive wrapper types. A guard at either limit can trigger the lint. Deeper wrappers stay quiet because the analysis stops at those bounds.

The type walk stops when it revisits an active recursive type, then checks sibling fields.

For an unrecognized helper that consumes a guard by value, the analysis clears the input and does not infer ownership from the helper's return value. It can miss a guard that such a helper returns inside a wrapper.

For an unrecognized helper that mutably borrows a tracked wrapper, the analysis marks that wrapper's ownership as unknown and stops tracking it. A helper that leaves the guard in place can therefore hide a guard from the lint. If the analysis cannot resolve the mutable alias, it keeps the prior ownership state and can warn after the helper releases the guard.

The analysis does not follow guards through arrays, slices, references, raw pointers, trait objects, closures, coroutine or future captures, generic parameters, or unresolved associated-type projections. It recognizes Box::new and resolved Option::take; opaque container helpers remain unsupported.

Example

struct Request {
    guard: Option<tracing::span::EnteredSpan>,
}

async fn other_work() {}

async fn work(span: tracing::Span) {
    let _request = Request {
        guard: Some(span.entered()),
    };
    other_work().await;
}

Use instead

Use Span::in_scope for synchronous work, or instrument the future.

use tracing::Instrument;

async fn other_work() {}

async fn work(span: &tracing::Span) {
    other_work().instrument(span.clone()).await;
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for enter called directly on tracing::Span::current().

Why is this bad?

Span::current() returns the current span. Entering it again does not change which span is current, so the guard does nothing. The guard also suggests a scope boundary that does not exist.

Known problems

The lint only checks a direct Span::current().enter() chain. It does not follow the span through a local binding, a field, or another function.

Example

fn handle() {
    let _ = tracing::Span::current().enter();
    work();
}

Use instead

Remove the guard:

fn handle() {
    work();
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for or_current called directly on tracing::Span::current().

Why is this bad?

Span::or_current returns the current span for a disabled receiver. The receiver is already the current span, so the call always returns its receiver and only adds noise.

Known problems

The lint only checks a direct Span::current().or_current() chain. It does not follow the span through a local binding, a field, or another function.

Example

fn current_span() -> tracing::Span {
    tracing::Span::current().or_current()
}

Use instead

Remove the or_current call:

fn current_span() -> tracing::Span {
    tracing::Span::current()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a direct call to the tracing::Span::record_all method in source code that no macro produces.

Why is this bad?

Tracing 0.1.42 removed Span::record_all from the documented API and recommends calls through tracing macros. Its ValueSet argument has no documented constructor, so a direct call depends on tracing internals that can change in any release. The tracing::record_all! macro records several fields through the supported API.

Known problems

The lint skips every call produced by a macro expansion, including calls in your own macro_rules! macros. It does not follow calls made through another function.

Example

fn record(span: &tracing::Span) {
    if let Some(metadata) = span.metadata() {
        span.record_all(&tracing::valueset!(metadata.fields(), answer = 42));
    }
}

Use instead

fn record(span: &tracing::Span) {
    tracing::record_all!(span, answer = 42);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a field in a tracing event or span macro whose value is a format! call, such as request = format!("{request_id}:{status}").

Why is this bad?

format! allocates a String every time the macro runs, even before a subscriber decides whether to record the event. It also joins several values into one text field, so a subscriber cannot filter or group by each value without parsing the text. Tracing can record each value as its own field and format it with the % (Display) or ? (Debug) sigil only when needed.

Known problems

Some fields must hold a composed string, for example to match an external log schema. The lint warns on those fields too.

The lint checks only a field value that is exactly format!(...), std::format!(...), or ::std::format!(...). In an event macro, it checks only fields written before the message. It gives help text but no automatic fix.

Example

fn handle(request_id: u64, status: &str) {
    tracing::info!(request = format!("{request_id}:{status}"));
}

Use instead

fn handle(request_id: u64, status: &str) {
    tracing::info!(request_id, %status);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for Instrument::instrument called with a direct tracing::Span::current() argument.

Why is this bad?

Instrument::in_current_span does the same thing. The longer form makes readers inspect the argument to learn that the future uses the current span.

Known problems

The lint only checks a direct Span::current() argument. It does not follow the span through a local binding, a field, or another function.

Example

use tracing::Instrument as _;

async fn work() {}

fn spawn_work() {
    let _task = work().instrument(tracing::Span::current());
}

Use instead

Call in_current_span:

use tracing::Instrument as _;

async fn work() {}

fn spawn_work() {
    let _task = work().in_current_span();
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a variable or field path that appears in the message of a tracing event macro, such as info! or event!. It reports the path when the same event has no structured field for it. It covers captured arguments such as {request_id} and positional arguments such as "request {} failed", request_id.

Why is this bad?

A value that appears only in the message becomes part of one text field. Subscribers cannot filter, group, or export it as a named field without parsing the message, and that parsing breaks when the wording changes.

Known problems

The lint checks only variables and dotted field paths. A function call or other expression in the message does not trigger it. It does not check span macros such as info_span!.

It gives help text but no automatic fix.

Example

fn complete(request_id: u64) {
    tracing::info!("request {request_id} completed");
}

Use instead

Record the value as a field. A message can still repeat a value that the event also records as a field.

fn complete(request_id: u64) {
    tracing::info!(request_id, "request completed");
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for enter called directly on tracing::Span::none().

Why is this bad?

Span::none() returns a disabled span. Entering it records nothing and does not change the current span, so the guard does nothing. The guard also suggests that the work runs inside a span when it does not.

Known problems

The lint only checks a direct Span::none().enter() chain. It does not follow the span through a local binding, a field, or another function.

Example

fn handle() {
    let _ = tracing::Span::none().enter();
    work();
}

Use instead

Remove the guard. If the work needs a span, enter an enabled span instead:

fn handle() {
    work();
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for or_current called directly on tracing::Span::none().

Why is this bad?

Span::or_current returns the current span for a disabled receiver. Span::none() always returns a disabled span, so the expression always returns Span::current(). The extra call hides that fixed result.

Known problems

The lint only checks a direct Span::none().or_current() chain. It does not follow the span through a local binding, a field, or another function.

Example

fn current_span() -> tracing::Span {
    tracing::Span::none().or_current()
}

Use instead

Call Span::current():

fn current_span() -> tracing::Span {
    tracing::Span::current()
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for record called directly on tracing::Span::none().

Why is this bad?

Span::none() returns a disabled span with no fields. Recording a value on it sends nothing to the subscriber, so no subscriber receives the value. The call suggests that the value reaches the trace when it does not.

Known problems

The lint only checks a direct Span::none().record(...) chain. It does not follow the span through a local binding, a field, or another function.

Example

fn handle(value: u64) {
    tracing::Span::none().record("field", value);
}

Use instead

Remove the call. If the value belongs in the trace, declare the field on an enabled span and record it there:

fn handle(value: u64) {
    let span = tracing::info_span!("request", field = tracing::field::Empty);
    span.record("field", value);
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a field written as name = name in a tracing event or span macro, such as info! or debug_span!. Dotted paths such as user.id = user.id and values with the % or ? sigil, such as user = ?user, also match.

Why is this bad?

Tracing macros accept a local variable or field path as shorthand for a field with the same name. Repeating the path adds noise and hides the fields that rename or transform a value.

Known problems

The lint compares source text after removing whitespace. It matches only identifiers and dotted paths, so name = *name or name = self.name does not trigger it.

When a reported field contains a comment, the lint gives help text but no automatic fix.

Example

fn handle(request_id: u64, user: &User) {
    tracing::info!(request_id = request_id, user.id = user.id);
    let _span = tracing::debug_span!("request", user = ?user);
}

Use instead

Keep the % or ? sigil before the shorthand field.

fn handle(request_id: u64, user: &User) {
    tracing::info!(request_id, user.id);
    let _span = tracing::debug_span!("request", ?user);
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Reports direct chains of Instrument::instrument(span) followed by in_current_span(), with advice to use span.or_current() when the inner span is disabled.

Why is this bad?

Instrument::instrument and Instrument::in_current_span each attach a span that is entered whenever the instrumented future is polled or dropped. See the tracing 0.1.44 Instrument documentation. Span::or_current selects the current span only when the supplied span is disabled. See the tracing 0.1.44 Span::or_current documentation.

When outer is current and an enabled parentless inner span is used, each poll and drop of the original chain calls on_enter(outer), on_enter(inner), on_exit(inner), then on_exit(outer). When those same spans are used with instrument(inner.or_current()), each poll and drop calls only on_enter(inner) and on_exit(inner). The recording subscriber test asserts both callback sequences.

Known problems

The lint cannot determine whether the runtime subscriber enables the inner span, so it reports a direct chain even when that span is enabled. If outer is current and an enabled parentless inner span is used, replacing the chain with instrument(inner.or_current()) removes on_enter(outer) and on_exit(outer) on each poll and drop.

The lint checks the direct method chain. It does not follow an instrumented future through a local binding or another function.

Example

use tracing::{Instrument, info_span};

async fn work() {}

fn drop_inner() {
    let _outer = info_span!("outer").entered();
    let _future = work()
        .instrument(info_span!(parent: None, "inner"))
        .in_current_span();
}

Use instead

If the subscriber disables the inner span, pass it through or_current() before instrument:

use tracing::{Instrument, info_span};

async fn work() {}

fn drop_inner() {
    let _outer = info_span!("outer").entered();
    let inner = info_span!(parent: None, "inner");
    let _future = work().instrument(inner.or_current());
}

Otherwise, keep the original chain when the captured current span's enter and exit callbacks are required. Remove the chain only when you intend to remove those callbacks.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a field in a tracing event or span macro whose value is ToString::to_string called on a variable or field path, such as error = error.to_string().

Why is this bad?

to_string allocates a String every time the macro runs, even before a subscriber decides whether to record the event. The % sigil records the same Display output without the allocation.

Known problems

The lint checks only a receiver that is a variable or a dotted field path, so error().to_string() does not trigger it. An inherent to_string method that is not ToString::to_string does not trigger it either. In an event macro, it checks only fields written before the message. It gives help text but no automatic fix.

Example

fn report(error: std::io::Error) {
    tracing::warn!(error = error.to_string(), "request failed");
}

Use instead

fn report(error: std::io::Error) {
    tracing::warn!(%error, "request failed");
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for a pub inherent associated function named new without self. Its body must be only a struct literal that moves each parameter into a field of the same name. The struct must be pub and not #[non_exhaustive], and every field must be pub.

Why is this bad?

Callers can already build the value with User { id, name }. The constructor adds a public function that the crate must document and support, and it does not enforce any invariant that the struct literal skips.

Known problems

  • Removing a public new breaks callers that use it. Keep it when the crate's API must stay stable.
  • The lint checks the declared pub visibility only. A pub struct in a private module still triggers.
  • The lint ignores fields marked pub(crate) or private, tuple structs, enum variants, trait methods, and literals with non-shorthand fields such as name: name.trim().to_owned().
  • The lint ignores constructors generated by macros.

Example

pub struct User {
    pub id: UserId,
    pub name: String,
}

impl User {
    pub fn new(id: UserId, name: String) -> Self {
        Self { id, name }
    }
}

Use instead

pub struct User {
    pub id: UserId,
    pub name: String,
}

fn build(id: UserId, name: String) -> User {
    User { id, name }
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Adds up the Cyclomatic Complexity of every method in one impl block and warns when the sum exceeds 40.

The lint checks inherent and trait impl blocks separately, and each block gets its own diagnostic. A method without branches contributes 1. Associated functions without self count as methods. The diagnostic reports the sum and the method count. The limit of 40 is half of the class-level default in PMD's Cyclomatic Complexity rule. One Rust impl block is usually smaller than a Java class.

Why is this bad?

The per-function lints catch one hard method, but not a type that collects many medium-sized ones. Ten methods with a score of 5 each put 50 independent paths behind one type. Such a type is hard to change without touching several policies, and its tests need broad setup because unrelated behavior shares state.

Known problems

Splitting one impl block into several blocks silences the lint without reducing what the type does. Closures inside methods, trait default methods, and methods generated by a macro do not count. A facade that coordinates several collaborators can legitimately exceed the limit.

Example

struct Busy;

impl Busy {
    fn one(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn two(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn three(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn four(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn five(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn six(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
}

Six methods with a score of 9 each sum to 54.

Use instead

Move a group of methods that owns separate state or policy to its own type.

struct Intake;

impl Intake {
    fn one(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn two(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn three(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
}

struct Output;

impl Output {
    fn four(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn five(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
    fn six(&self, v: [bool; 8]) {
        if v[0] {} if v[1] {} if v[2] {} if v[3] {} if v[4] {} if v[5] {} if v[6] {} if v[7] {}
    }
}

struct Busy {
    intake: Intake,
    output: Output,
}

Interpretation and sources

Chidamber and Kemerer defined six object-oriented design metrics:

  • Weighted Methods per Class, or WMC, sums method weights. Cyclomatic Complexity is now a common weight.
  • Depth of Inheritance Tree, or DIT, measures the longest path to a root class.
  • Number of Children, or NOC, counts immediate subclasses.
  • Coupling Between Object classes, or CBO, counts other classes coupled to a class.
  • Response For a Class, or RFC, counts methods that can execute in response to a message to the class.
  • Lack of Cohesion of Methods, or LCOM, measures how little methods share state.

WMC, coupling, response-set size, and cohesion have Rust analogues, but the implementation must choose the unit. A struct plus its inherent and trait impl blocks is one possible unit. A module is often more useful because Rust permits free functions and separates data from behavior. DIT and NOC do not transfer cleanly because trait implementation and supertrait relationships are not class inheritance.

This lint's type-method unit differs from a Java class. The local profile controls included methods and aggregation. Chidamber and Kemerer's paper.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for Result::map_err calls whose mapper only converts the error with From or Into. The resulting error type must equal the enclosing function's Result error type, and that type must implement From for the original error. Accepted mappers are Into::into, From::from, Error::from, |e| Error::from(e), and |e| e.into().

Why is this bad?

The ? operator already converts the error with From. The extra map_err adds a call that a reader must check, only to find that it changes nothing that ? would not change.

Known problems

  • The lint offers a machine-applicable fix only when ? directly follows the call. In a tail expression such as raw.parse::<u16>().map_err(Error::from), or inside Ok(..) without ?, the lint gives help without a fix. Rewrite these calls by hand as Ok(raw.parse::<u16>()?).
  • A call inside a macro body, or whose receiver comes from a macro call, gets help without a fix.
  • ? cannot infer the target of map_err(From::from), map_err(Into::into), or map_err(|e| e.into()), so these mappers trigger only without ?.
  • The lint skips calls inside closures and async function bodies.
  • The lint ignores mappers with any other body, such as a closure that adds context or runs a statement before the conversion.

Example

fn parse_port(raw: &str) -> Result<u16, Error> {
    let port = raw.parse::<u16>().map_err(Error::from)?;
    Ok(port)
}

Use instead

fn parse_port(raw: &str) -> Result<u16, Error> {
    let port = raw.parse::<u16>()?;
    Ok(port)
}
Applicability: MachineApplicable(?)
Related IssuesView Source

What it does

Checks for module files named mod.rs that are the only file in their directory, including its subdirectories. Empty subdirectories do not count as files.

Why is this bad?

The directory adds a level to the source tree but holds nothing beside mod.rs. Readers open a directory to find one file, and editor tabs show mod.rs instead of the module name. A plain worker.rs holds the same module.

Known problems

The lint reads the directory from disk. Any other entry keeps the directory, including non-Rust files such as README.md or test data, symbolic links, and entries it cannot read. It checks only mod.rs files compiled into the crate under the package directory, and never the crate root.

It also warns when mod.rs reaches the large_rust_file limits. In that case the help text says to keep the directory and split the module into child modules instead of moving it.

Example

The abbreviated snippets require their separate module files and omitted implementation.

// src/lib.rs
mod worker;

// src/worker/mod.rs, the only file in src/worker/
pub fn run() {}

Use instead

Move the module to a file named after it and remove the directory:

// src/lib.rs
mod worker;

// src/worker.rs
pub fn run() {}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for pub structs, enums, and type aliases whose name appears nowhere in the crate's source except their own declaration. It runs only in unpublished crates: the nearest Cargo.toml sets publish = false or publish = [], directly or through publish.workspace = true and a workspace with publish = false.

Why is this bad?

A pub type looks like part of an interface that other code depends on. Readers keep it, document it, and update it on refactors, while nothing in the crate uses it.

Known problems

The lint counts names in the crate's Rust files under the crate root directory. It also counts every .rs file under the nearest workspace root, or under the package directory when there is no workspace. It skips target directories and hidden entries. A mention in a comment, a string, or an unrelated item with the same name counts as a use, so the lint stays silent. A mention from another workspace crate also counts as a use, so the lint stays silent. The lint does not see a crate outside the workspace that uses the type.

It skips types that have a doc comment or an allow, expect, cfg, cfg_attr, test, repr, no_mangle, export_name, used, link_name, or wasm_bindgen attribute. It does not check pub(crate) types, and it does not recognize the inline form publish = { workspace = true }.

Example

// In a crate with `publish = false`
pub struct InternalPayload {
    value: String,
}

pub fn run() {}

Use instead

// In a crate with `publish = false`
pub fn run() {}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for named fields whose type is String or &str and whose name contains a URL word. The words are url, uri, endpoint, endpoints, webhook, and link, matched between underscores and with case.

Why is this bad?

A string field accepts any text, so a malformed URL moves through the program until a request fails far from where the value entered. A URL type parses once, at the boundary, and gives typed access to the scheme, host, and path.

Known problems

The compiler resolves type aliases before the lint examines the type. The lint peels up to eight consecutive standard Option layers at each point in its traversal. Longer chains, local Option lookalikes, and user-defined wrappers remain opaque. After peeling, it checks only String and &str, including type aliases and use renames. It does not inspect Vec<String>, Box<str>, or Cow<'_, str>.

It skips a field whose last word is description, format, label, name, pattern, prefix, suffix, template, text, or title, such as url_label or link_text. It still warns on other text that is not a URL, such as endpoint_kind. It does not flag other URL names, such as href or base.

Example

struct ServiceConfig<'a> {
    callback_url: String,
    endpoint: &'a str,
}

Use instead

use url::Url;

struct ServiceConfig {
    callback_url: Url,
    endpoint: Url,
}
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks for resolved Vec::remove(0) calls inside loops that may execute more than once. It follows one direct local receiver binding declared outside the loop and recognizes the standard Vec and a literal zero index. It skips element types whose resolved layout is zero-sized.

Why is this bad?

Removing the first element shifts every remaining element to preserve order. If a loop drains the same vector through repeated front removals, total shifts can grow quadratically.

Known problems

The lint does not follow calls into helper functions, closure bodies, vector fields, or computed receiver expressions. It skips bindings declared or directly assigned inside the loop. It also skips a direct final break, literal singleton arrays, and standard literal ranges with bounds that prove at most one item. Other iterators and control flow may still execute at most once. Keep contiguous storage when the workload requires it. The lint does not infer a Vec's current length from its initializer or earlier control flow, so it can warn when a loop removes a one-element vector once, even though that removal shifts no tail elements.

When the element layout is unavailable, including a generic T, the lint keeps the warning. Such a generic function can be instantiated with a zero-sized type.

Example

fn drain_front(mut values: Vec<String>) {
    while !values.is_empty() {
        values.remove(0);
    }
}

Use instead

fn drain_front(values: Vec<String>) {
    values.into_iter().for_each(drop);
}

When this loop repeatedly removes elements, consume the vector or use VecDeque::pop_front if it must remain a queue.

Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the nearest Cargo.toml above the crate root file of a workspace package for dependencies that set their own version, path or git source instead of using workspace = true. It checks [dependencies], [dev-dependencies], [build-dependencies], and their [target.'...'] variants. The warning points at the source value. When a dependency declares both version and path, the version diagnostic takes precedence.

The lint applies only when the workspace root declares [workspace.dependencies]. The workspace root is the package manifest itself when it has a [workspace] table. Otherwise, an explicit package.workspace path, resolved relative to the package manifest directory, selects the root. When that key is absent, the root is the nearest ancestor Cargo.toml with a [workspace] table that does not exclude the package.

Why is this bad?

When each package sets its own versions, one workspace can depend on several versions of the same crate. Upgrades and audits then need an edit in every manifest instead of one entry in [workspace.dependencies].

Known problems

The lint reports explicit sources even when the workspace has no entry for that dependency. Move the source into [workspace.dependencies] first.

Cargo validates malformed dependency values. This lint reports string version, path and git values.

A package that needs two versions of one crate must give each version its own renamed key in [workspace.dependencies], such as bevy_018 = { package = "bevy", version = "0.18.0" }.

Example

[package]
name = "example"
version = "0.1.0"
edition = "2024"

[workspace]

[workspace.dependencies]
anyhow = "1.0.0"

[dependencies]
anyhow.workspace = true
serde = "1.0.0"

Use instead

[package]
name = "example"
version = "0.1.0"
edition = "2024"

[workspace]

[workspace.dependencies]
anyhow = "1.0.0"
serde = "1.0.0"

[dependencies]
anyhow.workspace = true
serde.workspace = true
Applicability: NotMachineApplicable(?)
Related IssuesView Source

What it does

Checks the nearest Cargo.toml above the crate root file of a workspace package. If the manifest defines lints without workspace = true and the workspace root declares [workspace.lints], the lint warns at the package's lints key or table header. A [lints] table, a [lints.*] table, a dotted key, and an inline table all count.

The workspace root is the package manifest itself when it has a [workspace] table. Otherwise, an explicit package.workspace path, resolved relative to the package manifest directory, selects the root. When that key is absent, the root is the nearest ancestor Cargo.toml with a [workspace] table that does not exclude the package.

If an explicit path cannot select a workspace root, the lint does not search ancestor workspaces.

Why is this bad?

A package with its own lint table does not inherit [workspace.lints]. Changes to the shared lint policy then miss that package without any warning.

Known problems

Manifests with no lints key do not warn; package_lints_section reports them.

Example

[package]
name = "example"
version = "0.1.0"
edition = "2024"

[workspace]

[workspace.lints.rust]
unsafe_code = "forbid"

[lints.rust]
unsafe_code = "forbid"

Use instead

[package]
name = "example"
version = "0.1.0"
edition = "2024"

[workspace]

[workspace.lints.rust]
unsafe_code = "forbid"

[lints]
workspace = true
Applicability: NotMachineApplicable(?)
Related IssuesView Source