perfectionist lints

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

Index

LintDefaultDescription
arc_rc_cloneenabledcalling .clone() on an Arc<T> or Rc<T>; prefer the qualified Arc::clone / Rc::clone form
derive_orderingdisabledtrait names in a #[derive(...)] list are not in the configured order
flat_module_patternenabledsubmodule defined as module/mod.rs; prefer the flat module.rs layout
macro_argument_bindingenabledmacro invocation passes an impure expression that should be bound to a let first
macro_trailing_commaenabledmacro invocation does not follow rustfmt's vertical trailing-comma policy
non_exhaustive_errordisablederror-shaped type is missing #[non_exhaustive]
prefer_raw_stringenabledstring literal contains only raw-expressible escapes; prefer the raw-string form
single_letter_closure_paramenabledclosure parameter has a single-letter name
single_letter_function_paramenabledfunction parameter has a single-letter name
single_letter_genericenabledgeneric type parameter has a single-letter name
single_letter_let_bindingenabledlet binding has a single-letter name
unicode_ellipsis_in_commentsenabledU+2026 HORIZONTAL ELLIPSIS in non-doc comments; prefer ...
unicode_ellipsis_in_panic_messagesenabledU+2026 HORIZONTAL ELLIPSIS in panic / assertion / expect messages; prefer ...
unknown_perfectionist_lintsenabledlint-control attribute references a perfectionist::* lint that this plugin does not register

Rules

perfectionist::arc_rc_clone↑ top

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

What it does

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

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

Why restrict this?

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

Example

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

Use instead:

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

Configuration: none.

Source: src/rules/arc_rc_clone.rs

perfectionist::derive_ordering↑ top

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

What it does

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

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

Why restrict this?

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

The opinion is opt-in: a project that doesn't want to commit to a single ordering shouldn't have to set anything. The rule therefore ships disabled by default — enable it per crate by adding to dylint.toml:

[perfectionist]
enable = ["derive_ordering"]

Example

Under style = "alphabetical":

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

Use instead:

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

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

style : Style optional

Ordering policy. Defaults to 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

Style enum

"alphabetical" (Rust: Alphabetical)

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

"prefix_then_alphabetical" (Rust: PrefixThenAlphabetical)

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

Source: src/rules/derive_ordering.rs

perfectionist::flat_module_pattern↑ top

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

What it does

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

Why restrict this?

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

Example

// Bad
src/foo/mod.rs

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

Configuration: none.

Source: src/rules/flat_module_pattern.rs

perfectionist::macro_argument_binding↑ top

enabledmacro invocation passes an impure expression that should be bound to a let first

What it does

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

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

Why is this bad?

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

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

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

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

Terminology

In this rule, pure means safe for the surrounding macro to drop or duplicate: evaluating the argument zero, one, or many times is observationally equivalent. Impure is anything else, and is what the rule flags.

The classification is syntactic: the rule recognises a curated set of shapes known to satisfy the property and treats everything else as impure. A const fn call, a Result::map chain over a pure base, or vec.fold(...) is therefore impure under this rule unless its shape is recognised — the lint cannot prove side-effect-freedom in general, only spot it. The trade-off favours flagging side-effect-free expressions over silently passing a real hazard. The set is narrower than the functional-programming notion of purity and is keyed to what a macro can actually do with its captures, not to side-effect-freedom in the abstract.

The recognised pure shapes are: literals, paths, field accesses, indexing of pure bases, dereferences, references, casts, the unit literal (), parenthesised / tuple / array-literal / array-repeat groups whose elements are all pure, binary chains of pure operands joined by side-effect-free operators, zero-arg method calls whose name is in the curated pure-getter set (len, is_empty, as_str, as_bytes, as_ref, as_mut, as_deref, as_slice, plus anything in extra_pure_methods), and calls to core / std macros whose expansion is a compile- time constant (concat!, env!, option_env!, include_str!, include_bytes!, stringify!, cfg!, line!, column!, file!, module_path!, plus anything in extra_pure_macros). A comparison like vec.len() <= cap evaluates the same way regardless of how many times the macro touches it, so binding it to a let would only force the comparison to run in release builds for no benefit; the same logic applies to env!("HOME") inside debug_assert_eq!(...) — there is nothing to evaluate at runtime.

Example

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

Use instead:

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

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

mode : Mode optional

Eligibility mode.

deny_extra : [string] optional

Macros added to the built-in deny list. Each entry is a fully-qualified macro path (no trailing !) or a bare macro name to match by final segment only.

