perfectionist lints

perfectionist is a Dylint plugin; see the README for setup. Lint-control attributes use the perfectionist:: namespace.

Index

LintDefaultDescription
arc_rc_cloneWarncalling .clone() on an Arc<T> or Rc<T>; prefer the qualified Arc::clone / Rc::clone form
derive_orderingWarntrait names in a #[derive(...)] list are not in the configured order
flat_module_patternWarnsubmodule defined as module/mod.rs; prefer the flat module.rs layout
macro_argument_bindingWarnmacro invocation passes a non-trivial expression that should be bound to a let first
macro_trailing_commaWarnmacro invocation does not follow rustfmt's vertical trailing-comma policy
non_exhaustive_errorAllowerror-shaped type is missing #[non_exhaustive]
prefer_raw_stringWarnstring literal contains only raw-expressible escapes; prefer the raw-string form
single_letter_closure_paramWarnclosure parameter has a single-letter name
single_letter_function_paramWarnfunction parameter has a single-letter name
single_letter_genericWarngeneric type parameter has a single-letter name
single_letter_let_bindingWarnlet binding has a single-letter name
unicode_ellipsis_in_commentsWarnU+2026 HORIZONTAL ELLIPSIS in non-doc comments; prefer ...
unicode_ellipsis_in_panic_messagesWarnU+2026 HORIZONTAL ELLIPSIS in panic / assertion / expect messages; prefer ...
unknown_perfectionist_lintsWarnlint-control attribute references a perfectionist::* lint that this plugin does not register

Rules

perfectionist::arc_rc_clone

Warncalling .clone() on an Arc<T> or Rc<T>; prefer the qualified Arc::clone / Rc::clone form

What it does

Flags value.clone() where value is an Arc<T> or Rc<T>, and suggests rewriting it as the qualified Arc::clone(...) / Rc::clone(...) form. For a receiver of type Arc<T> / Rc<T>, the rewrite is Arc::clone(&value); for a receiver already typed as &Arc<T> / &Rc<T>, the leading & is dropped (Arc::clone(value)) so the result doesn't form a stutter-borrow &&Arc<T>.

The qualified form is accepted in every shape: the bare Arc::clone(...), the turbofish-typed Arc::<T>::clone(...), and the UFCS <Arc<T> as Clone>::clone(...) are all left untouched. The lint targets only the method-call shape, which reads as a generic Clone call rather than the cheap refcount bump it actually is.

Why restrict this?

This is a stylistic preference, not a correctness issue. Arc<T> and Rc<T> implement Clone precisely so the method call compiles; the practice forbidden here is the method-call dispatch (value.clone()), where the impl Rust picks depends on the receiver's type. The accepted qualified forms — bare (Arc::clone(&value)), turbofish (Arc::<T>::clone(&value)), and UFCS (<Arc<T> as Clone>::clone(&value)) — all name the impl at the call site, so the cost is visible to the reader. Two reasons to prefer them:

Example

fn spawn_worker(state: std::sync::Arc<State>) {
    let copy = state.clone();
    thread::spawn(move || work(copy));
}

Use instead:

fn spawn_worker(state: std::sync::Arc<State>) {
    let copy = std::sync::Arc::clone(&state);
    thread::spawn(move || work(copy));
}

Configuration: none.

Source: src/rules/arc_rc_clone.rs

perfectionist::derive_ordering

Warntrait names in a #[derive(...)] list are not in the configured order

What it does

Enforces a project-wide ordering of trait names inside a single #[derive(...)] list. Three styles are configurable via style:

Trait matching is by the final path segment, so serde::Deserialize is matched as Deserialize. The lint does not police how derives are partitioned across multiple #[derive(...)] lines — that's a layout decision left to the author.

Why restrict this?

This is a stylistic preference, not a correctness issue. The trait order inside #[derive(...)] has no semantic effect: #[derive(Debug, Clone)] and #[derive(Clone, Debug)] produce identical impls. A project-wide convention makes derive lists scan uniformly across the codebase. cargo fmt does not reorder derives, so this lint is the only mechanism for enforcing one.

Example

Under style = "alphabetical":

#[derive(Debug, Clone, Copy)]
struct Point;

Use instead:

#[derive(Clone, Copy, Debug)]
struct Point;
Configuration

Configure via dylint.toml under ["perfectionist::derive_ordering"].

style : Style optional

