lint-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:
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.
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:
preserve (default) — no-op.
alphabetical — 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.
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 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
Styleenum
"preserve"(Rust: Preserve)
No-op. The lint emits nothing.
"alphabetical"(Rust: Alphabetical)
Every trait name must appear in ASCII-case-insensitive
alphabetical order.
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
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!:
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.
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 : booleanoptional
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 : booleanoptional
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.
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.
Configure via dylint.toml under ["perfectionist::non_exhaustive_error"].
require_for : RequireForoptional
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
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.
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 : booleanoptional
Master on/off switch for the rule. Set to false to silence
every diagnostic without enumerating individual literals.
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 comparison / fold allowlist
(sort_by, sort_by_key, min_by, max_by,
binary_search_by, cmp_by, partial_cmp_by,
fold, try_fold, …);
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| vec![x]), or a
reference (|x| &x). Surrounding * / & operators
around the parameter inside any of these 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_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 integeroptional
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, …).
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_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 integeroptional
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, …).
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.
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 integeroptional
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, …).
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 integeroptional
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, …).
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
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 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.
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.
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.