allow_extra : [string] optional

Macros added to the built-in allow list. Same matching rules as deny_extra. Only meaningful in AllowAndDeny and Blanket modes; in DenyOnly the allow list is unused.

ignore : [string] optional

Macros to skip entirely, regardless of which list they would otherwise hit. Same matching rules as deny_extra.

extra_pure_methods : [string] optional

Method names added to the built-in pure-method list. Each entry is a bare method identifier (no (), no receiver). A .method() invocation on a pure base is then accepted as a pure postfix when the method takes no arguments. Add a project-local method here only when it is genuinely safe for the surrounding macro to drop or duplicate the call (the rule's working definition of pure) — typically an O(1) side-effect-free getter that the lint's syntactic classification can't otherwise see.

ignore_pure_methods : [string] optional

Method names to drop from the pure-method list, even if they appear in the built-in defaults or in extra_pure_methods. Empty by default; checked after the merge, so this knob always wins. Useful for opting back into linting on a default entry the project does not consider pure — for example, removing as_ref for a project that wraps it in an impure implementation.

extra_pure_macros : [string] optional

Macro names added to the built-in pure-macro list. Each entry is matched against the invocation's final path segment (so my_crate::const_str matches by the "const_str" tail). A pure-macro call passed as an argument to another macro is treated as a pure atom — the rule does not propose binding it to a let. Use this knob for project-specific macros whose expansion is a compile-time constant (a literal, a &'static str, a bool); their inclusion satisfies the rule's pure-as-drop-or-duplicate-safe definition trivially, since there is no runtime expression for the surrounding macro to drop or duplicate.

ignore_pure_macros : [string] optional

Macro names to drop from the pure-macro list, even if they appear in the built-in defaults or in extra_pure_macros. Checked after the merge, so this knob always wins.

Types

Mode enum

Eligibility mode. The default is AllowAndDeny. The matcher-based mode described in planned-rules/macro-argument-binding.md is not yet implemented and is therefore not exposed as a value here; a dylint.toml that names it will fail to deserialise with a useful error.

"deny_only" (Rust: DenyOnly)

Flag only invocations of the curated deny list (debug_assert* plus deny_extra). Every other macro is silently accepted.

"blanket" (Rust: Blanket)

Flag every function-like or array-like invocation that carries an impure top-level argument, regardless of any built-in classification — unless the invocation matches an allow_extra entry. The built-in allow list is deliberately ignored in this mode; project exceptions go in allow_extra.

"allow_and_deny" (Rust: AllowAndDeny)

Curated deny list plus curated allow list, both extensible via deny_extra / allow_extra. Macros on neither list are flagged — flagging unrecognised macros is deliberate so the rule remains useful in projects that depend on uncatalogued proc macros.

Source: src/rules/macro_argument_binding.rs

perfectionist::macro_trailing_comma↑ top

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

What it does

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

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

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

Why restrict this?

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

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

Example

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

Use instead:

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

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

name_based_extra : [string] optional

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

ignore : [string] optional

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

Source: src/rules/macro_trailing_comma.rs

perfectionist::non_exhaustive_error↑ top

disablederror-shaped type is missing #[non_exhaustive]

What it does

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

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

Why restrict this?

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

The opinion is opt-in: some projects deliberately use exhaustive error enums to force downstream consumers to handle every new variant, and binary crates have no SemVer surface to protect. The rule therefore ships disabled by default — enable it per crate by adding to dylint.toml:

[perfectionist]
enable = ["non_exhaustive_error"]

Example

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

Use instead:

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

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

require_for : RequireFor optional

Visibility threshold for the rule.

extra_suffixes : [string] optional

Additional identifier suffixes that mark a type as "an error" purely by name, without inspecting its trait implementations. Merged with the built-in defaults (["Error"]); empty by default. List project-specific vocabulary here (Failure, Fault, …) without having to re-state the standard suffix.

ignore_suffixes : [string] optional

Identifier suffixes to drop from the allowlist, even if they appear in the built-in defaults or in extra_suffixes. Empty by default; checked after the merge with the built-ins, so this knob always wins. Use it when a project deliberately does not want the Error suffix to trigger the by-name branch — types that implement std::error::Error are still flagged via the trait branch.

Types

RequireFor enum

"pub" (Rust: Pub)

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

"pub_crate" (Rust: PubCrate)

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

"all" (Rust: All)

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

Source: src/rules/non_exhaustive_error.rs

perfectionist::prefer_raw_string↑ top

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