Ordering policy. Defaults to preserve, which is a no-op; a project opts in by setting alphabetical or prefix_then_alphabetical.

prefix : [string] optional

Trait names that must appear first under the prefix_then_alphabetical style, in the order they should appear. Ignored under other styles. Matched by the final path segment, so a configured "Debug" matches both Debug and std::fmt::Debug written in the source.

Types

Style enum

"preserve" (Rust: Preserve)

No-op. The lint emits nothing.

"alphabetical" (Rust: Alphabetical)

Every trait name must appear in ASCII-case-insensitive alphabetical order.

"prefix_then_alphabetical" (Rust: PrefixThenAlphabetical)

Traits listed in the configured prefix come first, in the listed order; remaining traits are sorted alphabetically after.

Source: src/rules/derive_ordering.rs

perfectionist::flat_module_pattern

Warnsubmodule defined as module/mod.rs; prefer the flat module.rs layout

What it does

Forbids the module/mod.rs layout for submodules. Each submodule should be defined by a sibling file named after the module (module.rs), with any nested children placed inside the module/ directory next to it.

Why restrict this?

This is a stylistic preference, not a correctness issue. The flat layout keeps the file name unique to its module, so editors, terminal tabs, and grep results identify the module without their parent directory. The mod.rs form produces dozens of identically-named tabs in editors that don't disambiguate by directory.

Example

// Bad
src/foo/mod.rs

// Good
src/foo.rs
src/foo/bar.rs

Configuration: none.

Source: src/rules/flat_module_pattern.rs

perfectionist::macro_argument_binding

Warnmacro invocation passes a non-trivial expression that should be bound to a let first

What it does

Flags non-trivial expressions passed as top-level arguments to a function-like (name!(...)) or array-like (name![...]) macro invocation. The fix is to bind the expression to a let first and pass the binding instead, guaranteeing exactly-once evaluation.

Curly-brace invocations (name! { ... }) are out of scope: by convention they are DSL bodies (thread_local! { ... }, quote! { ... }, html! { ... }) where the evaluation contract is the macro's, not the call site's.

Why is this bad?

A function-like or array-like macro may evaluate any top-level argument zero, one, or many times depending on its matcher. Functions guarantee exactly-once evaluation per argument; macros do not, even when the call shape looks identical. The classic case is debug_assert_eq!:

debug_assert_eq!(map.insert(key, value), None, "duplicate");

In debug builds the call runs and the assertion holds. In release builds debug_assertions is off, the body folds to if false { ... }, and the argument expressions are not evaluated — insert never runs and the map ends the function in a state the author did not intend. The bug only surfaces under --release.

The same trap covers any macro that expands its capture more than once (min!/max!-style, retry loops): a side-effecting expression repeated produces wrong results.

Example

debug_assert_eq!(map.insert(key, value), None, "duplicate");

Use instead:

let ejected = map.insert(key, value);
debug_assert_eq!(ejected, None, "duplicate");

Source: src/rules/macro_argument_binding.rs

perfectionist::macro_trailing_comma

Warnmacro invocation does not follow rustfmt's vertical trailing-comma policy

What it does

For function-like macro invocations whose top-level arguments are comma-separated, enforces rustfmt's trailing_comma = "Vertical" policy that rustfmt itself does not apply inside macro bodies: multi-line invocations must end with a trailing comma; single-line invocations must not.

Eligibility is name-based — a curated list of core / std and well-known third-party macros (vec!, format!, println!, assert_eq!, dbg!, log::info!, tracing::debug!, anyhow::bail!, maplit::hashmap!, …), extended via extra_name_based and overridden via ignore.

