active#[allow] for a deterministically-firing lint should be removed or be #[expect]
What it does
Flags #[allow(<lints>)] when every named lint fires
deterministically — a built-in rustc lint (not on the exempt
list), a clippy::* / rustdoc::* lint, or a tool-namespaced
lint such as perfectionist::*. Such a suppression can be
resolved two ways:
Remove it — when the lint can no longer fire at the site
(a dead suppression, e.g. clippy::too_many_arguments left on
a function that has since shed its arguments).
Replace it with #[expect] — when the lint still fires and
you want the suppression to report itself the moment it stops.
If the attribute also names a lint the rule leaves alone (an
exempt or unknown lint), only the deterministic names are
resolved; the rest stay under #[allow].
Crate- and module-level scopes (#![allow(...)], and outer
#[allow(...)] on a mod item) are left alone by default,
because a cfg-conditional body inside the scope may fire the
lint in one configuration and not another — set
apply_to_outer_scopes = true to opt in.
Why restrict this?
This is a stylistic preference, not a correctness issue.
A suppression often outlives the problem it suppressed.
#[allow] stays silent forever, including after the underlying
issue is resolved, so a project accumulates stale #[allow]
attributes that no longer apply. #[expect] emits
unfulfilled_lint_expectations the moment the named lint stops
triggering at the site — exactly when the suppression becomes
dead — so routine compilation tells the author to remove it.
Every #[expect] is also a self-test that the lint does fire
at the site, so a future refactor that inadvertently fixes the
issue is observed rather than hidden.
Interaction with Clippy
clippy::allow_attributes (restriction, off by default)
also pushes #[allow] towards #[expect], but rewrites
every#[allow] indiscriminately, and only towards #[expect].
This rule flags only the lints that fire deterministically,
leaving lint groups and conditionally-firing lints under
#[allow]. Crucially, it does not assume #[expect] is always
the answer: a deterministic #[allow] may already be dead, in
which case #[expect] would be unfulfilled and removal is the
right fix, so the rule offers both. Reach for this rule for that
precision, or clippy::allow_attributes for a blunt crate-wide
sweep.
Configure via dylint.toml under ["perfectionist::allow_attributes"].
extra_exempt_lints : [string]optional
Extra lints to exempt, on top of the built-in default set (the
cfg-conditional unused_* / reachability lints). Names are
matched against the fully-namespaced lint name shown in
diagnostics (e.g. clippy::too_many_arguments). Merged with the
defaults rather than replacing them.
ignore_exempt_lints : [string]optional
Lints to drop from the exempt set, even if they appear in the
built-in defaults or in extra_exempt_lints. Use this to opt a
default exemption back into rewriting (e.g. ["dead_code"] in a
project with no cfg-gated dead code).
apply_to_outer_scopes : booleanoptional
When true, also rewrite crate-level #![allow(...)] and
module-level #[allow(...)] attributes. Default false
because cfg-conditional bodies inside the scope are common.
apply_to_tool_namespaces : booleanoptional
When false, only clippy::*, rustdoc::*, and built-in lints
are rewritten; other tool namespaces (perfectionist::* and
similar) are left alone. Default true — a tool namespace's
lints are assumed to fire deterministically like a built-in, so
perfectionist::* (and similar) are rewritten by default.
perfectionist::allow_attributes_without_reason↑ top
active#[allow] / #[expect] attribute lacks an explanatory reason = "..." field
What it does
Requires every #[allow(<lints>)] and #[expect(<lints>)]
attribute to carry an explanatory reason = "..." field.
#[allow] and #[expect] are the two levels that fully
silence a lint's output; the project's record of
suppressions needs to know why each one exists.
The check is purely local — the attribute itself — and does
not depend on any inherited or ambient lint level.
Why restrict this?
This is a stylistic preference, not a correctness issue.
Suppressions outlive the conditions that justify them. A
bare #[allow(clippy::too_many_arguments)] told the original
author to ignore a complaint; six months later, no one knows
whether the rationale was "matches upstream signature",
"intentional over-engineering", or "we'll fix it in the next
refactor". The reason field records intent at the moment of
suppression, and rustc renders it back in unfulfilled_lint_expectations
notes when a stale #[expect] is encountered.
Interaction with Clippy
clippy::allow_attributes_without_reason (restriction, off
by default) flags a missing reason = "..." on the same two
levels this rule does — #[allow] and #[expect] — and only
those, so the trigger overlaps. This rule adds a quality floor
Clippy lacks: min_reason_length (default 3) flags a
content-free reason = "ok" that Clippy accepts, and a blank
reason = "" is treated as missing. It also takes a per-lint
exempt_lints list and fires regardless of the crate's MSRV,
where Clippy's is gated on the lint_reasons stabilisation.
For the bare presence check alone, the Clippy lint is
equivalent.
Configure via dylint.toml under ["perfectionist::allow_attributes_without_reason"].
exempt_lints : [string]optional
Lints excluded from the requirement. Useful for project-wide
suppressions whose rationale lives in the project README
rather than per-site. Each entry is the lint's full name as
it appears inside the attribute (clippy::module_name_repetitions,
dead_code, ...).
Minimum length of the reason value. A one- or two-character
reason ("x", "ok") satisfies the literal presence
requirement but conveys nothing; the default floor of 3
excludes those cases. Projects that want a higher bar (e.g.
require a full sentence) can raise it. The lower bound is 1
— 0 is rejected at parse time, since an empty literal is
already treated as a missing reason regardless of this knob.
activestring 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.
Literals inside macro invocations are covered too: every string
literal in a macro call, whatever its position — including a
format!-family template that contains a {...} placeholder. The
rewrite is value-preserving (a raw string still parses
placeholders, and {{ / }} survive verbatim), so the literal's
role doesn't matter. Suppress a site where the regular form is
deliberately preferred.
That includes literals a macro uses for their source spelling
rather than their value, such as stringify! and dbg!, where
the raw form has the same value but a different reflected text.
This is intentional: code whose behaviour depends on a literal's
exact spelling instead of its value is rare and a code smell;
suppress it per site with
#[expect(perfectionist::avoidable_string_escapes)].
Pattern-position literals in ordinary code
(e.g. match s { "C:\\path" => ... }) are out of scope; only
expression-position literals are rewritten. A literal written as a
pattern inside a macro call (e.g. matches!(s, "C:\\path")) is
rewritten anyway, since a literal's position isn't distinguished
inside a macro.
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
Avoid:
let json ="{\"name\":\"foo\"}";let path ="C:\\Users\\foo\\bar";
Prefer:
let json =r#"{"name":"foo"}"#;let path =r"C:\Users\foo\bar";
Configuration
Configure via dylint.toml under ["perfectionist::avoidable_string_escapes"].
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. The lower
bound is 1 — 0 is rejected at parse time, since
suggesting r"hello" for "hello" would just trip
clippy::needless_raw_strings on the next pass, and a
minimum of 1 already excludes that case.
eligible_escapes : [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.
activebare email address in comment or doc comment; wrap in <...> or prefix with mailto:
What it does
Flags bare email addresses (user@example.com) in doc
comments (///, //!) and regular comments (//, /* */).
Wrapping in <...>, prefixing with mailto:, or both turns
the address into an explicit autolink across CommonMark,
GitHub-flavored markdown, and rustdoc.
A forbid style is available for projects that prefer to
keep contact information out of source entirely.
Why restrict this?
This is a stylistic preference, not a correctness issue. Bare
email addresses rely on the renderer's autolinkification,
which is inconsistent across markdown engines. The
<email> / mailto:email forms make the autolink intent
explicit.
Example
Avoid:
/// Report security issues to security@example.com.
Prefer:
/// Report security issues to <security@example.com>.
Configuration
Configure via dylint.toml under ["perfectionist::bare_email"].
style : Styleoptional
Required form for compliant email addresses. Defaults to
either.
Scan regular comments (//, /* */). Defaults to true.
skip_addresses : [string]optional
Skip these exact addresses. Useful for noreply@github.com
and similar placeholders that the project deliberately leaves
bare in changelog entries. Empty by default.
skip_domains : [string]optional
Skip addresses whose domain exactly equals any of these.
Empty by default.
Types
Styleenum
Required form for compliant email addresses.
"angle_brackets"(Rust: AngleBrackets)
Wrap the address with < and > — <user@example.com>.
"mailto"(Rust: Mailto)
Prefix the address with mailto: — mailto:user@example.com.
"both"(Rust: Both)
Combine both — <mailto:user@example.com>.
"either"(Rust: Either)
Accept any of the three wrapped forms (<email>,
mailto:email, or <mailto:email>); the autofix emits two
MaybeIncorrect suggestions for the author to pick from.
"forbid"(Rust: Forbid)
Forbid email addresses outright — no autofix, just a help
note recommending the address be moved to an external file
or removed entirely.
activebackticked identifier in a doc comment that resolves in scope should be an intra-doc link
What it does
Flags a backticked identifier in a doc comment (`Foo`)
that resolves as a Rust path in the documented item's scope but
is not written as a rustdoc intra-doc link ([`Foo`]).
Only bare single identifiers whose name resolves to an item in
the enclosing module's scope are flagged; a backticked word that
names nothing in scope is left alone.
A publicly-reachable item that mentions a private (not
publicly-reachable) item is also left alone: turning that mention
into a link would make rustdoc's rustdoc::private_intra_doc_links
fire under a plain cargo doc, and a public item leaning on a
private one is a separate concern from this rule's.
Why restrict this?
This is a stylistic preference, not a correctness issue. Both
`Foo` and [`Foo`] render as monospaced text, so the
page looks the same at a glance. The link form additionally
turns the mention into a clickable cross-reference and lets
rustdoc's rustdoc::broken_intra_doc_links lint catch the day a
rename leaves the prose pointing at a type that no longer
exists. Spelling every in-scope mention as a link keeps the
documentation navigable and the references checked.
Example
Avoid:
/// Installs the package described by `PackageManifest` into `Store`.
pubfninstall(manifest:&PackageManifest, store:&Store){}
Prefer:
/// Installs the package described by [`PackageManifest`] into [`Store`].
pubfninstall(manifest:&PackageManifest, store:&Store){}
Caveat: linking is not always correct
When a backticked word names a foreign symbol that merely
shares its name with an in-scope item, rewriting `Foo` to
[`Foo`] links to the local item — a wrong reference that
still compiles. The rule therefore only suggests the link
rather than applying it automatically. For an external target,
write an explicit reference link instead:
[`Foo`][foo-ext] plus a [foo-ext]: <url> definition.
Configuration
Configure via dylint.toml under ["perfectionist::bare_identifier_reference"].
skip_idents : [string]optional
Identifiers the rule never suggests linking, even when they
resolve in scope. Empty by default. Use this for a name a doc
comment deliberately mentions without wanting a cross-reference
— a historical type kept for context, or a word that happens to
collide with an in-scope item but is meant as prose.
reference_scope : ReferenceScopeoptional
How far from the documenting item a referenced name may resolve
for the rule to check it, by where the referenced item lives in
the module tree. A backticked word that matches an
accidentally-added cross-module import is a common source of
churn, so a project can narrow (or widen) this. Defaults to
crate: a project's own items are kept linked, but mentions of
the standard library and third-party crates are left alone.
check_pascal_case : booleanoptional
Whether to check PascalCase names. Defaults to true.
A mixed or non-conformist name (fooBar, foo_BAR, __foo,
foo__bar) is checked regardless of this field: such a spelling
is rare and, when it matches a local identifier, rarely an
accident.
check_upper_case : booleanoptional
Whether to check UPPER_CASE (SCREAMING_SNAKE_CASE) names.
Defaults to true.
A mixed or non-conformist name (fooBar, foo_BAR, __foo,
foo__bar) is checked regardless of this field: such a spelling
is rare and, when it matches a local identifier, rarely an
accident.
check_snake_case : booleanoptional
Whether to check snake_case names. Defaults to true.
A mixed or non-conformist name (fooBar, foo_BAR, __foo,
foo__bar) is checked regardless of this field: such a spelling
is rare and, when it matches a local identifier, rarely an
accident.
min_words : non-zero unsigned integeroptional
Minimum number of words a name must have to be checked. Defaults
to 1 (check everything). At 3, foo, foo_bar, Foo,
FooBar, FOO, and FOO_BAR are exempt, while foo_bar_baz,
FooBarBaz, and FOO_BAR_BAZ are checked.
A mixed or non-conformist name (fooBar, foo_BAR, __foo,
foo__bar) is checked regardless of this threshold: such a
spelling is rare and, when it matches a local identifier, rarely
an accident.
Types
ReferenceScopeenum
How far from the documenting item a referenced name may resolve for
the rule to check it, configured by reference_scope. The axis is
where the referenced item lives relative to the documenting item's
module, not the spelling of the use path that brought it in.
"own_module"(Rust: OwnModule)
Only items defined directly in the documenting item's own module.
Every name reached through a use is left alone.
"module_tree"(Rust: ModuleTree)
Also items from within that module's own subtree (e.g. reached
through use self::child::Item), but still not names that reach
outside the module — use super::..., use crate::..., and
imports from other crates.
"crate"(Rust: Crate)
Any item defined anywhere in the current crate (first-party), but
nothing from another crate. A project's own items are where
documentation drifts — a rename leaves a stale mention — while the
standard library and third-party crates are stable and outside the
project's control, so a bare mention of them is low risk.
"third_party"(Rust: ThirdParty)
The current crate and its third-party dependencies, but not the
standard / built-in libraries (std, core, alloc,
proc_macro, test). Use this when dependency references are
worth checking but the frozen standard library is not.
"anywhere"(Rust: Anywhere)
Any name that resolves in scope, however it got there — including
the standard library.
activeambiguous bare #NNN issue / PR reference in comment
What it does
Flags bare #NNN issue / pull-request references in doc
comments (///, //!) — and, when opted in, in plain //
line comments. The autofix rewrites the reference; the
doc_comment_form knob selects the shape (inline
[#123](URL), reference [#123], a bare URL, or a <URL>
autolink).
A bare #NNN is deeply ambiguous: it might be an issue, a
pull request, a colour like #123, or any other numbered
item, so no suggestion is ever MachineApplicable. The
suggest_issue_url / suggest_pr_url knobs choose which link
target(s) the autofix offers — each MaybeIncorrect — and
with neither enabled the lint is help-only. The author can
also resolve the ambiguity by enclosing the token in backticks
(so it reads as code) or by using a spelling without a leading
#.
Why restrict this?
This is a stylistic preference, not a correctness issue. A
bare #123 renders as literal text in CommonMark; only
GitHub's markdown flavour autolinks the token, and only when
the rendering surface is itself within a GitHub repository
view. The link form renders portably across rustdoc, GitHub,
and any other markdown engine.
Example
Avoid:
/// Closes #123 and supersedes #124.
Prefer: (with
repository = "https://github.com/owner/repo" — forge
is detected from the host), picking the issue link for one
and the pull-request link for the other:
/// Closes [#123](https://github.com/owner/repo/issues/123) and
/// supersedes [#124](https://github.com/owner/repo/pull/124).
Configuration
Configure via dylint.toml under ["perfectionist::bare_issue_reference"].
forge : Forgeoptional
Git-hosting service the repository is on — one of github,
gitlab, gitea — which fixes the issue / PR path layout.
When unset, it is detected from the repository host: the
public instances (github.com, gitlab.com, codeberg.org,
gitea.com) and the conventional self-hosted subdomains
gitlab.*, github.*, gitea.* and forgejo.* all need no
forge. Set it explicitly for a self-hosted instance on a
host that gives no such hint (e.g. git.example.com). If it is
neither set nor detected from the host, no issue / PR link is
suggested.
repository : stringoptional
The repository's URL, in any form you'd clone or paste: an
http(s):// URL ("https://github.com/owner/repo"), an
ssh:// URL ("ssh://git@github.com/owner/repo.git"), or the
scp-like shorthand ("git@github.com:owner/repo.git"). No
fixed default.
suggest_issue_url : booleanoptional
Offer a suggestion that links the reference as an issue.
Defaults to true.
suggest_pr_url : booleanoptional
Offer a suggestion that links the reference as a pull
request. Defaults to true. Ignored on GitLab, where a bare
#NNN is always an issue (merge requests are written !NNN),
so only the issue suggestion is offered there.
doc_comment_form : DocFormoptional
Doc-comment fix form: inline for [#N](URL), reference
for the two-piece [#N] + [#N]: URL form (the definition is
appended to the doc block; in a /** */ block doc comment only
the #N token is rewritten and the definition is left to the
author). Defaults to inline. Ignored for plain-comment fixes
— those follow plain_comment_form instead.
include_plain_comments : booleanoptional
When true, also lint plain // line comments. The
autofix in plain comments uses plain_comment_form's URL
shape (since plain comments aren't markdown). Plain block
comments (/* ... */) are out of scope regardless.
Defaults to false.
plain_comment_form : PlainFormoptional
Replacement form used inside plain // comments when
include_plain_comments = true. Defaults to bare_url.
Ignored for doc comments and when no repository is
configured.
Types
Forgeenum
A recognised git-hosting service. The chosen forge fixes the
issue / PR URL layout. It can be given explicitly (needed for a
self-hosted instance, whose host isn't recognised) or detected
from the repository's host via [Forge::detect].
"github"(Rust: GitHub)
GitHub or a GitHub Enterprise instance. Paths:
/issues/{number}, /pull/{number}.
"gitlab"(Rust: GitLab)
GitLab (gitlab.com or self-hosted). Paths:
/-/issues/{number}, /-/merge_requests/{number}.
Markdown-link shape produced by the autofix inside doc comments.
"inline"(Rust: Inline)
[#123](URL) — the URL is inlined. Keeps #123 as the
visible link text.
"reference"(Rust: Reference)
[#123], with a matching [#123]: URL definition appended to
the end of the doc block (after a blank line so it parses as a
definition). In a /** */ block doc comment the definition
can't be placed safely, so there the fix rewrites only the
#123 token and leaves the definition to the author.
"bare_url"(Rust: BareUrl)
https://.../issues/123 — the bare URL replaces the #123
token outright (the #123 text is not kept). NB: in a doc
comment the sibling perfectionist::bare_url lint then flags
the substituted URL; pick bracketed_url for a form it
accepts.
"bracketed_url"(Rust: BracketedUrl)
<https://.../issues/123> — a markdown autolink replaces the
#123 token outright. bare_url accepts this form.
PlainFormenum
URL shape used inside plain // comments when
include_plain_comments = true.
"bare_url"(Rust: BareUrl)
Substitute the URL itself (https://...), unwrapped.
Many editors auto-detect a bare URL as clickable. NB: the
sibling perfectionist::bare_url lint, whose default also
scans regular comments, will then flag the substituted URL —
pick bracketed_url to produce a form both rules accept.
"bracketed_url"(Rust: BracketedUrl)
Substitute <https://...>. The angle-bracket delimiter gives
the URL a clear boundary when it abuts surrounding
punctuation; editors that auto-link URLs typically recognise
it, and bare_url accepts it.
activebare URL in comment or doc comment; wrap in <...> or use a labelled markdown link
What it does
Flags bare http:// and https:// URLs in doc comments
(///, //!) and regular comments (//, /* */). Wrapping
the URL in <...> (or using the labelled [text](url) form)
is the portable rendering across CommonMark, GitHub-flavored
markdown, and rustdoc.
Why restrict this?
This is a stylistic preference, not a correctness issue. Bare
URLs rely on the renderer's autolinkification: rustdoc renders
them, GitHub renders them, but plain CommonMark does not. The
<...> form is the explicit, portable spelling.
Example
Avoid:
/// See https://example.com for details.
Prefer:
/// See <https://example.com> for details.
Configuration
Configure via dylint.toml under ["perfectionist::bare_url"].
Characters that, when the URL ends in one of them, keep the
autofix at MachineApplicable. Defaults to ["/", "_", "-", "=", "&", "+"]. ASCII alphanumerics and / are always
treated as safe regardless of this list; entries here
supplement that built-in set.
skip_hosts : [string]optional
Hosts to skip, compared case-insensitively. Defaults to
["localhost"].
activemarkdown construct in a clap-derived doc comment leaks into --help output
What it does
Forbids markdown-specific constructs in a doc comment that
clap's derive macros consume as --help text — HTML tags,
inline / reference / intra-doc links, code blocks, code spans,
and headings. The rule fires on the doc comment of a struct or
enum deriving clap::Parser, Args, Subcommand, ValueEnum,
or CommandFactory, and on the doc comments of their fields and
variants.
A ValueEnum's own type-level doc comment is left alone: unlike
its variant docs — clap's per-value help — it never reaches
--help.
Bold, italics, and lists are not flagged by default — clap
renders them as their literal characters, which usually reads
cleanly — but are available through the extra_constructs knob.
The lint stays silent on a node that overrides its help text
with a plain string (#[arg(help = "...")],
#[command(about = "...")], ...), because the doc comment is
then no longer the source of truth for --help. A node marked
#[clap(verbatim_doc_comment)] instead gets a softer note that
the markdown will appear verbatim in the terminal.
A project that never wants a doc comment to become --help text
at all — preferring an explicit override on every command,
argument, and value — can opt into the stricter
require_help_override mode. It flags every clap-derived doc
comment that reaches --help without such an override, markdown
or not, and reports each as a single missing-override finding
rather than per markdown construct. A node with no doc comment is
still left alone. This mode is off by default.
Why is this bad?
By default, clap does not render doc comments through a
markdown processor. The raw text is shown verbatim in the
terminal --help output. Writing [`PathBuf`] produces a
docs.rs link in HTML output but shows literally as
[`PathBuf`] in the terminal — a classic two-audience leak.
The doc comment serves both cargo doc readers and --help
readers, and markdown that helps the former actively degrades
the latter.
The escape hatch is to override the help text with a plain
string, keeping the rich doc comment for cargo doc:
/// Builds the lockfile by walking [`Dependency`] graphs.
#[arg(help ="Builds the lockfile by walking dependency graphs.")]pub deps: PathBuf,
Example
Avoid:
#[derive(clap::Parser)]structCli{/// Path to the [`PackageManifest`].
////// See [the manifest format](https://example.com/manifest).
manifest: PathBuf,
}
Prefer: (no markdown in the help text)
#[derive(clap::Parser)]structCli{/// Path to the package manifest.
manifest: PathBuf,
}
Configuration
Configure via dylint.toml under ["perfectionist::clap_help_markdown"].
extra_constructs : [ConstructCategory]optional
Markdown constructs to forbid in addition to the built-in default
set (html, inline_link, reference_link, intra_doc_link,
code_block, code_span, heading). Empty by default; bold,
italic, and list are the usual additions — clap renders them
acceptably, so they are off by default.
ignore_constructs : [ConstructCategory]optional
Constructs to drop from the forbidden set, even if they appear in
the built-in defaults. Empty by default; applied after the merge
with extra_constructs, so this knob always wins. Use it to
permit a construct in help text, e.g.
ignore_constructs = ["code_span"].
require_help_override : booleanoptional
For projects that never let a doc comment become --help text,
preferring an explicit clap override (#[arg(help = "...")],
#[command(about = "...")], ...) on every command, argument, and
value. When true, the rule flags every clap-derived doc comment
that reaches --help without such an override, regardless of
whether it contains markdown — the doc comment stays for
cargo doc, but --help must come from a plain-string override.
A node with no doc comment is left alone: the requirement is only
that a present doc comment never feeds --help unoverridden, so
a bare #[arg(help = "...")] with no /// is fine. This
supersedes the markdown scan — an override silences the markdown
concern anyway — so an unoverridden doc comment is reported once as
a missing override rather than per markdown construct. Defaults to
false.
Types
ConstructCategoryenum
A markdown construct category the rule can be configured to forbid,
as it appears in the extra_constructs / ignore_constructs arrays
of dylint.toml. The coarse policy counterpart to the scanner's
fine-grained [ConstructKind]: several kinds map onto one category
via [ConstructCategory::from_kind] — reference_link covers both a
[text][id] link and its [id]: dest definition, and an autolink
maps to nothing (it is never forbidden).
"html"(Rust: Html)
Raw HTML tags (<br>, <code>, <a href="...">, ...).
"inline_link"(Rust: InlineLink)
Inline links: [text](https://example.com).
"reference_link"(Rust: ReferenceLink)
Reference links ([text][id]) and their [id]: ...
definitions.
"intra_doc_link"(Rust: IntraDocLink)
Intra-doc links: [`Type`] and [Type].
"code_block"(Rust: CodeBlock)
Fenced, ~~~-fenced, or four-space-indented code blocks.
"code_span"(Rust: CodeSpan)
Inline code spans: `value`.
"heading"(Rust: Heading)
ATX (# Heading) and Setext (Heading\n=====) headings.
activeinline test code should be extracted to a separate file
What it does
Caps how much inline unit-test code a production file may carry.
Inline test code — #[cfg(test)] mod X { ... } blocks, #[test] fns, #[cfg(test)] fn helpers, and any other #[cfg(test)]
item — is summed per file. The default external_when_long style
flags a file once its inline-test footprint crosses
inline_max_lines (or the optional
inline_max_fraction_of_file); external_only flags every inline
test item regardless of length. A file whose top-level items are
entirely test code is exempt — it is itself a valid extraction
target.
An external #[cfg(test)] mod <name>; (its body in a separate
file) is neutral: it is already extracted, so it neither charges
the footprint nor is otherwise checked. A test module gated by a
compound predicate that still implies test —
#[cfg(all(test, unix))], #[cfg(all(test, feature = "..."))] —
is recognised the same as a bare #[cfg(test)], so its external
file is not re-flagged as inline test code in a production file.
Only the library or binary crate is checked. Integration tests
(tests/), benchmarks (benches/), and examples (examples/)
are separate targets, not the library or binary whose unit-test
footprint this rule governs; for those compiled under cfg(test)
their top-level #[test] functions are the target rather than
unit tests misplaced in a production file, so they are left
untouched.
Why restrict this?
This is a stylistic preference, not a correctness issue. Both
source projects keep large test suites out of the production
file, so the file an editor tab, a grep hit, or a diff shows
is production code rather than a wall of fixtures. The threshold
is deliberately configurable because the exact budget varies by
project.
Example
Avoid:
// File: foo.rs
#[cfg(test)]modtests{/* ... 200 lines of test code ... */}
Prefer:
// File: foo.rs
modtests;
// File: foo/tests.rs
/* ... 200 lines of test code ... */
Configuration
Configure via dylint.toml under ["perfectionist::excessive_inline_tests"].
inline_style : InlineStyleoptional
How inline test code is handled. Defaults to
external_when_long.
inline_max_lines : unsigned integeroptional
Absolute cap, in lines, on the summed inline-test footprint of a
file under external_when_long; always active. Defaults to 50.
inline_max_fraction_of_file : floatoptional
Optional relative cap: the share inline_test_lines / file_lines
a file's inline tests may occupy under external_when_long.
Accepted values are 0.0 <= x < 1.0; omit the key to disable the
relative cap (the default).
Types
InlineStyleenum
How inline test code is treated (the inline_style knob).
"external_only"(Rust: ExternalOnly)
Every inline test item is flagged; all test code must move to an
external mod <name>;. Matches pacquet's strict policy.
"external_when_long"(Rust: ExternalWhenLong)
Inline test code is allowed up to the configured budget; beyond
that it must move to a file. Matches parallel-disk-usage's
guidance.
inactiveerror-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 is therefore inactive by default — enable it per
crate by adding to dylint.toml:
[perfectionist]enable=["exhaustive_error_enums"]
Interaction with Clippy
clippy::exhaustive_enums and clippy::exhaustive_structs
(restriction, off by default) require #[non_exhaustive] on
every exported enum / struct. This rule scopes that same
requirement to error types — those whose name ends in
Error or that implement std::error::Error — because adding
a variant to an error type downstream code matches on is the
breaking change #[non_exhaustive] exists to absorb, while
demanding it on all exported types is usually too broad. Enable
the Clippy lints instead if you do want that blanket policy.
Configure via dylint.toml under ["perfectionist::exhaustive_error_enums"].
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 by-name match set,
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.
activeimport granularity does not match the configured import_granularity_mismatch.style
What it does
Enforces a single project-wide import-granularity style, chosen
via style:
crate — one use per crate root, with every shared prefix
collapsed into nested braces
(use std::{collections::HashMap, io::Read};).
module (default) — one use per leaf module; items from the
same module are merged into one braced list while sibling
modules sit on their own lines
(use std::collections::{BTreeMap, HashMap};).
item — one use per leaf path
(use std::collections::BTreeMap;).
The names map one-to-one onto rustfmt's unstable
imports_granularity (Crate / Module / Item). Only use
statements that sit next to each other in a module body, share a
visibility, and carry matching attributes are merged; the three
respect_* knobs tighten or loosen that grouping.
Under crate style a name that is both an item and a module
(use crate::thing; next to use crate::thing::T;) has two valid
one-use shapes — the self-fold crate::thing::{self, T} and
the sibling-split crate::{thing, thing::T} — that bind different
namespaces. By default neither single-statement form is flagged
and both are offered when a merge is forced; the optional
self_merge knob (fold / split) picks one and enforces it.
Globs (use foo::*) are governed by perfectionist::wildcard_imports,
not by this rule: a top-level glob is left alone under item.
Why restrict this?
This is a stylistic preference, not a correctness issue. None of
the three shapes is wrong in the abstract — the violation is a
mismatch with the project's configured style. Enforcing one
keeps use blocks scanning uniformly and makes import diffs
predictable. rustfmt can enforce the same shape, but only on the
nightly channel; this lint gives stable-toolchain projects a hard
CI check instead of a silent reformat.
Configure via dylint.toml under ["perfectionist::import_granularity_mismatch"].
style : Styleoptional
Import-granularity style to enforce. Defaults to module — the
shape that scales best as a use block grows. Set crate to
collapse every crate root into one nested use, or item to
put every imported name on its own line.
respect_cfg_blocks : booleanoptional
Never merge use statements that carry differing #[cfg(...)]
/ #[cfg_attr(...)] attributes. Defaults to true: a
platform-gated import is never folded together with an
unconditional one. Set false to ignore cfg attributes when
deciding what may merge.
respect_visibility : booleanoptional
Never merge a pub use (or pub(crate) use, etc.) with a
plain use, or two re-exports whose visibility differs.
Defaults to true. Set false to ignore visibility when
deciding what may merge.
respect_doc_comments : booleanoptional
Never merge a use that carries its own doc comment (/// or
#[doc = "..."]) into a neighbouring statement, so the comment
keeps describing exactly the import it was written above.
Defaults to true. Set false to allow such a use to merge.
self_merge : SelfMergeoptional
Under style = "crate", force one shape when a path segment
names both an item and a module (use crate::thing; next to
use crate::thing::T;). Unset by default — the two shapes are
not interchangeable, so the rule flags neither single-statement
form and offers both as MaybeIncorrect when a merge is forced.
Set fold to always enforce use crate::thing::{self, T};, or
split to always enforce use crate::{thing, thing::T};;
either value also flags the opposite single-statement form.
Ignored under module and item style.
Types
Styleenum
Import-granularity style. The three values map one-to-one onto
rustfmt's unstable imports_granularity option (Crate, Module,
Item).
"crate"(Rust: Crate)
One use per crate root. Every shared prefix is collapsed into
nested braces, e.g.
use std::{collections::HashMap, io::{Error, ErrorKind}};.
"module"(Rust: Module)
One use per leaf module. Items pulled from the same module are
merged into a single braced list; items from sibling modules sit
on their own use lines, e.g.
use std::collections::{BTreeMap, HashMap};.
"item"(Rust: Item)
One use per leaf item. Every imported name lives on its own
line, e.g. use std::collections::BTreeMap;.
SelfMergeenum
How to resolve a path segment that names both an item and a
module under crate style — use crate::thing; next to
use crate::thing::T;. The two one-use-per-root shapes are not
interchangeable, so unless this knob is set the rule offers both and
the author picks. Only consulted under style = "crate".
"fold"(Rust: Fold)
Always enforce the self-fold use crate::thing::{self, T}; —
the shape rustfmt's imports_granularity = "Crate" produces. The
sibling-split form is flagged and rewritten to it.
"split"(Rust: Split)
Always enforce the sibling-split use crate::{thing, thing::T};.
The self-fold form is flagged and rewritten to it.
inactiveimport grouping does not match the configured import_grouping_mismatch.style
What it does
Enforces a single project-wide grouping style for the run of
use statements at the top of a module body. The rule is
inactive by default; a project opts in and sets style to one of:
single_block — every use sits in one contiguous block with
no blank lines between imports, except that #[cfg(...)]-gated
imports are carved into their own trailing block (one blank
line below the rest). Set cfg_block_handling = "merge" to keep
them in the one block.
multi_block — imports are partitioned into ordered groups
separated by exactly one blank line. The group set, in order,
is std (std / core / alloc / proc_macro / test),
internal (crate / super / self), then third-party (every
other crate). A bare-path import of a first-party submodule
(mod error; use error::Foo;) is classified as internal, not
third-party: a bare first segment that names a mod declared in
the same module scope is recognised as first-party. The order
and cfg_block_handling
knobs tune the partition; the inner ordering within each group
is left to cargo fmt.
Orthogonally to style, the reexports knob controls pub
re-exports — any use with an explicit visibility. Like style it
is mandatory once the rule is enabled, and is one of: grouped,
which pulls every re-export into one contiguous leading block above
the private imports, visibility outranking path and cfg gating;
split, which breaks that block into two — submodule re-exports
(pub use child::Item;, a multi-segment path) above alias
re-exports (pub use Item; / pub use Item as Alias;, a
single-segment path); or by_path, which gives re-exports no
dedicated block, classifying each by its path like a private import.
This rule only governs the partitioning of imports into blocks.
Whether items within each use are merged or split is the job of
perfectionist::import_granularity_mismatch.
Why restrict this?
This is a stylistic preference, not a correctness issue. Neither
layout is wrong in the abstract — the violation is a mismatch
with the project's configured style. Enforcing one keeps import
blocks scanning uniformly and makes import diffs predictable.
rustfmt's group_imports option can enforce the same shape, but
only on the nightly channel; this lint gives stable-toolchain
projects a hard CI check instead of a silent reformat. The rule
is inactive by default; enable it and pick a style in
dylint.toml:
Re-exports in their own block (reexports = "grouped")
Avoid: (a pub use re-export mixed in with private imports)
pubusereflection::Reflection;usesuper::size;
Prefer: (re-exports kept in their own leading block)
pubusereflection::Reflection;usesuper::size;
Split re-exports (reexports = "split")
Avoid: (submodule re-exports and alias re-exports intermixed)
pubuse Reflection as HardlinkListReflection;pubuseiter::Iter;pubusereflection::Reflection;
Prefer: (submodule re-exports lead, alias re-exports follow)
pubuseiter::Iter;pubusereflection::Reflection;pubuse Reflection as HardlinkListReflection;
Configuration
Configure via dylint.toml under ["perfectionist::import_grouping_mismatch"].
style : Stylemandatory
The grouping style to enforce: single_block or multi_block. It
has no default — a project enabling the rule states which layout
it wants — so it must be set when the rule is enabled.
reexports : ReexportGroupingmandatory
How pub re-exports are grouped: grouped, split, or by_path.
Like style it has no default — a project enabling the rule states
how it wants re-exports laid out — so it must be set when the rule
is enabled. grouped pulls every re-export into one contiguous
leading block above all private imports; split breaks that block
into two — submodule re-exports (pub use child::Item;) above alias
re-exports (pub use Item; / pub use Item as Alias;); by_path
gives re-exports no dedicated block at all, classifying each by its
path like a private import.
order : [Group]optional
The order the groups appear in, top to bottom. Defaults to
["std", "internal", "thirdparty"].
cfg_block_handling : CfgBlockHandlingoptional
How #[cfg(...)]-gated imports are grouped. Defaults to
trailing: a cfg-gated import forms its own trailing block under
both styles. Set merge to keep cfg-gated imports with the rest —
in their natural path group under multi_block, or in the single
block under single_block.
Types
Styleenum
How use statements are partitioned into blocks.
"single_block"(Rust: SingleBlock)
Every use statement sits in one contiguous block, with no
blank lines between imports.
"multi_block"(Rust: MultiBlock)
Imports are partitioned into ordered groups separated by exactly
one blank line. The group set is
std (std / core / alloc / proc_macro / test), internal
(crate / super / self), and third-party (every other crate).
ReexportGroupingenum
How pub re-exports are grouped relative to the private imports. A
re-export is any use with an explicit visibility (pub,
pub(crate), pub(super), pub(in ...)); a private (Inherited)
import is not one.
"by_path"(Rust: ByPath)
Re-exports get no dedicated block: each is classified purely by
its path, exactly like a private import, so a pub use child::Item
sits in the same block as a private import of the same origin.
"grouped"(Rust: Grouped)
Every re-export is pulled into one contiguous leading block above
all private imports, separated by a blank line. A cfg-gated
re-export stays in this block rather than the trailing cfg block:
visibility takes precedence, keeping the public surface together.
"split"(Rust: Split)
Re-exports form a leading region split into two blank-separated
blocks: submodule re-exports (a multi-segment path such as
pub use child::Item;, which lifts an item out of a child module)
above alias re-exports (a single-segment path such as
pub use Item; or pub use Item as Alias;, which only renames an
item already in scope). The single-vs-multi-segment split is
purely syntactic; a pub use foo; that re-exports an external
crate counts as an alias re-export. As under grouped, a cfg-gated
re-export stays in its re-export sub-block rather than the trailing
cfg block.
Groupenum
One of the three groups a use statement is classified into. The
order knob is a permutation of these three values.
"std"(Rust: Std)
std, core, alloc, proc_macro, test.
"internal"(Rust: Internal)
crate, super, self.
"thirdparty"(Rust: Thirdparty)
Every other crate.
CfgBlockHandlingenum
How a #[cfg(...)]-gated import is grouped.
"trailing"(Rust: Trailing)
Give every #[cfg(...)]-gated import its own trailing block,
regardless of the imported path: an always-last group under
multi_block, a trailing block below the single block under
single_block.
"merge"(Rust: Merge)
Keep a cfg-gated import with the rest: slotted into its natural
path group under multi_block, or left in the single block under
single_block.
activemacro 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,
the logical / bitwise not of a pure expression (!ready),
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::impure_macro_arguments"].
mode : Modeoptional
Eligibility mode. Defaults to allow_and_deny.
deny_extra : [string]optional
Macros added to the built-in deny set. 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 set. Each entry is a
fully-qualified macro path (no trailing !) or a bare macro
name to match by final segment only. Only meaningful in
AllowAndDeny and Blanket modes; in DenyOnly the allow
set is unused.
ignore : [string]optional
Macros to skip entirely, regardless of which set they would
otherwise match. Each entry is a fully-qualified macro path
(no trailing !) or a bare macro name to match by final
segment only.
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.
"deny_only"(Rust: DenyOnly)
Flag only invocations of the curated deny set (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 set is deliberately ignored in this
mode; project exceptions go in allow_extra.
"allow_and_deny"(Rust: AllowAndDeny)
Curated deny set plus curated allow set, both extensible via
deny_extra / allow_extra. Macros classified by neither are
flagged — flagging unrecognised macros is deliberate so the
rule remains useful in projects that depend on uncatalogued
proc macros.
perfectionist::lint_attribute_trailing_comment↑ top
activetrailing comment on a lint-level attribute should be lifted into a reason = "..." field
What it does
When a lint-level attribute (#[allow], #[expect], #[warn],
#[deny], #[forbid]) carries a trailing // ... line comment
— on the same source line as the attribute's closing ] —
that documents why the level was chosen, lifts the comment
into the attribute's reason = "..." field and removes the
original comment.
Only the trailing placement counts: a same-line comment after
] is unambiguously about the attribute. A comment on the
preceding line is intentionally out of scope — it is just as
often documentation for the next item as it is attribute
rationale, and a static check cannot tell the two apart.
Doc comments (///, //!) and block comments (/* ... */)
are out of scope.
Why restrict this?
This is a stylistic preference, not a correctness issue.
reason = "..." is part of the attribute and travels with it
through every refactor; a free-floating comment can be
separated from its attribute by an unrelated edit. Compiler
diagnostics render the reason field in the lint's message,
so the rationale reaches the reader at the moment of confusion.
One canonical location for the rationale also removes the
"is this comment for the attribute, or for the next item?"
question.
activemacro 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_macros 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
Avoid:
let xs =vec![1,2,3];let ys =vec![1,2,3,];
Prefer:
let xs =vec![1,2,3,];let ys =vec![1,2,3];
Configuration
Configure via dylint.toml under ["perfectionist::macro_trailing_comma"].
extra_macros : [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_macros. Matched by final path segment, like
extra_macros. Checked first, so this knob always wins
over eligibility. Empty by default.
activenamed item cherry-picked from a prelude module instead of glob-imported
What it does
Flags a use statement that cherry-picks a named item out of a
prelude module (use serde::prelude::Serialize;) and leaves the
glob form (use serde::prelude::*;) alone. The set of segment
names treated as preludes is configurable via
prelude_segment_names (default ["prelude"]), and individual
prelude paths can be exempted with allowed_paths.
This is the dual of perfectionist::wildcard_imports: that rule
restricts globs in general but lets preludes glob freely, while
this rule restricts named imports from a prelude and lets the
glob form through.
Why restrict this?
This is a stylistic preference, not a correctness issue. A
prelude module is, by convention, a curated set of items the
crate author decided should always travel together as a glob.
Cherry-picking individual items from a prelude defeats that
intent and usually means the importer should reach into the
prelude's source module instead. A standalone import is rewritten
to the item's canonical module; a brace-list leaf
(use foo::prelude::{A, B};) — or a name that resolves through
several modules at once — is flagged with a help instead, since
a single use can't always reproduce it.
As a prelude glob: pull in the whole curated set at once.
usediesel::prelude::*;
Configuration
Configure via dylint.toml under ["perfectionist::named_prelude_imports"].
prelude_segment_names : [string]optional
Path segment names recognised as preludes. Matches the knob of
the same name on perfectionist::wildcard_imports, so a project
can flip both rules with one value. Defaults to ["prelude"].
allowed_paths : [string]optional
Prelude module paths whose named imports are never flagged — the
module path leading up to and including the prelude segment. Each
entry is absolute: an extern-crate path with a leading ::
("::diesel::prelude"), or a crate-root path written
"crate::prelude". Because an entry matches the path up to and
including the prelude segment, it must end with a
prelude_segment_names segment. Matching is exact and syntactic:
the entry must equal the
path as written in the use (up to the prelude segment), with no
re-export or alias resolution. Defaults to [].
activeborrowed parameter is only used to produce its owned form
What it does
Flags a function or method parameter taken by shared reference
(&str, &Path, &OsStr, &CStr, &[T]) whose only use in
the body is to produce its owned counterpart (String,
PathBuf, OsString, CString, Vec<T>) — via to_owned,
to_string, to_path_buf, to_vec, to_os_string, clone,
into, or String::from(..) and friends. Such a parameter
should take the owned form directly.
Only the conservative single-use case is implemented: the
parameter must be referenced exactly once, that use must be the
conversion, and the conversion must execute unconditionally — no
if / match / loop, closure, or short-circuiting && / ||
may sit between it and the enclosing function. This is
deliberately conservative: even the always-executed if
condition and match scrutinee positions count as
disqualifying, not just the branch arms. The broader
dominance-analysis cases described in
planned-rules/needless-borrowed-parameters.md are still pending.
Why restrict this?
This is a stylistic preference, not a correctness issue. Taking
the borrowed form and converting inside the body forces a copy
even when the caller already owns the value. Taking the owned
form moves the caller's value straight in; a caller that holds
only a borrow performs the very same conversion at the call site
that the borrowed signature was performing internally. The total
number of copies is identical for the worst caller and one
cheaper for the best caller.
Interaction with Clippy
clippy::needless_pass_by_value (pedantic, off by default)
covers the opposite direction: a by-value parameter that is
never consumed should be taken by reference. clippy::ptr_arg
(style, on by default) is orthogonal — it rewrites a
&String / &Vec<T> / &PathBuf parameter to the more
permissive &str / &[T] / &Path. Neither moves a borrowed
parameter to its owned form, so enabling all three gives full
coverage of the owned-vs-borrowed trade-off.
Configure via dylint.toml under ["perfectionist::needless_borrowed_parameters"].
extra_conversion_methods : [string]optional
Additional method names that count as a conversion of the
borrowed parameter to its owned form. Merged with the built-in
defaults (["to_owned", "to_string", "to_path_buf", "to_vec", "to_os_string", "clone", "into"]); empty by default. A
flagged conversion must still actually produce the owned
counterpart of the parameter's type, so listing an unrelated
method here never widens the lint beyond owned-producing calls.
ignore_conversion_methods : [string]optional
Method names to drop from the conversion set, even if they
appear in the built-in defaults or in
extra_conversion_methods. Empty by default; checked after the
merge with the built-ins, so this knob always wins.
activesplittable print macro with an embedded-newline template exceeds the configured line width
What it does
Flags a println!-style macro call whose format template
embeds a \n newline and whose source line is wider than
max_line_width display columns, and folds the template across
lines with the backslash-newline continuation escape:
println!("error: The error was caused by {err_src}\n\
hint: Run {magic_cmd} to solve the problem",);
The rewrite is byte-for-byte output-preserving: every \n
stays, and the trailing \<newline><indent> continuation
strips exactly the source newline and indentation it adds.
Eligibility is name-based — a curated list of the macros whose
output is unchanged by the fold (println!, eprintln!,
print!, eprint!, writeln!, write!, and the log family
log! / error! / warn! / info! / debug! / trace!),
replaced wholesale via target_macros. Macros that return a
value (format!, format_args!) or terminate (panic!,
assert!, the debug_assert* family, ...) are deliberately
out of scope.
A template that is a runtime expression rather than a string
literal, a raw string literal, or a template with no foldable
interior \n, is left alone.
Why restrict this?
This is a stylistic preference, not a correctness issue. A long
single line whose string already contains \n is hard to read
and hard to scan in a diff; folding it at the embedded newlines
lets each output line read as its own source line without
changing a byte of what the program prints.
Example
Avoid:
println!("error: The error was caused by {err_src}\nhint: Run {magic_cmd} to solve the problem");
Prefer:
println!("error: The error was caused by {err_src}\n\
hint: Run {magic_cmd} to solve the problem",);
Configuration
Configure via dylint.toml under ["perfectionist::overly_long_print_macro"].
max_line_width : unsigned integeroptional
Source-line width that triggers the rule. The width is the
Unicode display width of the line containing the macro
invocation, not its byte length, so a line of CJK text is
measured the way a terminal renders it. Common alternatives to
the default 100 are 80 (terminal) or 120 (wide editors).
target_macros : [string]optional
Macros eligible for folding, each a "a::b::c"-style path (no
trailing !). A single-segment entry matches by the
invocation's final segment (so "info" covers log::info!);
a multi-segment entry tail-matches the invocation path.
Replaces the built-in list wholesale when present.
Flags closure parameters whose identifier is one ASCII
letter, unless the closure is a trivial single-expression
callback or the identifier is in the conventional-name
exempt set (n for an unsigned count, f for a
fmt::Formatter, i / j / k for indices).
"Single-expression" is a shared precondition for the
trivial-callback exception: the body must be a bare
expression or a block whose only content is a trailing
expression — a body with any let binding or other
statement before the trailing expression disqualifies the
closure regardless of which branch below would otherwise
apply. Given that, one of two further shapes must hold:
the closure is the immediate argument of a call whose
callee name is in the trivial-callback method set
(sort_by, sort_by_key, min_by, max_by,
binary_search_by, cmp_by, partial_cmp_by,
fold, try_fold, ...). The set 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.
The conventional-name exempt set matches the one used by
perfectionist::single_letter_function_param: |i| ...
is the canonical index closure, just as fn step(i: usize)
is the canonical index parameter. Bodies that use the
index for slicing or arithmetic (|i| &hex[i..i + 2])
are not structurally trivial, so the exempt set is what
keeps them out of the diagnostic.
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.
Interaction with Clippy
clippy::min_ident_chars (restriction, off by default)
also flags single-character closure parameters, under a
single crate-wide threshold and one shared allow-list. It
cannot express this rule's trivial-callback exemption
(.map(|x| x.field), sort_by(|a, b| ...)) or its
conventional-name set, so it flags the short callbacks this
rule deliberately allows. Enabling both is redundant — choose
the context-aware behaviour here, or min_ident_chars for
the one-knob sweep.
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 trivial-callback
set, 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.
Additional identifiers to allow as closure 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. Each entry is a
single ASCII letter (a-z, A-Z); any other
character is rejected at config-parse time.
Identifiers to deny (always flag), removing them from the
exempt set 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.
Each entry is a single ASCII letter (a-z, A-Z);
any other character is rejected at config-parse time.
activeconst generic parameter has a single-letter name
What it does
Flags const generic parameter declarations
(<const N: usize>) whose identifier is one ASCII letter.
Why restrict this?
This is a stylistic preference, not a correctness issue.
A single-letter const generic parameter is opaque at every
use site; a descriptive identifier (LEN, COLS, LANES)
documents the parameter's role both at the declaration and
at every substitution.
Interaction with Clippy
clippy::min_ident_chars (restriction, off by default)
flags short identifiers in many positions, but explicitly
skips const generic parameters — its visitor returns early
on GenericParamKind::Const, so it never flags
<const N: usize>. This rule covers exactly that gap, so the
two are complementary rather than redundant: enable both to
also catch short bindings, parameters, and item names.
Configure via dylint.toml under ["perfectionist::single_letter_const_generic"].
allowed_idents : [single-letter string]optional
Identifiers the rule will not flag. Empty by default. Each
entry is a single ASCII letter (a-z, A-Z); any other
character is rejected at config-parse time.
Flags const items (free, associated, and block-level)
whose identifier is one ASCII letter.
Why restrict this?
This is a stylistic preference, not a correctness issue.
A single-letter const item is opaque at every use site,
and the item's scope (module-wide or crate-wide for
pub const) makes that opacity propagate. A descriptive
identifier (DIMENSION, BUFFER_LEN, MAX_RETRIES)
carries its own documentation.
Interaction with Clippy
clippy::min_ident_chars (restriction, off by default)
also flags single-character const item names, under a
single crate-wide min-ident-chars-threshold and one shared
allow-list that spans several kinds at once — bindings,
parameters, and item names. (It skips generic and
const-generic parameters, which the single_letter_* family
handles separately.) This rule governs const items alone,
with its own allowed_idents. Enabling both is redundant for
const items — choose per-kind control here, or
min_ident_chars for the one-knob sweep.
Example
Avoid:
const N:usize=2;
Prefer:
constDIMENSION_COUNT:usize=2;
Configuration
Configure via dylint.toml under ["perfectionist::single_letter_const_item"].
allowed_idents : [single-letter string]optional
Identifiers the rule will not flag. Empty by default. Each
entry is a single ASCII letter (a-z, A-Z); any other
character is rejected at config-parse time.
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.
Interaction with Clippy
clippy::min_ident_chars (restriction, off by default)
also flags single-character function parameters, under a
single crate-wide min-ident-chars-threshold and one shared
allow-list spanning several kinds at once (bindings,
parameters, item names). This rule governs parameters alone
and ships parameter-tuned default allowances (n, f, i,
j, k) plus an independent extra_allowed_idents /
extra_denied_idents pair. Enabling both is redundant for
parameters — choose per-kind control here, or
min_ident_chars for the one-knob sweep.
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. Each entry is a
single ASCII letter (a-z, A-Z); any other
character is rejected at config-parse time.
Identifiers to deny (always flag), removing them from the
exempt set 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.
Each entry is a single ASCII letter (a-z, A-Z);
any other character is rejected at config-parse time.
activegeneric type parameter has a single-letter name
What it does
Flags generic type parameters whose identifier is one ASCII
letter (T, U, K, V, ...).
Why restrict this?
This is a stylistic preference, not a correctness issue.
Single-letter generic names propagate through the type
signatures and bounds; they force every reader to scroll
back to the declaration to recover the role of each
parameter. Descriptive names (Element, Key, Reader)
keep complex signatures self-documenting.
Interaction with Clippy
clippy::min_ident_chars (restriction, off by default)
flags short identifiers in many positions, but explicitly
skips generic type parameters — its visitor returns early
on GenericParamKind::Type, so it never flags <T>. This
rule covers exactly that gap, so the two are complementary
rather than redundant: enable both to also catch short
bindings, parameters, and item names.
Single-character lifetime names ('a) are likewise out of
scope for the single_letter_* family;
clippy::single_char_lifetime_names (restriction) covers
those.
Flags let x = ...; bindings whose identifier is one ASCII
letter.
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
set of exempt identifiers for the well-worn cases
(unsigned counts).
Interaction with Clippy
clippy::min_ident_chars (restriction, off by default)
also flags single-character let bindings, under a single
crate-wide min-ident-chars-threshold and one shared
allow-list spanning several kinds at once (bindings,
parameters, item names). This rule governs let bindings
alone, with its own exempt set. Enabling both is redundant
for let bindings — choose per-kind control here, or
min_ident_chars for the one-knob sweep.
Example
Avoid:
let m = entry.metadata()?;
Prefer:
let metadata = entry.metadata()?;
Configuration
Configure via dylint.toml under ["perfectionist::single_letter_let_binding"].
Additional identifiers to allow as let binding names.
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. Each entry is a single ASCII letter
(a-z, A-Z); any other character is rejected at
config-parse time.
Identifiers to deny (always flag), removing them from the
exempt set 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.
Each entry is a single ASCII letter (a-z, A-Z);
any other character is rejected at config-parse time.
Flags static items whose identifier is one ASCII letter.
Why restrict this?
This is a stylistic preference, not a correctness issue.
A single-letter static item is opaque at every use site,
and the item's scope (module-wide or crate-wide for
pub static) makes that opacity propagate. A descriptive
identifier (BUFFER, CACHE, COUNTER) carries its own
documentation.
Interaction with Clippy
clippy::min_ident_chars (restriction, off by default)
also flags single-character static item names, under a
single crate-wide min-ident-chars-threshold and one shared
allow-list that spans several kinds at once — bindings,
parameters, and item names. (It skips generic and
const-generic parameters, which the single_letter_* family
handles separately.) This rule governs static items alone,
with its own allowed_idents. Enabling both is redundant for
static items — choose per-kind control here, or
min_ident_chars for the one-knob sweep.
Configure via dylint.toml under ["perfectionist::single_letter_static_item"].
allowed_idents : [single-letter string]optional
Identifiers the rule will not flag. Empty by default. Each
entry is a single ASCII letter (a-z, A-Z); any other
character is rejected at config-parse time.
activethiserror import, derive, or attribute; this catalogue prefers derive_more::{Display, Error}
What it does
Flags every use of thiserror in
the consumer crate. Three syntactic shapes trigger the lint:
Derives.#[derive(thiserror::Error)] directly, or
#[derive(Error)] / #[derive(te::Error)] when a sibling
use thiserror::Error; / use thiserror as te; brings the
derive macro into scope under any local name, anywhere in
the crate. #[cfg_attr(_, derive(thiserror::Error))] is
unwrapped (including nested cfg_attr).
Attributes.#[error(...)] attributes attached to an
item the rule has already classified as thiserror-derived,
on the item, its enum variants, or its fields.
#[cfg_attr(_, error(...))] is unwrapped symmetrically
with the derive side.
Imports. Every use or extern crate statement that
brings a thiserror path into scope:
use thiserror::*, use thiserror::Error,
use thiserror::Error as MyError;,
use thiserror::{self as te};, use thiserror as te;,
extern crate thiserror;, extern crate thiserror as te;,
the braced top-level form use {thiserror::Error, ...};,
and pub use re-exports.
The lint is detection-only: it emits a help-style diagnostic
pointing at the offending site and suggests migrating to
#[derive(derive_more::Display, derive_more::Error)]. There is
no autofix — the migration involves a mix of derive-list edits,
format-string positional translation (thiserror's {0} ↔
derive_more's {_0}), attribute renames (#[error(...)] ↔
#[display(...)]), and edge cases (#[error(transparent)],
#[backtrace]) whose mechanical rewrite is too risky to apply
without review.
Alias collection is crate-wide rather than per-module: a
use thiserror::Error; anywhere in the crate makes the bare
#[derive(Error)] short-hand resolve as thiserror everywhere.
In practice that overlap is rare and
the rule treats it as acceptable false-positive surface; a
project that hits it can suppress individual sites with
#[expect(perfectionist::thiserror_usage)].
Why restrict this?
This is a stylistic preference, not a correctness issue. The
catalogue picks derive_more for error formatting and source
chaining. Mixing in thiserror fragments the attribute
vocabulary across the codebase and adds a second derive crate
that has no functional capability derive_more lacks. A
project that wants the choice the other way around can disable
this rule.
Example
Avoid:
usethiserror::Error;#[derive(Debug, Error)]pubenumMyError{#[error("missing field {0}")]
MissingField(MissingFieldError),}
Prefer:
usederive_more::{Display, Error};#[derive(Debug, Display, Error)]pubenumMyError{#[display("missing field {_0}")]
MissingField(MissingFieldError),}
inactivea module import and an adjacent item import from it can be combined through self
What it does
Folds two adjacent use statements — one importing a module
and one importing an item from that same module — into a
single use module::{self, item};. The rule is inactive by
default; a project opts in via [perfectionist].enable.
Why restrict this?
This is a stylistic preference, not a correctness issue. A
project that wants a module and the items it re-exports to
travel under one use enables this rule so the grouping is
applied uniformly rather than decided case by case:
[perfectionist]enable=["uncombined_self_import"]
The autofix is MaybeIncorrect whenever it narrows the
namespaces an import brings into scope: folding a bare
use module; into {self, ...} imports only the module,
while the bare form imports every namespace named module
(type, value, macro) — a difference that matters only in the
rare case where a value or macro shares the module's name in
the same parent.
Interaction with Clippy
clippy::unnecessary_self_imports (restriction, off by
default) is the opposite policy: it flags a sole
use module::{self}; and unwraps it to use module;. It
matches only the single-item {self} form, so it never
touches the multi-item {self, item} groups this rule
produces. The two are mirror images — pick one direction and
do not enable both.
activeU+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
Avoid:
// TODO: handle the empty-tree case…
Prefer:
// TODO: handle the empty-tree case...
Configuration
Configure via dylint.toml under ["perfectionist::unicode_ellipsis_in_comments"].
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.
activeU+2026 HORIZONTAL ELLIPSIS in doc comments; prefer ...
What it does
Forbids U+2026 HORIZONTAL ELLIPSIS (…) in doc comments —
/// and //! line forms and the /** */ / /*! */ block
forms. Prefer the three-ASCII-dot form .... Regular // and
/* */ comments are covered by a sibling lint
(perfectionist::unicode_ellipsis_in_comments).
Why restrict this?
This is a stylistic preference, not a correctness issue.
ASCII ... survives every encoding round-trip, every terminal,
every copy-paste, every grep invocation, and every git diff
viewer without rendering as ? or a tofu box. The visual
difference between … and ... is small enough that the
Unicode form usually arrives by accident — autocorrect, an IDE
smart-quote setting — rather than as a deliberate choice in
technical writing.
Example
Avoid:
/// Walk the tree, collecting sizes…
Prefer:
/// Walk the tree, collecting sizes...
Configuration
Configure via dylint.toml under ["perfectionist::unicode_ellipsis_in_docs"].
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.
scan_code_spans : booleanoptional
Whether to also flag a character inside an inline code span
(`...`). Defaults to false: code spans often quote example
text where the ellipsis is meaningful, so they are left alone
unless this is set to true. Code blocks — fenced
(``` ... ```), ~~~-fenced, four-space indented, and the
doc-test code they hold — are always skipped regardless of this
knob.
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.
Interaction with Clippy
clippy::non_ascii_literal (restriction, off by default)
also catches U+2026 in a panic-family message, but only as a
side effect of forbidding all non-ASCII in string and char
literals: it cannot be narrowed to the ellipsis, offers no
... autofix, and does not reach the sibling
perfectionist::unicode_ellipsis_in_comments /
unicode_ellipsis_in_docs cases (comments and doc comments are
not literals). Use this rule for the targeted fix, or
non_ascii_literal for a blanket ASCII-only-literals policy.
Example
Avoid:
panic!("could not parse manifest…");let manifest =load().expect("config missing…");
Prefer:
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.
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.
activelint-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.
inactivetrait 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.
A cfg-gated derive written as
#[cfg_attr(<cfg>, derive(...))] is checked the same way as a
bare #[derive(...)]; the cfg predicate is left untouched.
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
is therefore inactive by default — enable it per crate by
adding to dylint.toml:
Configure via dylint.toml under ["perfectionist::unordered_derives"].
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.
activerepository URL references a branch or tag instead of a commit SHA
What it does
Flags URLs that reference a file or directory inside a hosted
git repository (GitHub, GitLab, Bitbucket, Codeberg / Gitea,
sourcehut, etc.) when the ref in the URL is a branch or tag
rather than a commit SHA. Projects that deliberately link to
version-shaped refs can opt into accepting those patterns via
allow_version_patterns. Scans doc comments and regular
comments by default; string literals are opt-in via
scan_string_literals.
This rule only concerns whether the ref is mutable; the
length of an accepted SHA is perfectionist::commit_id_length_mismatch's
concern, and perfectionist::bare_url ensures the URL is
wrapped. The three lints layer rather than overlap.
Why restrict this?
This is a stylistic preference, not a correctness issue. A
branch ref such as /blob/main/... resolves to whatever that
branch currently points at, so the linked content can change —
or disappear — without warning after the link is written. A tag
ref is steadier but still not pinned: a tag can be removed, and
a version-shaped name is not even guaranteed to be a tag (a
branch named v1.2.3 is valid Git). A commit SHA is the only
ref that always denotes the exact content the author linked to.
Example
Avoid:
/// See <https://github.com/owner/repo/blob/main/src/lib.rs>.
Prefer:
/// See <https://github.com/owner/repo/blob/8c1f6e2/src/lib.rs>.
Configuration
Configure via dylint.toml under ["perfectionist::unpinned_repo_ref"].
Scan regular comments (//, /* */). Defaults to true.
scan_string_literals : booleanoptional
Scan string literals ("...", r"..."). Defaults to false.
sha_recognition_length : unsigned integeroptional
Minimum hex length for a ref to be recognised as a commit SHA.
A pure-hex ref shorter than this is treated as a branch and
rejected. Defaults to 4 (Git's own minimum SHA length),
which trades a small false-negative window (branch names like
dead, face, beef) for fewer false positives on
branch names that merely look hex-ish. Set to 1 to treat any
pure-hex ref as a SHA.
allow_version_patterns : booleanoptional
Whether refs shaped like version patterns (1.2.3, v1.2.3, or
either form with a non-empty -suffix of ASCII letters, ASCII
digits, ., -, and _) are accepted without a
commit SHA. Defaults to false; tags can move, and a
version-shaped branch name is valid Git, so projects must opt in
to this convenience explicitly.
hosts : [HostEntry]optional
Hostnames to scan, each mapped to the forge kind that fixes its
URL shape. Defaults to the common public forges:
github.com / gitee.com (github-shape), gitlab.com,
bitbucket.org, codeberg.org (gitea-shape), and git.sr.ht.
Register a self-hosted instance by adding an entry with the
matching kind. Supplying this field replaces the built-in
list rather than extending it.
skip_hosts : [string]optional
Hostnames to skip, as *-glob patterns compared
case-insensitively against the URL host. A host matching any
pattern here is ignored even when it also appears in hosts.
Defaults to [].
Types
HostEntrystruct
One row of the hosts table: a hostname (or glob) and the forge
kind whose URL shape applies to it.
hostname : string
Hostname to match, compared case-insensitively. A * wildcard
matches any run of characters, so gitlab.*.example.com
covers every subdomain in one entry.
kind : ForgeKind
Forge kind whose URL shape applies to this hostname: one of
github (also covers gitee), gitlab, bitbucket, gitea
(also covers Codeberg / Forgejo), or sourcehut.
activeglob (*) import in a module body, outside the prelude and root-re-export exceptions
What it does
Flags glob (*) use statements — use foo::bar::*; — in module
bodies. Two exceptions are enabled by default and each can be
turned off individually:
prelude — a glob whose final non-glob path segment names a
prelude module is allowed: use rayon::prelude::*;,
use diesel::prelude::*;. The recognised names are configurable
via prelude_segment_names.
root_reexport — a re-export glob (pub use ...::*) at the top
level of a module body is allowed: pub use submodule::*; in
lib.rs.
The case the rule is most concerned with is use super::*; inside
a #[cfg(test)] mod tests block; explicit imports must replace it.
A project that wants a stricter posture disables either or both
exceptions, or names extra always-allowed paths, in dylint.toml.
Why restrict this?
This is a stylistic preference, not a correctness issue. A glob
import compiles and runs correctly; the project simply prefers
explicit imports. Naming each imported item keeps a module's
dependencies visible at the top of the file, stops an upstream
addition from silently shadowing a local name, and makes a
grep for where a name comes from land on a real use. Preludes
and root re-exports are the two places the glob form is
idiomatic, so they are exempt by default.
Why not clippy::wildcard_imports?
Clippy's wildcard_imports (an allow-by-default pedantic lint)
flags the same glob uses and, by default, exempts prelude
imports, pub use re-exports, and use super::*; inside any
module whose name contains test — overlapping this rule's
exceptions. But all of those exemptions, together with the
allowed-wildcard-imports path list, are coupled behind a single
warn-on-all-wildcard-imports boolean: left at its default
(false) the test-module super::*; carve-out is on, so
use super::*; inside mod tests is not flagged; set to true,
every exemption drops at once, including the prelude one. There is
no setting that keeps prelude::* exempt while flagging
use super::*; in a #[cfg(test)] mod tests block — which is
exactly this rule's headline case. This rule decouples them: the
prelude and root_reexport exceptions toggle independently
(with a configurable prelude_segment_names and the allowed_paths
escape hatch), and there is no test-module carve-out, so the test
super::*; glob is flagged by default. Reach for
clippy::wildcard_imports if its coarser, all-or-nothing exemption
model is enough; reach for this rule when the test-module
distinction matters. Paired with
perfectionist::named_prelude_imports it expresses the project's
full posture: preludes must be glob-imported, and globs are
allowed only for preludes.
Not flagged: the prelude and root-re-export exceptions.
userayon::prelude::*;pubusesubmodule::*;
Configuration
Configure via dylint.toml under ["perfectionist::wildcard_imports"].
prelude_exception : booleanoptional
Whether a glob whose final non-glob path segment names a prelude
module (use rayon::prelude::*;) is exempt. The recognised
segment names come from prelude_segment_names. Defaults to
true; set false to flag prelude globs too.
root_reexport_exception : booleanoptional
Whether a bare-pub re-export glob (pub use submodule::*;) at
the top level of a module body is exempt. Defaults to true; set
false to flag re-export globs too.
prelude_segment_names : [string]optional
Path segment names recognised as preludes for the prelude
exception. Defaults to ["prelude"].
allowed_paths : [string]optional
Module paths whose glob import is never flagged, regardless of the
exceptions above — the path before the ::* of a use <path>::*.
Each entry is absolute: an extern-crate path with a leading
:: ("::rayon::iter", which exempts both use rayon::iter::*;
and use ::rayon::iter::*;), or a crate-root path written
"crate::...". Matching is exact and syntactic: the entry must
equal the path as written in the glob use, with no re-export or
alias resolution. Defaults to [].