enabledcalling .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:
Explicit cost.Arc::clone is O(1) reference-count
bump regardless of what T is; T::clone may be an
arbitrarily expensive deep copy. If the binding's type
later changes from Arc<Vec<u8>> to &Vec<u8>, the
method-call form value.clone() silently switches to
<Vec<u8> as Clone>::clone and starts allocating; the
qualified form does not type check and fails loudly.
Reader signal.Arc::clone(&handle) reads as "share a
handle"; handle.clone() reads as a generic Clone
invocation whose cost is unknown without checking the
binding's type.
disabledtrait 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. Two styles are configurable via
style:
alphabetical (default) — every trait name must be in
ASCII-case-insensitive alphabetical order.
prefix_then_alphabetical — the configured prefix list of
traits goes first, in the listed order; remaining traits are
sorted alphabetically after.
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.
The opinion is opt-in: a project that doesn't want to commit
to a single ordering shouldn't have to set anything. The rule
therefore ships disabled by default — enable it per crate by
adding to dylint.toml:
[perfectionist]enable=["derive_ordering"]
Example
Under style = "alphabetical":
#[derive(Debug, Clone, Copy)]structPoint;
Use instead:
#[derive(Clone, Copy, Debug)]structPoint;
Configuration
Configure via dylint.toml under ["perfectionist::derive_ordering"].
style : Styleoptional
Ordering policy. Defaults to alphabetical; set
prefix_then_alphabetical to pin a configured prefix list
of traits ahead of the alphabetised tail.
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
Styleenum
"alphabetical"(Rust: Alphabetical)
Every trait name must appear in ASCII-case-insensitive
alphabetical order.
enabledsubmodule 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
enabledmacro invocation passes an impure expression that should be bound to a let first
What it does
Flags impure 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!:
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.
Terminology
In this rule, pure means safe for the surrounding macro
to drop or duplicate: evaluating the argument zero, one, or
many times is observationally equivalent. Impure is
anything else, and is what the rule flags.
The classification is syntactic: the rule recognises a
curated set of shapes known to satisfy the property and
treats everything else as impure. A const fn call, a
Result::map chain over a pure base, or vec.fold(...) is
therefore impure under this rule unless its shape is
recognised — the lint cannot prove side-effect-freedom in
general, only spot it. The trade-off favours flagging
side-effect-free expressions over silently passing a real
hazard. The set is narrower than the functional-programming
notion of purity and is keyed to what a macro can actually
do with its captures, not to side-effect-freedom in the
abstract.
The recognised pure shapes are: literals, paths, field
accesses, indexing of pure bases, dereferences, references,
casts, the unit literal (), parenthesised / tuple /
array-literal / array-repeat groups whose elements are all
pure, binary chains of pure operands joined by
side-effect-free operators, zero-arg method calls whose name
is in the curated pure-getter set (len, is_empty,
as_str, as_bytes, as_ref, as_mut, as_deref,
as_slice, plus anything in extra_pure_methods), and
calls to core / std macros whose expansion is a compile-
time constant (concat!, env!, option_env!,
include_str!, include_bytes!, stringify!, cfg!,
line!, column!, file!, module_path!, plus anything in
extra_pure_macros). A comparison like vec.len() <= cap
evaluates the same way regardless of how many times the
macro touches it, so binding it to a let would only force
the comparison to run in release builds for no benefit; the
same logic applies to env!("HOME") inside
debug_assert_eq!(...) — there is nothing to evaluate at
runtime.
let ejected = map.insert(key, value);debug_assert_eq!(ejected,None,"duplicate");
Configuration
Configure via dylint.toml under ["perfectionist::macro_argument_binding"].
mode : Modeoptional
Eligibility mode.
deny_extra : [string]optional
Macros added to the built-in deny list. Each entry is a
fully-qualified macro path (no trailing !) or a bare macro
name to match by final segment only.
allow_extra : [string]optional
Macros added to the built-in allow list. Same matching rules
as deny_extra. Only meaningful in AllowAndDeny and
Blanket modes; in DenyOnly the allow list is unused.
ignore : [string]optional
Macros to skip entirely, regardless of which list they would
otherwise hit. Same matching rules as deny_extra.
extra_pure_methods : [string]optional
Method names added to the built-in pure-method list. Each
entry is a bare method identifier (no (), no receiver). A
.method() invocation on a pure base is then accepted as a
pure postfix when the method takes no arguments. Add a
project-local method here only when it is genuinely safe
for the surrounding macro to drop or duplicate the call
(the rule's working definition of pure) — typically an
O(1) side-effect-free getter that the lint's syntactic
classification can't otherwise see.
ignore_pure_methods : [string]optional
Method names to drop from the pure-method list, even if they
appear in the built-in defaults or in extra_pure_methods.
Empty by default; checked after the merge, so this knob always
wins. Useful for opting back into linting on a default entry
the project does not consider pure — for example, removing
as_ref for a project that wraps it in an impure
implementation.
extra_pure_macros : [string]optional
Macro names added to the built-in pure-macro list. Each
entry is matched against the invocation's final path segment
(so my_crate::const_str matches by the "const_str" tail).
A pure-macro call passed as an argument to another macro is
treated as a pure atom — the rule does not propose binding
it to a let. Use this knob for project-specific macros
whose expansion is a compile-time constant (a literal, a
&'static str, a bool); their inclusion satisfies the
rule's pure-as-drop-or-duplicate-safe definition trivially,
since there is no runtime expression for the surrounding
macro to drop or duplicate.
ignore_pure_macros : [string]optional
Macro names to drop from the pure-macro list, even if they
appear in the built-in defaults or in extra_pure_macros.
Checked after the merge, so this knob always wins.
Types
Modeenum
Eligibility mode. The default is AllowAndDeny. The matcher-based
mode described in planned-rules/macro-argument-binding.md is not
yet implemented and is therefore not exposed as a value here; a
dylint.toml that names it will fail to deserialise with a
useful error.
"deny_only"(Rust: DenyOnly)
Flag only invocations of the curated deny list (debug_assert*
plus deny_extra). Every other macro is silently accepted.
"blanket"(Rust: Blanket)
Flag every function-like or array-like invocation that carries
an impure top-level argument, regardless of any built-in
classification — unless the invocation matches an allow_extra
entry. The built-in allow list is deliberately ignored in this
mode; project exceptions go in allow_extra.
"allow_and_deny"(Rust: AllowAndDeny)
Curated deny list plus curated allow list, both extensible via
deny_extra / allow_extra. Macros on neither list are
flagged — flagging unrecognised macros is deliberate so the
rule remains useful in projects that depend on uncatalogued
proc macros.
enabledmacro 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
name_based_extra 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"].
name_based_extra : [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
name_based_extra. Matched by final path segment, like
name_based_extra. Checked first, so this knob always wins
over eligibility. Empty by default.
disablederror-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 rule therefore ships disabled by default — enable it per
crate by adding to dylint.toml:
Configure via dylint.toml under ["perfectionist::non_exhaustive_error"].
require_for : RequireForoptional
Visibility threshold for the rule.
extra_suffixes : [string]optional
Additional identifier suffixes that mark a type as "an
error" purely by name, without inspecting its trait
implementations. Merged with the built-in defaults
(["Error"]); empty by default. List project-specific
vocabulary here (Failure, Fault, …) without having to
re-state the standard suffix.
ignore_suffixes : [string]optional
Identifier suffixes to drop from the allowlist, even if
they appear in the built-in defaults or in extra_suffixes.
Empty by default; checked after the merge with the
built-ins, so this knob always wins. Use it when a project
deliberately does not want the Error suffix to trigger
the by-name branch — types that implement
std::error::Error are still flagged via the trait branch.
Types
RequireForenum
"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.
enabledstring 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"].
min_escapes_to_trigger : unsigned integeroptional
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.
Flags closure parameters whose identifier is one ASCII
letter, unless the closure is a trivial single-expression
callback. Two shapes qualify as trivial:
the closure is the immediate argument of a call whose
callee name is in the trivial-callback allowlist
(sort_by, sort_by_key, min_by, max_by,
binary_search_by, cmp_by, partial_cmp_by,
fold, try_fold, …). The allowlist also covers the
matching adaptors from itertools (sorted_by,
k_smallest_by, minmax_by_key, …) and into-sorted
(into_sorted_by, into_sorted_by_key, …);
the body is a trivial wrapper around the parameter —
a field access (|x| x.field), a method call
(|x| x.foo()), a one-argument call where the
parameter is the sole argument (|x| f(x)), a
reference (|x| &x), or a macro call
(|x| vec![x], |x| dbg!(x),
|x| format!("{x}")). Surrounding * / &
operators around the parameter inside any of the
non-macro shapes are peeled before the match, so
|s| (*s).foo() qualifies.
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.
Configure via dylint.toml under ["perfectionist::single_letter_closure_param"].
extra_trivial_callback_methods : [string]optional
Additional method / function names whose closure argument
may carry single-letter parameters when the body is a
single expression. Merged with the built-in defaults (the
curated core / std callbacks plus selected itertools
and into-sorted adaptors); empty by default. List
project-specific DSL helpers (when, iter_by, third-party
callbacks such as into_sorted_by, …) here without having
to re-state the standard ones.
Method / function names to drop from the allowlist, even if
they appear in the built-in defaults or in
extra_trivial_callback_methods. Empty by default; checked
after the merge with the built-ins, so this knob always
wins. Useful for opting back into linting on a default
entry the project does not consider trivial.
enabledfunction 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.
Configure via dylint.toml under ["perfectionist::single_letter_function_param"].
extra_allowed_idents : [string]optional
Additional identifiers to allow as function or method
parameter names. Merged with the built-in defaults
(["n", "f", "i", "j", "k"]); empty by default. Use this
to whitelist project-specific conventional names without
having to re-state the standard ones.
ignore_allowed_idents : [string]optional
Identifiers to drop from the allowlist, even if they appear
in the built-in defaults or in extra_allowed_idents.
Empty by default; checked after the merge with the
built-ins, so this knob always wins.
enabledgeneric 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.
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_let_binding"].
extra_allowed_idents : [string]optional
Additional identifiers to allow as let binding names,
even outside #[cfg(test)] code. Merged with the built-in
defaults (["n"]); empty by default. Use this to
whitelist project-specific conventional names without
having to re-state the standard ones.
ignore_allowed_idents : [string]optional
Identifiers to drop from the allowlist, even if they
appear in the built-in defaults or in
extra_allowed_idents. Empty by default; checked after
the merge with the built-ins, so this knob always wins.
enabledU+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
Scopeenum
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.
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 extra_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"].
extra_macros : [string]optional
Additional macros whose call site should be scanned for
the flagged characters. Merged with the built-in defaults
(the standard panic and assertion macros — panic,
unimplemented, todo, unreachable, debug_unreachable,
and the assert* family); empty by default. Use this to
add project-specific assertion-shaped macros without having
to re-state the standard ones.
ignore_macros : [string]optional
Macros to drop from the scanned set, even if they appear in
the built-in defaults or in extra_macros. Empty by
default; checked after the merge with the built-ins, so
this knob always wins. Use it when a project deliberately
uses … in one of the default macros.
extra_methods : [string]optional
Additional method names on Option / Result whose first
argument is the panic message. Merged with the built-in
defaults (expect, expect_err); empty by default. Use
this to add project-specific expect-shaped wrappers
without having to re-state the standard pair.
ignore_methods : [string]optional
Methods to drop from the scanned set, even if they appear
in the built-in defaults or in extra_methods. Empty by
default; checked after the merge with the built-ins, so
this knob always wins.
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.
enabledlint-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.
Configure via dylint.toml under ["perfectionist::unknown_perfectionist_lints"].
suggestion_distance : unsigned integeroptional
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.