Attribute-style invocations (#[derive(...)], #[serde(...)], etc.) are out of scope.

Why restrict this?

This is a stylistic preference, not a correctness issue. rustfmt's default trailing_comma = "Vertical" policy keeps argument lists uniform: every multi-line list ends with a comma, every single-line list does not. rustfmt opts out of macro bodies because a macro matcher can make the trailing comma load-bearing; for the curated macros covered by this lint, it cannot, and the policy applies without risk.

Multi-line invocations whose first top-level token starts on the opening-delimiter line (visual-indent / compact layout, e.g. vec![Inner { ... }]) are skipped: rustfmt's Vertical policy only adds a trailing comma when each top-level item is on its own line, separate from the delimiter, and strips any comma added to the compact shape. The two tools have to agree.

Example

let xs = vec![
    1,
    2,
    3
];
let ys = vec![1, 2, 3,];

Use instead:

let xs = vec![
    1,
    2,
    3,
];
let ys = vec![1, 2, 3];
Configuration

Configure via dylint.toml under ["perfectionist::macro_trailing_comma"].

enabled : boolean optional

Master on/off switch for the rule. Defaults to true. Set to false to silence every diagnostic this lint would emit without having to enumerate every macro under ignore.

matcher_based : boolean optional

Accepted for forward compatibility with the matcher-based half of the rule. Currently a no-op — only name-based eligibility is implemented; see planned-rules/macro-trailing-comma.md for the status breakdown.

extra_name_based : [string] optional

Additional macro paths to treat as name-based eligible, on top of the curated built-in list. Each entry is matched by its final path segment, so "my_crate::vec_like" and "vec_like" both target invocations whose last segment is vec_like. Empty by default. Only add macros whose trailing comma is syntactically optional at the top level; macros that treat the comma as a fully optional separator throughout (rather than only at the tail) should not be listed here.

ignore : [string] optional

Macro paths to opt out of the rule, even if they would otherwise be eligible via the built-in list or extra_name_based. Matched by final path segment, like extra_name_based. Checked first, so this knob always wins over eligibility. Empty by default.

Source: src/rules/macro_trailing_comma.rs

perfectionist::non_exhaustive_error

Allowerror-shaped type is missing #[non_exhaustive]

What it does

Flags publicly-exposed error enums that lack a #[non_exhaustive] attribute. An enum is treated as an error enum when its name ends in Error (configurable) or it implements std::error::Error. Publicly-exposed sum-like structs (a single field whose type is itself an enum) follow the same rule.

"Publicly-exposed" defaults to pub items; pub(crate) and the whole-crate "every item" sweep are configurable.

Why restrict this?

This is a stylistic preference, not a correctness issue. Adding a variant to an error enum is one of the most common reasons to publish a new minor version of an error-producing library, and #[non_exhaustive] is the standard way to make that addition not a SemVer break for downstream pattern matches. Applying it up front means future variants land without a coordinated major release across the dependents that exhaustively match on the enum.

The opinion is opt-in: some projects deliberately use exhaustive error enums to force downstream consumers to handle every new variant, and binary crates have no SemVer surface to protect. The lint therefore defaults to Allow — enable it per crate with #![warn(perfectionist::non_exhaustive_error)] (or deny) on projects that want it.

Example

#[derive(Debug)]
pub enum RuntimeError {
    SerializationFailure,
}

Use instead:

#[derive(Debug)]
#[non_exhaustive]
pub enum RuntimeError {
    SerializationFailure,
}
Configuration

Configure via dylint.toml under ["perfectionist::non_exhaustive_error"].

require_for : RequireFor optional

Visibility threshold for the rule.

suffixes : [string] optional

Identifier suffixes that mark a type as "an error" purely by name, without inspecting its trait implementations.

Setting this option replaces the built-in default, rather than extending it: configuring suffixes = ["Failure"] matches only *Failure names, not *Error or *Failure. To keep the default suffix alongside a project-specific one, list it explicitly: suffixes = ["Error", "Failure"].

Defaults to ["Error"]. A type that implements std::error::Error is flagged regardless of suffix.

Types

RequireFor enum

"pub" (Rust: Pub)

Require #[non_exhaustive] on items that are effectively reachable from outside the crate (declared pub, re-exported pub, and not buried inside a non-pub module). A pub enum FooError inside a non-pub module is not flagged because it cannot be matched on by any downstream crate.

"pub_crate" (Rust: PubCrate)

In addition to the Pub case, require #[non_exhaustive] on items literally declared pub(crate) (i.e., restricted to the crate root). Items declared pub(in some::module) are not promoted by this mode even if their effective reach happens to extend to the crate root.

"all" (Rust: All)

Require #[non_exhaustive] on every error-shaped item regardless of visibility.

Source: src/rules/non_exhaustive_error.rs

perfectionist::prefer_raw_string

Warnstring literal contains only raw-expressible escapes; prefer the raw-string form

What it does

Forbids regular string literals whose only backslash escapes are ones a raw string would express verbatim — \", \\, and \'. The autofix rewrites the literal to the raw form r"..." / r#"..."#, picking the smallest hash count that avoids a delimiter collision.

This includes literals passed as arguments to macros such as println!, format!, vec!, and assert!. Suppress per call site with #[allow(perfectionist::prefer_raw_string)] when the regular form is deliberately preferred.

Pattern-position literals (e.g. match s { "C:\\path" => ... }) are out of scope — the rule only visits expression literals.

Whitespace and control-character escapes (\n, \t, \r, \0) and Unicode escapes (\x.., \u{..}) are exempt — a raw string cannot express them, and the regular form is the only choice. A literal that mixes eliminable and inexpressible escapes is also left alone; the rewrite would force the author to split the literal or fall back to concat!, which loses more than it gains.

Why restrict this?

This is a stylistic preference, not a correctness issue. The rule trades one noise source (interior backslash escapes) for a slightly more elaborate string syntax. The benefit is highest in strings full of file paths, regex patterns, JSON snippets, or embedded source code — all of which would otherwise be a sea of \\ and \".

Example

let json = "{\"name\":\"foo\"}";
let path = "C:\\Users\\foo\\bar";

Use instead:

let json = r#"{"name":"foo"}"#;
let path = r"C:\Users\foo\bar";
Configuration

Configure via dylint.toml under ["perfectionist::prefer_raw_string"].

enabled : boolean optional

Master on/off switch for the rule. Set to false to silence every diagnostic without enumerating individual literals.

min_escapes_to_trigger : unsigned integer optional

Minimum number of eliminable escapes a string must contain before the lint fires. Default 1 catches every escapable string; set to 2 to skip single-escape literals where the raw form is arguably noisier than the original.

escapes_eligible : [string] optional

Escape sequences considered eliminable by switching to raw form. Only the three Rust escapes whose decoded character is exactly the byte after the backslash — "\"", "\\", "\\'" — are accepted; entries listed here that fall outside that closed set are silently dropped. (\n, \t, \xNN, \u{...} and other escapes decode to a different character and cannot be expressed verbatim in a raw string, so they have no place in this list.) Use this knob to narrow eligibility — e.g. ["\\\""] to only flag literals whose sole escapes are escaped quotes — not to extend it.

Source: src/rules/prefer_raw_string.rs

perfectionist::single_letter_closure_param

Warnclosure parameter has a single-letter name

What it does

Flags closure parameters whose identifier is one ASCII letter, unless the closure is a trivial single-expression callback. Two shapes qualify as trivial:

Why restrict this?

This is a stylistic preference, not a correctness issue. A multi-line closure body whose parameter is a single letter forces the reader to scroll back to the closure header for context on every reference. The trivial- callback exception covers sort_by(|a, b| ...) and .map(|x| x.field) shapes that are short enough that the parameter's role is unambiguous from the call site.

Example

.map(|t| {
    let columns = build_columns(t);
    format_row(&columns)
})

Use instead:

.map(|tree_row| {
    let columns = build_columns(tree_row);
    format_row(&columns)
})
Configuration

Configure via dylint.toml under ["perfectionist::single_letter_names"].

let_binding_allowed_idents : [string] optional

Identifiers that are always allowed as let binding names, even outside #[cfg(test)] code. Defaults to ["n"].

fn_param_allowed_idents : [string] optional

Identifiers that are always allowed as function or method parameter names. Defaults to ["n", "f", "i", "j", "k"].

short_impl_max_lines : unsigned integer optional

Maximum number of source lines an impl Trait for Type block may span and still permit single-letter generic parameter names. Defaults to 20.

comparison_methods : [string] optional

Method / function names whose closure argument may carry single-letter parameters when the body is a single expression. Extend this list to add project-specific DSL helpers (when, iter_by, …).

Source: src/rules/single_letter_names.rs

perfectionist::single_letter_function_param

Warnfunction parameter has a single-letter name

What it does

Flags function and method parameters whose identifier is one ASCII letter, except for a curated set of conventional names (n for an unsigned count, f for a fmt::Formatter, i / j / k for indices).

Why restrict this?

This is a stylistic preference, not a correctness issue. Parameter names are the first piece of documentation a caller reads (in rustdoc, in IDE hover tips, in error messages). A descriptive parameter name carries that documentation; a single letter does not.

Example

fn write_row(w: &mut Writer, t: &TreeRow) -> io::Result<()> { ... }

Use instead:

fn write_row(writer: &mut Writer, tree_row: &TreeRow) -> io::Result<()> { ... }
Configuration

Configure via dylint.toml under ["perfectionist::single_letter_names"].

let_binding_allowed_idents : [string] optional

Identifiers that are always allowed as let binding names, even outside #[cfg(test)] code. Defaults to ["n"].

fn_param_allowed_idents : [string] optional

Identifiers that are always allowed as function or method parameter names. Defaults to ["n", "f", "i", "j", "k"].

short_impl_max_lines : unsigned integer optional

Maximum number of source lines an impl Trait for Type block may span and still permit single-letter generic parameter names. Defaults to 20.

comparison_methods : [string] optional

Method / function names whose closure argument may carry single-letter parameters when the body is a single expression. Extend this list to add project-specific DSL helpers (when, iter_by, …).

Source: src/rules/single_letter_names.rs

perfectionist::single_letter_generic

Warngeneric type parameter has a single-letter name

What it does

Flags generic type parameters whose identifier is one ASCII letter (T, U, K, V, …), except inside trait impl blocks whose body fits within a small line threshold.

Why restrict this?

This is a stylistic preference, not a correctness issue. Single-letter generic names propagate through the type signatures and bounds; in a long impl block they force every reader to scroll back to the impl header to recover the role of each parameter. Descriptive names (Element, Key, Reader) keep complex signatures self-documenting. The short-trait-impl exception covers the canonical impl<T> From<T> for Wrapper<T> shape where the body is small enough that a reader cannot lose track of T.

Example

pub fn collect_keys<K, V>(map: BTreeMap<K, V>) -> Vec<K> {
    /* fifty lines */
}

Use instead:

pub fn collect_keys<Key, Value>(map: BTreeMap<Key, Value>) -> Vec<Key> {
    /* fifty lines */
}
Configuration

Configure via dylint.toml under ["perfectionist::single_letter_names"].

let_binding_allowed_idents : [string] optional

Identifiers that are always allowed as let binding names, even outside #[cfg(test)] code. Defaults to ["n"].

fn_param_allowed_idents : [string] optional

Identifiers that are always allowed as function or method parameter names. Defaults to ["n", "f", "i", "j", "k"].

short_impl_max_lines : unsigned integer optional

Maximum number of source lines an impl Trait for Type block may span and still permit single-letter generic parameter names. Defaults to 20.

comparison_methods : [string] optional

Method / function names whose closure argument may carry single-letter parameters when the body is a single expression. Extend this list to add project-specific DSL helpers (when, iter_by, …).

Source: src/rules/single_letter_names.rs

perfectionist::single_letter_let_binding

Warnlet binding has a single-letter name

What it does

Flags let x = ...; bindings whose identifier is one ASCII letter, outside #[cfg(test)] code.

Why restrict this?

This is a stylistic preference, not a correctness issue. A descriptive let binding documents what the right-hand side computed; a single-letter name does not. The rule allows let n = ... and other names in a configurable allowlist for the well-worn cases (unsigned counts), and switches off entirely under #[cfg(test)] where fixtures such as let a = ...; let b = ...; for interchangeable specimens are a recognised idiom.

Example

let m = entry.metadata()?;

Use instead:

let metadata = entry.metadata()?;
Configuration

Configure via dylint.toml under ["perfectionist::single_letter_names"].

let_binding_allowed_idents : [string] optional

Identifiers that are always allowed as let binding names, even outside #[cfg(test)] code. Defaults to ["n"].

fn_param_allowed_idents : [string] optional

Identifiers that are always allowed as function or method parameter names. Defaults to ["n", "f", "i", "j", "k"].

short_impl_max_lines : unsigned integer optional

Maximum number of source lines an impl Trait for Type block may span and still permit single-letter generic parameter names. Defaults to 20.

comparison_methods : [string] optional

Method / function names whose closure argument may carry single-letter parameters when the body is a single expression. Extend this list to add project-specific DSL helpers (when, iter_by, …).

Source: src/rules/single_letter_names.rs

perfectionist::unicode_ellipsis_in_comments

WarnU+2026 HORIZONTAL ELLIPSIS in non-doc comments; prefer ...

What it does

Forbids U+2026 HORIZONTAL ELLIPSIS () in regular // and /* */ comments. Doc comments (///, //!) are covered by a sibling lint.

Why restrict this?

This is a stylistic preference, not a correctness issue. ASCII ... survives every encoding round-trip, every terminal, every grep invocation, and every git diff viewer without rendering as ? or a tofu box. The Unicode form usually arrives by accident from autocorrect.

Example

// TODO: handle the empty-tree case…

Use instead:

// TODO: handle the empty-tree case...
Configuration

Configure via dylint.toml under ["perfectionist::unicode_ellipsis_in_comments"].

also_flag : [string] optional

Extra characters to flag alongside U+2026. Useful for catching near-relatives such as U+22EF MIDLINE HORIZONTAL ELLIPSIS () or U+2025 TWO DOT LEADER () that the same autocorrect pipelines occasionally insert. Empty by default.

scope : [Scope] optional

Which comment forms to scan. Defaults to both line (//) and block (/* */). Narrow this if a project intentionally uses one form for prose and wants the lint to ignore it.

Types

Scope enum

Selector for which comment syntaxes the rule scans.

"line" (Rust: Line)

//-prefixed line comments, including consecutive runs that rustc treats as a single logical comment.

"block" (Rust: Block)

/* ... */ block comments, including nested ones.

Source: src/rules/unicode_ellipsis_in_comments.rs

perfectionist::unicode_ellipsis_in_panic_messages

WarnU+2026 HORIZONTAL ELLIPSIS in panic / assertion / expect messages; prefer ...

What it does

Forbids U+2026 HORIZONTAL ELLIPSIS () in the message of a panic-family or assertion-style macro (panic!, unimplemented!, todo!, unreachable!, assert!, assert_eq!, assert_ne!, debug_assert*!) and in the expect / expect_err argument on Option and Result. Prefer the three-ASCII-dot form ....

Why restrict this?

This is a stylistic preference, not a correctness issue. Panic and assertion messages surface in stderr, CI logs, crash reporters, and on terminals whose locale or encoding may not be UTF-8. ASCII ... renders identically everywhere.

Example

panic!("could not parse manifest…");
let manifest = load().expect("config missing…");

Use instead:

panic!("could not parse manifest...");
let manifest = load().expect("config missing...");

Custom macros

The macros configuration accepts any macro name, but the lint's per-macro knowledge of which argument is the message only covers the built-in panic / assertion macros. A custom macro added through this knob is treated as if its first argument were the message; an assert_eq!-shaped wrapper would therefore also scan its value-position literals. Adding per-macro skip counts requires extending the configuration schema and is out of scope for the initial rule.

Configuration

Configure via dylint.toml under ["perfectionist::unicode_ellipsis_in_panic_messages"].

macros : [string] optional

Macros whose call site should be scanned for the flagged characters. Defaults to the standard panic and assertion macros (panic, unimplemented, todo, unreachable, debug_unreachable, and the assert* family). Override to add project-specific assertion-shaped macros, or to narrow the set when a project deliberately uses in one of them.

methods : [string] optional

Method names on Option / Result whose first argument is the panic message. Defaults to expect and expect_err.

also_flag : [string] optional

Extra characters to flag alongside U+2026, in the same spirit as unicode_ellipsis_in_comments.also_flag. Empty by default.

Source: src/rules/unicode_ellipsis_in_panic_messages.rs

perfectionist::unknown_perfectionist_lints

Warnlint-control attribute references a perfectionist::* lint that this plugin does not register

What it does

Flags lint-control attributes (allow, warn, deny, forbid, expect, including under cfg_attr) whose lint name starts with perfectionist:: but does not name a lint this plugin actually registers.

Why is this bad?

Typos and stale references in #[allow(perfectionist::...)] silently neutralise the suppression they were written for. rustc's own unknown_lints covers tool-prefixed names inconsistently; this rule fills the gap and offers a "did you mean" hint against the registered set.

Example

#[allow(perfectionist::unicode_ellipsis_in_comment)] // typo
fn legacy() {}

Use instead:

#[allow(perfectionist::unicode_ellipsis_in_comments)]
fn legacy() {}
Configuration

Configure via dylint.toml under ["perfectionist::unknown_perfectionist_lints"].

suggestion_distance : unsigned integer optional

Maximum Levenshtein edit distance between an unknown perfectionist::* name and a registered lint for the lint to emit a "did you mean" suggestion. Defaults to 2, which catches single-character typos and short transpositions without producing wild guesses. Set to 0 to disable suggestions entirely.

Source: src/rules/unknown_perfectionist_lints.rs