What it does

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

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

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

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

Why restrict this?

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

Example

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

Use instead:

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

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

min_escapes_to_trigger : unsigned integer optional

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

escapes_eligible : [string] optional

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

Source: src/rules/prefer_raw_string.rs

perfectionist::single_letter_closure_param↑ top

enabledclosure parameter has a single-letter name

What it does

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

Why restrict this?

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

Example

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

Use instead:

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

Configure via dylint.toml under ["perfectionist::single_letter_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.

ignore_trivial_callback_methods : [string] optional

Method / function names to drop from the allowlist, even if they appear in the built-in defaults or in extra_trivial_callback_methods. Empty by default; checked after the merge with the built-ins, so this knob always wins. Useful for opting back into linting on a default entry the project does not consider trivial.

Source: src/rules/single_letter_closure_param.rs

perfectionist::single_letter_function_param↑ top

enabledfunction parameter has a single-letter name

What it does

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

Why restrict this?

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

Example

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

Use instead:

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

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

extra_allowed_idents : [string] optional

Additional identifiers to allow as function or method parameter names. Merged with the built-in defaults (["n", "f", "i", "j", "k"]); empty by default. Use this to whitelist project-specific conventional names without having to re-state the standard ones.

ignore_allowed_idents : [string] optional

Identifiers to drop from the allowlist, even if they appear in the built-in defaults or in extra_allowed_idents. Empty by default; checked after the merge with the built-ins, so this knob always wins.

Source: src/rules/single_letter_function_param.rs

perfectionist::single_letter_generic↑ top

enabledgeneric type parameter has a single-letter name

What it does

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

Why restrict this?

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

Example

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

Use instead:

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

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

short_impl_max_lines : unsigned integer optional

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

Source: src/rules/single_letter_generic.rs

perfectionist::single_letter_let_binding↑ top

enabledlet binding has a single-letter name

What it does

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

Why restrict this?

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

Example

let m = entry.metadata()?;

Use instead:

let metadata = entry.metadata()?;
Configuration

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

extra_allowed_idents : [string] optional

Additional identifiers to allow as let binding names, even outside #[cfg(test)] code. Merged with the built-in defaults (["n"]); empty by default. Use this to whitelist project-specific conventional names without having to re-state the standard ones.

ignore_allowed_idents : [string] optional

Identifiers to drop from the allowlist, even if they appear in the built-in defaults or in extra_allowed_idents. Empty by default; checked after the merge with the built-ins, so this knob always wins.

Source: src/rules/single_letter_let_binding.rs

perfectionist::unicode_ellipsis_in_comments↑ top

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

What it does

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

Why restrict this?

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

Example

// TODO: handle the empty-tree case…

Use instead:

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

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

also_flag : [string] optional

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

scope : [Scope] optional

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

Types

Scope enum

Selector for which comment syntaxes the rule scans.

"line" (Rust: Line)

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

"block" (Rust: Block)

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

Source: src/rules/unicode_ellipsis_in_comments.rs

perfectionist::unicode_ellipsis_in_panic_messages↑ top

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

What it does

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

Why restrict this?

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

Example

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

Use instead:

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

Custom macros

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

Configuration

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

extra_macros : [string] optional

Additional macros whose call site should be scanned for the flagged characters. Merged with the built-in defaults (the standard panic and assertion macros — panic, unimplemented, todo, unreachable, debug_unreachable, and the assert* family); empty by default. Use this to add project-specific assertion-shaped macros without having to re-state the standard ones.

ignore_macros : [string] optional

Macros to drop from the scanned set, even if they appear in the built-in defaults or in extra_macros. Empty by default; checked after the merge with the built-ins, so this knob always wins. Use it when a project deliberately uses in one of the default macros.

extra_methods : [string] optional

Additional method names on Option / Result whose first argument is the panic message. Merged with the built-in defaults (expect, expect_err); empty by default. Use this to add project-specific expect-shaped wrappers without having to re-state the standard pair.

ignore_methods : [string] optional

Methods to drop from the scanned set, even if they appear in the built-in defaults or in extra_methods. Empty by default; checked after the merge with the built-ins, so this knob always wins.

also_flag : [string] optional

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

Source: src/rules/unicode_ellipsis_in_panic_messages.rs

perfectionist::unknown_perfectionist_lints↑ top

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

What it does

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

Why is this bad?

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

Example

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

Use instead:

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

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

suggestion_distance : unsigned integer optional

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

Source: src/rules/unknown_perfectionist_lints.rs