From d3dc47fa1d19d19a9547a42889cc1f2959349d63 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 22:06:16 +0000 Subject: [PATCH 01/13] chore(deps): upgrade `perfectionist` and enable `non_exhaustive_error` Bump `perfectionist` past `0.0.0-rc.7` to its current `master` HEAD, which introduces the `non_exhaustive_error` rule and a suite of `single_letter_*` rules. Wire `non_exhaustive_error` in as a deny-level lint at the crate root and address the violations it surfaces: - Add `#[non_exhaustive]` to `args::fraction::ConversionError` and `args::fraction::FromStrError`. - Drop the unused `derive_more::Error` derive from `bytes_format::parsed_value::ParsedValue`. The enum is a formatter return value rather than an error type, so it no longer participates in the rule. The new `single_letter_*` rules also surface unrelated violations, addressed as follows: - Rename trait-method parameters `a` / `b` to `path` / `prefix` in `app::overlapping_arguments::Api::starts_with`, and rename the closure parameter `r` to `range` in `usage_md`. - Configure the rule's `comparison_methods` allowlist in `dylint.toml` to include the project-specific helper `sort_reflection_by` and the `into-sorted` crate's `into_sorted_by`, both of which take comparison closures whose `|a, b|` parameters CLAUDE.md explicitly permits. - Allow `single_letter_let_binding` at the `args` module level because `clap_derive`'s `default_value_t` expansion produces a `let s = ...` binding that is outside our control. Register the `perfectionist` tool and gate the deny directive on `cfg(dylint_lib = "perfectionist")` so non-dylint builds do not see an unknown lint tool. Declare the matching `check-cfg` entry in `Cargo.toml` to keep `unexpected_cfgs` quiet on ordinary builds. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- Cargo.toml | 3 ++ dylint.toml | 29 ++++++++++++++++++- src/app/overlapping_arguments.rs | 6 ++-- .../test_remove_overlapping_paths.rs | 4 +-- src/args.rs | 8 +++++ src/args/fraction.rs | 2 ++ src/bytes_format/parsed_value.rs | 4 +-- src/lib.rs | 6 ++++ src/usage_md.rs | 2 +- tests/_utils.rs | 2 +- 10 files changed, 56 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d5e761cc..83dd73d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,9 @@ terminal_size = "0.4.4" text-block-macros = "0.2.0" zero-copy-pads = "0.2.0" +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(dylint_lib, values("perfectionist"))'] } + [dev-dependencies] build-fs-tree = "0.8.1" command-extra = "1.0.0" diff --git a/dylint.toml b/dylint.toml index d91405ef..aadf026e 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,4 +1,31 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.7" }, + { git = "https://github.com/KSXGitHub/perfectionist", rev = "3acd734c12fbd56d2c760f3c7bc9a19e45e62541" }, +] + +["perfectionist::single_letter_names"] +# Treat project-specific and third-party comparison-style helpers as if +# they were standard `sort_by`-shaped methods, so the idiomatic +# `|a, b| a.field.cmp(&b.field)` closure does not trip the rule. The +# defaults are included explicitly because setting this list replaces +# the built-in allowlist rather than extending it. +comparison_methods = [ + "sort_by", + "sort_unstable_by", + "sort_by_key", + "sort_unstable_by_key", + "min_by", + "max_by", + "min_by_key", + "max_by_key", + "binary_search_by", + "binary_search_by_key", + "cmp_by", + "partial_cmp_by", + "eq_by", + "fold", + # Project-specific helper used in integration tests. + "sort_reflection_by", + # `into-sorted` crate's consuming sort. + "into_sorted_by", ] diff --git a/src/app/overlapping_arguments.rs b/src/app/overlapping_arguments.rs index ee2966a3..328b55bb 100644 --- a/src/app/overlapping_arguments.rs +++ b/src/app/overlapping_arguments.rs @@ -14,7 +14,7 @@ pub trait Api { type RealPathError; fn canonicalize(path: &Self::Argument) -> Result; fn is_real_dir(path: &Self::Argument) -> bool; - fn starts_with(a: &Self::RealPath, b: &Self::RealPath) -> bool; + fn starts_with(path: &Self::RealPath, prefix: &Self::RealPath) -> bool; } /// Implementation of [`Api`] that interacts with the real system. @@ -36,8 +36,8 @@ impl Api for RealApi { } #[inline] - fn starts_with(a: &Self::RealPath, b: &Self::RealPath) -> bool { - a.starts_with(b) + fn starts_with(path: &Self::RealPath, prefix: &Self::RealPath) -> bool { + path.starts_with(prefix) } } diff --git a/src/app/overlapping_arguments/test_remove_overlapping_paths.rs b/src/app/overlapping_arguments/test_remove_overlapping_paths.rs index 2a8a0e3c..7cbab6d2 100644 --- a/src/app/overlapping_arguments/test_remove_overlapping_paths.rs +++ b/src/app/overlapping_arguments/test_remove_overlapping_paths.rs @@ -64,8 +64,8 @@ impl Api for MockedApi { .all(|(link, _)| PathBuf::from(link).normalize() != path) } - fn starts_with(a: &Self::RealPath, b: &Self::RealPath) -> bool { - a.starts_with(b) + fn starts_with(path: &Self::RealPath, prefix: &Self::RealPath) -> bool { + path.starts_with(prefix) } } diff --git a/src/args.rs b/src/args.rs index b2713d9f..4e1d1540 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,3 +1,11 @@ +#![cfg_attr( + dylint_lib = "perfectionist", + allow( + perfectionist::single_letter_let_binding, + reason = "the `let s` bindings flagged by this lint originate in `clap_derive` macro expansion of `default_value_t` and are outside our control", + ) +)] + pub mod depth; pub mod fraction; pub mod quantity; diff --git a/src/args/fraction.rs b/src/args/fraction.rs index 4145b74f..a481308d 100644 --- a/src/args/fraction.rs +++ b/src/args/fraction.rs @@ -11,6 +11,7 @@ pub struct Fraction(f32); /// Error that occurs when calling [`Fraction::new`]. #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Error)] +#[non_exhaustive] pub enum ConversionError { /// Provided value is greater than or equal to 1. #[display("greater than or equal to 1")] @@ -43,6 +44,7 @@ impl TryFrom for Fraction { /// Error that occurs when parsing a string as [`Fraction`]. #[derive(Debug, Display, Clone, PartialEq, Eq, Error)] +#[non_exhaustive] pub enum FromStrError { ParseFloatError(ParseFloatError), Conversion(ConversionError), diff --git a/src/bytes_format/parsed_value.rs b/src/bytes_format/parsed_value.rs index 89d8d3e0..d8aad461 100644 --- a/src/bytes_format/parsed_value.rs +++ b/src/bytes_format/parsed_value.rs @@ -1,7 +1,7 @@ -use derive_more::{Display, Error}; +use derive_more::Display; /// Return value of [`Formatter::parse_value`](super::Formatter::parse_value). -#[derive(Debug, Display, Clone, Copy, Error)] +#[derive(Debug, Display, Clone, Copy)] pub enum ParsedValue { /// When input value is less than `scale_base`. #[display("{value} ")] diff --git a/src/lib.rs b/src/lib.rs index f5a4d044..04ecb231 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,12 @@ //! [`tree_builder::TreeBuilder`], [`data_tree::DataTree`], or [`visualizer::Visualizer`]. #![deny(warnings)] +#![cfg_attr(dylint_lib = "perfectionist", feature(register_tool))] +#![cfg_attr(dylint_lib = "perfectionist", register_tool(perfectionist))] +#![cfg_attr( + dylint_lib = "perfectionist", + deny(perfectionist::non_exhaustive_error) +)] #[cfg(feature = "json")] pub use serde; diff --git a/src/usage_md.rs b/src/usage_md.rs index 0e6e6445..d718568d 100644 --- a/src/usage_md.rs +++ b/src/usage_md.rs @@ -85,7 +85,7 @@ fn render_argument(out: &mut String, arg: &Arg) { .unwrap_or_else(|| arg.get_id().as_str()); let is_multiple = arg .get_num_args() - .map(|r| r.max_values() > 1) + .map(|range| range.max_values() > 1) .unwrap_or(false); let display_name = if arg.is_required_set() { if is_multiple { diff --git a/tests/_utils.rs b/tests/_utils.rs index 1d30da0b..c2ba7e8b 100644 --- a/tests/_utils.rs +++ b/tests/_utils.rs @@ -466,7 +466,7 @@ impl<'a> Default for CommandList<'a> { /// Initialize a list with one `pdu` command. fn default() -> Self { CommandRepresentation::default() - .pipe(|x| vec![x]) + .pipe(|command| vec![command]) .pipe(CommandList) } } From 0d3465ceae68ded60f07f2be45d103537fdb136d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:23:31 +0000 Subject: [PATCH 02/13] chore(deps): upgrade `perfectionist` and fix new lint violations Bump `perfectionist` from `3acd734` to `a21a146`. The upstream delta adds three rules: `arc_rc_clone`, `derive_ordering`, and `prefer_raw_string`. `derive_ordering` is silent; the other two surface reasonable violations: - `arc_rc_clone`: switch `progress.clone()` to `Arc::clone(&progress)` in `reporter::progress_and_error_reporter` to make the cheap refcount bump explicit. - `prefer_raw_string`: rewrite the three string literals that embed escaped quotes as raw strings, in `reporter::error_report::text_report` and `tests::cli_errors`. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 2 +- src/reporter/error_report/text_report.rs | 2 +- src/reporter/progress_and_error_reporter.rs | 2 +- tests/cli_errors.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dylint.toml b/dylint.toml index aadf026e..b823bc1f 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,6 +1,6 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", rev = "3acd734c12fbd56d2c760f3c7bc9a19e45e62541" }, + { git = "https://github.com/KSXGitHub/perfectionist", rev = "a21a146eff0ff5b61571160d287fec100879046d" }, ] ["perfectionist::single_letter_names"] diff --git a/src/reporter/error_report/text_report.rs b/src/reporter/error_report/text_report.rs index 48203d6b..75510e58 100644 --- a/src/reporter/error_report/text_report.rs +++ b/src/reporter/error_report/text_report.rs @@ -42,6 +42,6 @@ fn test() { ), }; let actual = TextReport(report).to_string(); - let expected = "[error] read_dir \"path/to/a/directory\": Something goes wrong (os error 420)"; + let expected = r#"[error] read_dir "path/to/a/directory": Something goes wrong (os error 420)"#; assert_eq!(actual, expected); } diff --git a/src/reporter/progress_and_error_reporter.rs b/src/reporter/progress_and_error_reporter.rs index 529f1dc3..04e3daca 100644 --- a/src/reporter/progress_and_error_reporter.rs +++ b/src/reporter/progress_and_error_reporter.rs @@ -47,7 +47,7 @@ where ReportProgress: Fn(ProgressReport) + Send + Sync + 'static, { let progress = Arc::new(ProgressReportState::default()); - let progress_thread = progress.clone(); + let progress_thread = Arc::clone(&progress); let progress_reporter_handle = spawn(move || { loop { sleep(progress_report_interval); diff --git a/tests/cli_errors.rs b/tests/cli_errors.rs index 5c863ff8..0f9a7b3f 100644 --- a/tests/cli_errors.rs +++ b/tests/cli_errors.rs @@ -168,8 +168,8 @@ fn fs_errors() { .map(|line| line.trim_start_matches('\r')) .collect(); let expected_stderr_lines = btreeset! { - "[error] read_dir \"./nested/0\": Permission denied (os error 13)", - "[error] read_dir \"./empty-dir\": Permission denied (os error 13)", + r#"[error] read_dir "./nested/0": Permission denied (os error 13)"#, + r#"[error] read_dir "./empty-dir": Permission denied (os error 13)"#, }; assert_eq!(actual_stderr_lines, expected_stderr_lines); From 2c9f3c66be5c4119c178b5bc5fcd077e8cc1674b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 10:41:12 +0000 Subject: [PATCH 03/13] chore(deps): upgrade `perfectionist` to `0.0.0-rc.9` The new rule `macro_argument_binding` flags 25 sites across the codebase. Investigation showed every single one to be a false positive belonging to one of five upstream bug classes, filed as #415, #416, #417, #418, and #419. None of the suggested "bind to a `let` first" fixes are correct here: - The 13 `debug_assert_op!` / `debug_assert_op_expr!` sites would have their arguments forced to run in release builds (#415), defeating the entire point of `debug_assert_*`. - The `make_const!` and `bump!` matchers (#416) use definition / compound-assignment shapes that have no `let`-binding form. - `app::App::run` passes the unit literal `()`, which the rule's grammar fails to recognise as trivial (#417). - `debug_assert_op_expr!`'s middle operator argument (`==`, `>`) is a bare token, not an expression at all (#418). - The late pass anchors item-position macro violations at the crate root (#419), which is why module-scope `#[expect]` cannot suppress them and `proportion_bar.rs`'s `make_const!` macro had to be expanded into five `pub const` lines by hand. Apply `#[cfg_attr(dylint_lib = "perfectionist", expect(...))]` at the smallest scope each false positive permits (function scope where the macro call is statement-position, crate root in `tests/bytes_format.rs` where the call is item-position). Rewrite the `link!` / `symlink!` macros in `tests/_utils.rs` as closures so the test-utility module needs no suppression. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 2 +- src/app.rs | 7 +++ src/data_tree/hardlink.rs | 7 +++ src/reporter/progress_and_error_reporter.rs | 7 +++ src/visualizer/methods/bar_table.rs | 7 +++ src/visualizer/methods/initial_table.rs | 7 +++ src/visualizer/methods/tree_table.rs | 7 +++ src/visualizer/proportion_bar.rs | 16 ++--- tests/_utils.rs | 68 ++++++++++++--------- tests/bytes_format.rs | 10 +++ 10 files changed, 97 insertions(+), 41 deletions(-) diff --git a/dylint.toml b/dylint.toml index b823bc1f..f7477f03 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,6 +1,6 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", rev = "a21a146eff0ff5b61571160d287fec100879046d" }, + { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.9" }, ] ["perfectionist::single_letter_names"] diff --git a/src/app.rs b/src/app.rs index 42d6e6e3..a2b712ef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -39,6 +39,13 @@ impl App { } /// Run the application. + #[cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::macro_argument_binding, + reason = "the unit literal `()` is the canonical trivial value, but the rule's trivial-expression grammar does not yet accept parenthesised forms; see #417", + ) + )] pub fn run(mut self) -> Result<(), RuntimeError> { // DYNAMIC DISPATCH POLICY: // diff --git a/src/data_tree/hardlink.rs b/src/data_tree/hardlink.rs index 8cbf93b8..76cab0ba 100644 --- a/src/data_tree/hardlink.rs +++ b/src/data_tree/hardlink.rs @@ -12,6 +12,13 @@ where { /// Reduce the size of the directories that have hardlinks. #[cfg_attr(not(unix), expect(unused))] + #[cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::macro_argument_binding, + reason = "binding a `debug_assert_op!` argument to a `let` forces it to run in release builds, which defeats the entire point of `debug_assert_*`; see #415", + ) + )] pub(crate) fn par_deduplicate_hardlinks(&mut self, hardlink_info: &[(Size, Vec<&Path>)]) { if hardlink_info.is_empty() { return; diff --git a/src/reporter/progress_and_error_reporter.rs b/src/reporter/progress_and_error_reporter.rs index 04e3daca..101d2637 100644 --- a/src/reporter/progress_and_error_reporter.rs +++ b/src/reporter/progress_and_error_reporter.rs @@ -79,6 +79,13 @@ where ReportError: Fn(ErrorReport) + Sync, u64: Into, { + #[cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::macro_argument_binding, + reason = "the `bump!` macro's `($field:ident += $delta:expr)` matcher uses structural assignment syntax rather than a single expression, so binding the argument to a `let` is not even syntactically possible; see #416", + ) + )] fn report(&self, event: Event) { use Event::*; let ProgressAndErrorReporter { diff --git a/src/visualizer/methods/bar_table.rs b/src/visualizer/methods/bar_table.rs index 1adbe225..1c71d423 100644 --- a/src/visualizer/methods/bar_table.rs +++ b/src/visualizer/methods/bar_table.rs @@ -12,6 +12,13 @@ pub(super) struct BarRow { pub(super) proportion_bar: ProportionBar, } +#[cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::macro_argument_binding, + reason = "the flagged sites are all `debug_assert_op!` / `debug_assert_op_expr!` calls on side-effect-free locals; binding to a `let` would defeat the debug-only contract (see #415), and the rule also miscategorises bare operator tokens like `==` as expressions (see #418)", + ) +)] pub(super) fn render_bars<'a, Name, Size>( tree_table: TreeTable<&'a Name, Size>, total: u64, diff --git a/src/visualizer/methods/initial_table.rs b/src/visualizer/methods/initial_table.rs index 31580115..1409bd56 100644 --- a/src/visualizer/methods/initial_table.rs +++ b/src/visualizer/methods/initial_table.rs @@ -37,6 +37,13 @@ impl InitialColumnWidth { pub(super) type InitialTable = Table, InitialColumnWidth>; +#[cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::macro_argument_binding, + reason = "the flagged sites are `debug_assert_op!` calls on side-effect-free locals; binding to a `let` would defeat the debug-only contract. See #415", + ) +)] pub(super) fn render_initial( visualizer: Visualizer<'_, Name, Size>, ) -> InitialTable<&'_ Name, Size> diff --git a/src/visualizer/methods/tree_table.rs b/src/visualizer/methods/tree_table.rs index 6419ee51..b8adde15 100644 --- a/src/visualizer/methods/tree_table.rs +++ b/src/visualizer/methods/tree_table.rs @@ -41,6 +41,13 @@ impl TreeColumnWidth { pub(super) type TreeTable = Table, TreeColumnWidth>; +#[cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::macro_argument_binding, + reason = "the flagged sites are `debug_assert_op!` / `debug_assert_op_expr!` calls; binding to a `let` would defeat the debug-only contract (see #415), and the rule miscategorises bare operator tokens like `==` and `>` as expressions (see #418)", + ) +)] pub(super) fn render_tree<'a, Name, Size>( visualizer: Visualizer<'a, Name, Size>, initial_table: InitialTable<&'a Name, Size>, diff --git a/src/visualizer/proportion_bar.rs b/src/visualizer/proportion_bar.rs index 9e9cd19a..b8ff978a 100644 --- a/src/visualizer/proportion_bar.rs +++ b/src/visualizer/proportion_bar.rs @@ -7,17 +7,11 @@ use std::fmt::{Display, Error, Formatter}; #[derive(Debug, Clone, Copy, PartialEq, Eq, AsRef, Deref, Display, Into)] pub struct ProportionBarBlock(char); -macro_rules! make_const { - ($name:ident = $content:literal) => { - pub const $name: ProportionBarBlock = ProportionBarBlock($content); - }; -} - -make_const!(LEVEL0_BLOCK = '█'); -make_const!(LEVEL1_BLOCK = '▓'); -make_const!(LEVEL2_BLOCK = '▒'); -make_const!(LEVEL3_BLOCK = '░'); -make_const!(LEVEL4_BLOCK = ' '); +pub const LEVEL0_BLOCK: ProportionBarBlock = ProportionBarBlock('█'); +pub const LEVEL1_BLOCK: ProportionBarBlock = ProportionBarBlock('▓'); +pub const LEVEL2_BLOCK: ProportionBarBlock = ProportionBarBlock('▒'); +pub const LEVEL3_BLOCK: ProportionBarBlock = ProportionBarBlock('░'); +pub const LEVEL4_BLOCK: ProportionBarBlock = ProportionBarBlock(' '); /// Proportion bar. #[derive(Debug, Clone, Copy, PartialEq, Eq, From, Into)] diff --git a/tests/_utils.rs b/tests/_utils.rs index c2ba7e8b..4de50ef7 100644 --- a/tests/_utils.rs +++ b/tests/_utils.rs @@ -125,22 +125,36 @@ impl SampleWorkspace { .build(&temp) .expect("build the filesystem tree for the sample workspace"); - macro_rules! link { - ($original:literal -> $link:literal) => {{ - let original = $original; - let link = $link; - if let Err(error) = hard_link(temp.join(original), temp.join(link)) { - panic!("Failed to link {original} to {link}: {error}"); - } - }}; - } + let link = |original: &str, link: &str| { + if let Err(error) = hard_link(temp.join(original), temp.join(link)) { + panic!("Failed to link {original} to {link}: {error}"); + } + }; - link!("main/sources/one-internal-hardlink.txt" -> "main/internal-hardlinks/link-0.txt"); - link!("main/sources/two-internal-hardlinks.txt" -> "main/internal-hardlinks/link-1a.txt"); - link!("main/sources/two-internal-hardlinks.txt" -> "main/internal-hardlinks/link-1b.txt"); - link!("main/sources/one-external-hardlink.txt" -> "external-hardlinks/link-2.txt"); - link!("main/sources/one-internal-one-external-hardlinks.txt" -> "main/internal-hardlinks/link-3a.txt"); - link!("main/sources/one-internal-one-external-hardlinks.txt" -> "external-hardlinks/link-3b.txt"); + link( + "main/sources/one-internal-hardlink.txt", + "main/internal-hardlinks/link-0.txt", + ); + link( + "main/sources/two-internal-hardlinks.txt", + "main/internal-hardlinks/link-1a.txt", + ); + link( + "main/sources/two-internal-hardlinks.txt", + "main/internal-hardlinks/link-1b.txt", + ); + link( + "main/sources/one-external-hardlink.txt", + "external-hardlinks/link-2.txt", + ); + link( + "main/sources/one-internal-one-external-hardlinks.txt", + "main/internal-hardlinks/link-3a.txt", + ); + link( + "main/sources/one-internal-one-external-hardlinks.txt", + "external-hardlinks/link-3b.txt", + ); SampleWorkspace(temp) } @@ -149,21 +163,17 @@ impl SampleWorkspace { use std::os::unix::fs::symlink; let workspace = SampleWorkspace::simple_tree_with_some_hardlinks(sizes); - macro_rules! symlink { - ($link_name:literal -> $target:literal) => { - let link_name = $link_name; - let target = $target; - if let Err(error) = symlink(target, workspace.join(link_name)) { - panic!("Failed create symbolic link {link_name} pointing to {target}: {error}"); - } - }; - } + let link = |link_name: &str, target: &str| { + if let Err(error) = symlink(target, workspace.join(link_name)) { + panic!("Failed create symbolic link {link_name} pointing to {target}: {error}"); + } + }; - symlink!("workspace-itself" -> "."); - symlink!("main/main-itself" -> "."); - symlink!("main/parent-of-main" -> ".."); - symlink!("main-mirror" -> "./main"); - symlink!("sources-mirror" -> "./main/sources"); + link("workspace-itself", "."); + link("main/main-itself", "."); + link("main/parent-of-main", ".."); + link("main-mirror", "./main"); + link("sources-mirror", "./main/sources"); workspace } diff --git a/tests/bytes_format.rs b/tests/bytes_format.rs index 12a40c3d..675739b1 100644 --- a/tests/bytes_format.rs +++ b/tests/bytes_format.rs @@ -1,3 +1,13 @@ +#![cfg_attr(dylint_lib = "perfectionist", feature(register_tool))] +#![cfg_attr(dylint_lib = "perfectionist", register_tool(perfectionist))] +#![cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::macro_argument_binding, + reason = "the `test_case!` macro uses a `name -> value in system == expected` DSL whose separators (`->`, `in`, `==`) are matcher tokens, not expression operators; binding the argument to a `let` is not even syntactically applicable. See #416. The crate-root scope is forced by an upstream late-pass anchoring quirk: violations in module-level item-position macro expansions resolve to the crate root, where finer-scoped `#[expect]` cannot reach them.", + ) +)] + use parallel_disk_usage::bytes_format::BytesFormat; use pretty_assertions::assert_eq; From eec253d5c1c4547d9d9a3da25ae4ffbb390064ea Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 15:34:21 +0000 Subject: [PATCH 04/13] lint!: enable `perfectionist::derive_ordering` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure the rule for `prefix_then_alphabetical` with an explicit `prefix` that encodes the project's preferred derive families in order: standard, comparison, `Hash`, formatting / error, conversions, reference wrappers, iteration, arithmetic operator pairs and folds, and integer-format derives. Anything not in the prefix (`Setters`, `SmartDefault`, `Parser`, …) falls in ASCII- case-insensitive alphabetical order after, matching the existing convention. Reorder 18 derive lists across the codebase to satisfy the rule. Every case had derive-more-style traits mixed into the standard- trait block, contradicting the convention CONTRIBUTING.md already documents; the rule's suggestions match that documented intent. Document the rule in `CONTRIBUTING.md`'s derive-ordering section, and drop the now-redundant within-line ordering bullet from the shared AI-instructions template (the rule enforces it). The line-splitting convention (separate `#[cfg_attr(...)]` line for feature-gated derives) is not enforced by the rule and stays in the contributing guide as a manual style preference. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- .github/copilot-instructions.md | 1 - AGENTS.md | 1 - CLAUDE.md | 1 - CONTRIBUTING.md | 8 ++---- dylint.toml | 35 ++++++++++++++++++++++++ src/args.rs | 2 +- src/args/depth.rs | 2 +- src/args/fraction.rs | 6 ++-- src/args/threads.rs | 2 +- src/bytes_format/output.rs | 2 +- src/bytes_format/parsed_value.rs | 2 +- src/device.rs | 2 +- src/hardlink/aware.rs | 2 +- src/hardlink/hardlink_list.rs | 4 +-- src/hardlink/hardlink_list/reflection.rs | 2 +- src/hardlink/hardlink_list/summary.rs | 2 +- src/inode.rs | 2 +- src/json_data.rs | 2 +- src/json_data/binary_version.rs | 2 +- src/os_string_display.rs | 6 ++-- src/reporter/progress_report.rs | 2 +- src/tree_builder/info.rs | 2 +- src/visualizer/methods/table.rs | 2 +- src/visualizer/proportion_bar.rs | 2 +- src/visualizer/tree.rs | 2 +- template/ai-instructions/shared.md | 1 - 26 files changed, 62 insertions(+), 35 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ef492d48..051bf7bd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -12,7 +12,6 @@ Read and follow the CONTRIBUTING.md file in this repository for all code style c - Use descriptive names for variables and closure parameters. Single letters are permitted only in these cases: (1) conventional names like `n` for count or `f` for formatter; (2) comparison closures like `|a, b|`; (3) trivial single-expression closures; (4) fold accumulators; (5) index variables `i`/`j`/`k` in short closures or index-based loops; and (6) test fixtures with identical roles. Single letters are never permitted in multi-line functions or closures. - Use `pipe-trait` to chain through unary functions such as constructors, `Some`, `Ok`, and free functions. Use it to flatten nested calls and to continue method chains. Do not use it for simple standalone calls; prefer `foo(value)` over `value.pipe(foo)`. - Prefer `where` clauses when a type has multiple trait bounds. -- Derive order: standard traits, then comparison traits, then `Hash`, then `derive_more`, then feature-gated derives. - For error types, only derive `Display` and `Error` from `derive_more` when each is actually needed. Not all displayable types are errors. - Minimize `unwrap()` in non-test code. Use proper error handling instead. - Prefer `#[cfg_attr(..., ignore = "reason")]` over `#[cfg(...)]` when skipping tests. Use `#[cfg]` on tests only when the code cannot compile under the condition, such as when it references types or functions that do not exist on other platforms. diff --git a/AGENTS.md b/AGENTS.md index ef492d48..051bf7bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,6 @@ Read and follow the CONTRIBUTING.md file in this repository for all code style c - Use descriptive names for variables and closure parameters. Single letters are permitted only in these cases: (1) conventional names like `n` for count or `f` for formatter; (2) comparison closures like `|a, b|`; (3) trivial single-expression closures; (4) fold accumulators; (5) index variables `i`/`j`/`k` in short closures or index-based loops; and (6) test fixtures with identical roles. Single letters are never permitted in multi-line functions or closures. - Use `pipe-trait` to chain through unary functions such as constructors, `Some`, `Ok`, and free functions. Use it to flatten nested calls and to continue method chains. Do not use it for simple standalone calls; prefer `foo(value)` over `value.pipe(foo)`. - Prefer `where` clauses when a type has multiple trait bounds. -- Derive order: standard traits, then comparison traits, then `Hash`, then `derive_more`, then feature-gated derives. - For error types, only derive `Display` and `Error` from `derive_more` when each is actually needed. Not all displayable types are errors. - Minimize `unwrap()` in non-test code. Use proper error handling instead. - Prefer `#[cfg_attr(..., ignore = "reason")]` over `#[cfg(...)]` when skipping tests. Use `#[cfg]` on tests only when the code cannot compile under the condition, such as when it references types or functions that do not exist on other platforms. diff --git a/CLAUDE.md b/CLAUDE.md index a6ba4eee..9eabaa1d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,6 @@ Read and follow the CONTRIBUTING.md file in this repository for all code style c - Use descriptive names for variables and closure parameters. Single letters are permitted only in these cases: (1) conventional names like `n` for count or `f` for formatter; (2) comparison closures like `|a, b|`; (3) trivial single-expression closures; (4) fold accumulators; (5) index variables `i`/`j`/`k` in short closures or index-based loops; and (6) test fixtures with identical roles. Single letters are never permitted in multi-line functions or closures. - Use `pipe-trait` to chain through unary functions such as constructors, `Some`, `Ok`, and free functions. Use it to flatten nested calls and to continue method chains. Do not use it for simple standalone calls; prefer `foo(value)` over `value.pipe(foo)`. - Prefer `where` clauses when a type has multiple trait bounds. -- Derive order: standard traits, then comparison traits, then `Hash`, then `derive_more`, then feature-gated derives. - For error types, only derive `Display` and `Error` from `derive_more` when each is actually needed. Not all displayable types are errors. - Minimize `unwrap()` in non-test code. Use proper error handling instead. - Prefer `#[cfg_attr(..., ignore = "reason")]` over `#[cfg(...)]` when skipping tests. Use `#[cfg]` on tests only when the code cannot compile under the condition, such as when it references types or functions that do not exist on other platforms. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3c570c4..7a267ae1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,13 +71,9 @@ pub use event::Event; ### Derive Macro Ordering -When deriving multiple traits, use this order and split across multiple `#[derive(...)]` lines for readability: +The order of trait names within each `#[derive(...)]` attribute is enforced automatically by the `perfectionist::derive_ordering` rule, configured for the `prefix_then_alphabetical` style. The configured `prefix` in `dylint.toml` lists the trait families in their project-preferred order: standard traits, then comparison traits, then `Hash`, then formatting / error derives, then conversions, then reference wrappers, then iteration, then arithmetic operator pairs and folds, then integer-format derives. Any trait that is not in the `prefix` (project-specific derives such as `Setters`, `SmartDefault`, and `Parser`) falls in ASCII-case-insensitive alphabetical order after the prefix entries. -1. **Standard traits:** `Debug`, `Default`, `Clone`, `Copy` -2. **Comparison traits:** `PartialEq`, `Eq`, `PartialOrd`, `Ord` -3. **Hash** -4. **`derive_more` traits:** `Display`, `From`, `Into`, `Add`, `AddAssign`, etc. -5. **Feature-gated derives** on a separate `#[cfg_attr(...)]` line +The remaining conventions are not enforced by the rule and must be applied by hand. When a type derives many traits, split them across multiple `#[derive(...)]` lines for readability, and place feature-gated derives on a separate `#[cfg_attr(...)]` line. ```rust #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] diff --git a/dylint.toml b/dylint.toml index f7477f03..32d7fefb 100644 --- a/dylint.toml +++ b/dylint.toml @@ -3,6 +3,41 @@ libraries = [ { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.9" }, ] +["perfectionist::derive_ordering"] +# Match the convention documented in `CONTRIBUTING.md`. The `prefix` +# extends the rule's default with the derive-more-style families this +# project uses, in the project-preferred logical order: formatting +# (`Display`, `Error`), conversions (`From`, `Into`, …), reference +# wrappers (`AsRef`, `AsMut`, `Deref`, `DerefMut`), iteration +# (`IntoIterator`), arithmetic operator pairs (`Add` / `AddAssign`, +# `Sub` / `SubAssign`, …), and integer-format derives (`LowerHex`, +# `UpperHex`, `Octal`). Anything not listed here (project-specific +# `Setters`, `SmartDefault`, `Parser`, etc.) falls alphabetically +# after the prefix, which matches the existing convention. +style = "prefix_then_alphabetical" +prefix = [ + # Standard traits. + "Debug", "Default", "Clone", "Copy", + # Comparison traits. + "PartialEq", "Eq", "PartialOrd", "Ord", + # Hash. + "Hash", + # Formatting / error. + "Display", "Error", + # Conversions. + "From", "Into", "TryFrom", "TryInto", "FromStr", + # Reference wrappers. + "AsRef", "AsMut", "Deref", "DerefMut", + # Iteration. + "IntoIterator", + # Arithmetic operator pairs. + "Add", "AddAssign", "Sub", "SubAssign", "Mul", "MulAssign", "Div", "DivAssign", + # Folds. + "Sum", "Product", + # Integer-format derives. + "LowerHex", "UpperHex", "Octal", +] + ["perfectionist::single_letter_names"] # Treat project-specific and third-party comparison-style helpers as if # they were standard `sort_by`-shaped methods, so the idiomatic diff --git a/src/args.rs b/src/args.rs index 4e1d1540..8e822415 100644 --- a/src/args.rs +++ b/src/args.rs @@ -25,7 +25,7 @@ use terminal_size::{Width, terminal_size}; use text_block_macros::text_block; /// The CLI arguments. -#[derive(Debug, SmartDefault, Setters, Clone, Parser)] +#[derive(Debug, Clone, Parser, Setters, SmartDefault)] #[clap( name = "pdu", diff --git a/src/args/depth.rs b/src/args/depth.rs index d4d5a4b1..819f73e6 100644 --- a/src/args/depth.rs +++ b/src/args/depth.rs @@ -25,7 +25,7 @@ impl Depth { } /// Error that occurs when parsing a string as [`Depth`]. -#[derive(Debug, Display, Clone, PartialEq, Eq, Error)] +#[derive(Debug, Clone, PartialEq, Eq, Display, Error)] #[non_exhaustive] pub enum FromStrError { #[display("Value is neither {INFINITE:?} nor a positive integer: {_0}")] diff --git a/src/args/fraction.rs b/src/args/fraction.rs index a481308d..9e0e903c 100644 --- a/src/args/fraction.rs +++ b/src/args/fraction.rs @@ -6,11 +6,11 @@ use std::{ }; /// Floating-point value that is greater than or equal to 0 and less than 1. -#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, AsRef, Deref, Display, Into)] +#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Display, Into, AsRef, Deref)] pub struct Fraction(f32); /// Error that occurs when calling [`Fraction::new`]. -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Error)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)] #[non_exhaustive] pub enum ConversionError { /// Provided value is greater than or equal to 1. @@ -43,7 +43,7 @@ impl TryFrom for Fraction { } /// Error that occurs when parsing a string as [`Fraction`]. -#[derive(Debug, Display, Clone, PartialEq, Eq, Error)] +#[derive(Debug, Clone, PartialEq, Eq, Display, Error)] #[non_exhaustive] pub enum FromStrError { ParseFloatError(ParseFloatError), diff --git a/src/args/threads.rs b/src/args/threads.rs index 44810350..d6d7d5f4 100644 --- a/src/args/threads.rs +++ b/src/args/threads.rs @@ -19,7 +19,7 @@ pub enum Threads { } /// Error that occurs when parsing a string as [`Threads`]. -#[derive(Debug, Display, Clone, PartialEq, Eq, Error)] +#[derive(Debug, Clone, PartialEq, Eq, Display, Error)] #[non_exhaustive] pub enum FromStrError { #[display("Value is neither {AUTO:?}, {MAX:?}, nor a number: {_0}")] diff --git a/src/bytes_format/output.rs b/src/bytes_format/output.rs index 69b47c95..7e221b71 100644 --- a/src/bytes_format/output.rs +++ b/src/bytes_format/output.rs @@ -2,7 +2,7 @@ use super::ParsedValue; use derive_more::Display; /// The [`DisplayOutput`](crate::size::Size::DisplayOutput) type of [`Bytes`](crate::size::Bytes). -#[derive(Debug, Display, Clone, Copy)] +#[derive(Debug, Clone, Copy, Display)] pub enum Output { /// Display the value as-is. PlainNumber(u64), diff --git a/src/bytes_format/parsed_value.rs b/src/bytes_format/parsed_value.rs index d8aad461..3f99ffae 100644 --- a/src/bytes_format/parsed_value.rs +++ b/src/bytes_format/parsed_value.rs @@ -1,7 +1,7 @@ use derive_more::Display; /// Return value of [`Formatter::parse_value`](super::Formatter::parse_value). -#[derive(Debug, Display, Clone, Copy)] +#[derive(Debug, Clone, Copy, Display)] pub enum ParsedValue { /// When input value is less than `scale_base`. #[display("{value} ")] diff --git a/src/device.rs b/src/device.rs index 545cce12..3bb63ef3 100644 --- a/src/device.rs +++ b/src/device.rs @@ -23,7 +23,7 @@ impl DeviceBoundary { /// The device number of a filesystem. #[derive( - Debug, Display, LowerHex, UpperHex, Octal, Clone, Copy, PartialEq, Eq, Hash, From, Into, + Debug, Clone, Copy, PartialEq, Eq, Hash, Display, From, Into, LowerHex, UpperHex, Octal, )] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct DeviceNumber(u64); diff --git a/src/hardlink/aware.rs b/src/hardlink/aware.rs index 65f2b0af..d5a82e94 100644 --- a/src/hardlink/aware.rs +++ b/src/hardlink/aware.rs @@ -19,7 +19,7 @@ use std::{convert::Infallible, fmt::Debug, os::unix::fs::MetadataExt, path::Path /// Detect files with more than 1 links and record them. /// Deduplicate them (remove duplicated size) from total size to /// accurately reflect the real size of their containers. -#[derive(Debug, SmartDefault, Clone, AsRef, AsMut, From, Into)] +#[derive(Debug, Clone, From, Into, AsRef, AsMut, SmartDefault)] pub struct Aware { /// Map each file (identified by inode number and device number) to its size and detected paths. record: HardlinkList, diff --git a/src/hardlink/hardlink_list.rs b/src/hardlink/hardlink_list.rs index 8879567d..9e8101f2 100644 --- a/src/hardlink/hardlink_list.rs +++ b/src/hardlink/hardlink_list.rs @@ -21,7 +21,7 @@ use pipe_trait::Pipe; use std::path::Path; /// Internal key used to uniquely identify an inode across all filesystems. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct InodeKey { /// Inode number within the device. ino: InodeNumber, @@ -45,7 +45,7 @@ struct Value { /// **Reflection:** `HardlinkList` does not implement `PartialEq`, `Eq`, /// `Deserialize`, and `Serialize` directly. Instead, it can be converted into a /// [`Reflection`] which implement these traits. -#[derive(Debug, SmartDefault, Clone)] +#[derive(Debug, Clone, SmartDefault)] pub struct HardlinkList( /// Map an inode key (device + inode number) to its size, number of links, and detected paths. DashMap>, diff --git a/src/hardlink/hardlink_list/reflection.rs b/src/hardlink/hardlink_list/reflection.rs index 7a6659c2..fe4d6472 100644 --- a/src/hardlink/hardlink_list/reflection.rs +++ b/src/hardlink/hardlink_list/reflection.rs @@ -119,7 +119,7 @@ impl From> for Reflection { /// Error that occurs when an attempt to convert a [`Reflection`] into a /// [`HardlinkList`] fails. -#[derive(Debug, Display, Error, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)] #[non_exhaustive] pub enum ConversionError { /// When the source has a duplicated `(inode, device)` pair. diff --git a/src/hardlink/hardlink_list/summary.rs b/src/hardlink/hardlink_list/summary.rs index b6ca4aaa..79871e05 100644 --- a/src/hardlink/hardlink_list/summary.rs +++ b/src/hardlink/hardlink_list/summary.rs @@ -11,7 +11,7 @@ use std::{ use serde::{Deserialize, Serialize}; /// Summary from [`HardlinkList`] or [`Reflection`]. -#[derive(Debug, Default, Setters, Clone, Copy, PartialEq, Eq, Add, AddAssign, Sum)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Add, AddAssign, Sum, Setters)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] #[setters(prefix = "with_")] #[non_exhaustive] diff --git a/src/inode.rs b/src/inode.rs index 2305dfb8..87140143 100644 --- a/src/inode.rs +++ b/src/inode.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// The inode number of a file or directory. #[derive( - Debug, Display, LowerHex, UpperHex, Octal, Clone, Copy, PartialEq, Eq, Hash, From, Into, + Debug, Clone, Copy, PartialEq, Eq, Hash, Display, From, Into, LowerHex, UpperHex, Octal, )] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct InodeNumber(u64); diff --git a/src/json_data.rs b/src/json_data.rs index eed2b094..680be09d 100644 --- a/src/json_data.rs +++ b/src/json_data.rs @@ -16,7 +16,7 @@ use smart_default::SmartDefault; use serde::{Deserialize, Serialize}; /// The `"shared"` field of [`JsonData`]. -#[derive(Debug, SmartDefault, Clone)] +#[derive(Debug, Clone, SmartDefault)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] #[cfg_attr(feature = "json", serde(rename_all = "kebab-case"))] pub struct JsonShared { diff --git a/src/json_data/binary_version.rs b/src/json_data/binary_version.rs index 6b520dea..5a99fac6 100644 --- a/src/json_data/binary_version.rs +++ b/src/json_data/binary_version.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Version of the `pdu` program that created the input JSON. -#[derive(Debug, Clone, PartialEq, Eq, AsMut, AsRef, From, FromStr, Into)] +#[derive(Debug, Clone, PartialEq, Eq, From, Into, FromStr, AsRef, AsMut)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct BinaryVersion(String); diff --git a/src/os_string_display.rs b/src/os_string_display.rs index e713aff6..1ee46b06 100644 --- a/src/os_string_display.rs +++ b/src/os_string_display.rs @@ -20,12 +20,12 @@ use serde::{Deserialize, Serialize}; Eq, PartialOrd, Ord, - AsMut, + From, + FromStr, AsRef, + AsMut, Deref, DerefMut, - From, - FromStr, )] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct OsStringDisplay(pub Inner) diff --git a/src/reporter/progress_report.rs b/src/reporter/progress_report.rs index 6aa690b4..cbb875e5 100644 --- a/src/reporter/progress_report.rs +++ b/src/reporter/progress_report.rs @@ -3,7 +3,7 @@ use derive_setters::Setters; use std::fmt::Write; /// Scan progress. -#[derive(Debug, Default, Setters, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Setters)] #[setters(prefix = "with_")] pub struct ProgressReport { /// Number of scanned items. diff --git a/src/tree_builder/info.rs b/src/tree_builder/info.rs index c6a72d28..4e0e167f 100644 --- a/src/tree_builder/info.rs +++ b/src/tree_builder/info.rs @@ -3,7 +3,7 @@ use derive_more::From; use smart_default::SmartDefault; /// Information to return from `get_info` of [`super::TreeBuilder`]. -#[derive(Debug, SmartDefault, From)] +#[derive(Debug, From, SmartDefault)] pub struct Info { /// Size associated with given `path`. pub size: Size, diff --git a/src/visualizer/methods/table.rs b/src/visualizer/methods/table.rs index fb35de63..fcc04843 100644 --- a/src/visualizer/methods/table.rs +++ b/src/visualizer/methods/table.rs @@ -2,7 +2,7 @@ use derive_more::{Deref, DerefMut}; use smart_default::SmartDefault; use std::collections::LinkedList; -#[derive(SmartDefault, Deref, DerefMut)] +#[derive(Deref, DerefMut, SmartDefault)] pub struct Table { #[deref] #[deref_mut] diff --git a/src/visualizer/proportion_bar.rs b/src/visualizer/proportion_bar.rs index b8ff978a..ac6342b9 100644 --- a/src/visualizer/proportion_bar.rs +++ b/src/visualizer/proportion_bar.rs @@ -4,7 +4,7 @@ use fmt_iter::repeat; use std::fmt::{Display, Error, Formatter}; /// Block of proportion bar. -#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRef, Deref, Display, Into)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Into, AsRef, Deref)] pub struct ProportionBarBlock(char); pub const LEVEL0_BLOCK: ProportionBarBlock = ProportionBarBlock('█'); diff --git a/src/visualizer/tree.rs b/src/visualizer/tree.rs index 133b58c1..2a12bd4d 100644 --- a/src/visualizer/tree.rs +++ b/src/visualizer/tree.rs @@ -18,7 +18,7 @@ pub struct TreeSkeletalComponent { } /// String made by calling [`TreeSkeletalComponent::visualize`](TreeSkeletalComponent). -#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRef, Deref, Display, Into)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Into, AsRef, Deref)] pub struct TreeSkeletalComponentVisualization(&'static str); impl TreeSkeletalComponent { diff --git a/template/ai-instructions/shared.md b/template/ai-instructions/shared.md index ef492d48..051bf7bd 100644 --- a/template/ai-instructions/shared.md +++ b/template/ai-instructions/shared.md @@ -12,7 +12,6 @@ Read and follow the CONTRIBUTING.md file in this repository for all code style c - Use descriptive names for variables and closure parameters. Single letters are permitted only in these cases: (1) conventional names like `n` for count or `f` for formatter; (2) comparison closures like `|a, b|`; (3) trivial single-expression closures; (4) fold accumulators; (5) index variables `i`/`j`/`k` in short closures or index-based loops; and (6) test fixtures with identical roles. Single letters are never permitted in multi-line functions or closures. - Use `pipe-trait` to chain through unary functions such as constructors, `Some`, `Ok`, and free functions. Use it to flatten nested calls and to continue method chains. Do not use it for simple standalone calls; prefer `foo(value)` over `value.pipe(foo)`. - Prefer `where` clauses when a type has multiple trait bounds. -- Derive order: standard traits, then comparison traits, then `Hash`, then `derive_more`, then feature-gated derives. - For error types, only derive `Display` and `Error` from `derive_more` when each is actually needed. Not all displayable types are errors. - Minimize `unwrap()` in non-test code. Use proper error handling instead. - Prefer `#[cfg_attr(..., ignore = "reason")]` over `#[cfg(...)]` when skipping tests. Use `#[cfg]` on tests only when the code cannot compile under the condition, such as when it references types or functions that do not exist on other platforms. From d948bd58594f08303b539769080feb13b7e77693 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 00:18:14 +0000 Subject: [PATCH 05/13] chore(deps): bump `perfectionist` to `KSXGitHub/perfectionist#52` head MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 52 (`93cb529`) fixes four of the five `macro_argument_binding` bug classes filed earlier: - #415 — `debug_assert_*` trivial-comparison no longer flagged when the comparison is over side-effect-free operands. Removed function-scope `#[expect]` suppressions in `data_tree::hardlink`, `visualizer::methods::bar_table`, and `visualizer::methods::initial_table`. - #416 — `name = value` / `name += value` DSL matchers no longer flagged. Removed the function-scope suppression on `reporter::progress_and_error_reporter::report`. The narrower `->` / `in` separators used by `tests::bytes_format::test_case!` are not yet covered, so its crate-root suppression remains, with the reason updated to point at the partial fix. - #417 — unit literal `()` is now trivial. Removed the function-scope suppression on `app::App::run`. - #418 — bare operator tokens (`==`, `>`) used as positional separators in `debug_assert_op_expr!` are no longer flagged. The lone residual flag in `visualizer::methods::tree_table` (`debug_assert_op_expr!(intermediate_table.len(), ==, ...)`) is correct rule behaviour: the rule cannot prove `Vec::len` is pure. Replaced the broad suppression with a narrower one citing `Vec::len`'s `O(1)`-pure-read contract. PR 43, folded into PR 52's branch, splits `single_letter_names` into separate per-rule config keys. Migrate the `comparison_methods` allowlist from `[perfectionist::single_letter_names]` to the new `[perfectionist::single_letter_closure_param]` section so the project-specific `sort_reflection_by` / `into_sorted_by` helpers remain exempt from the closure-parameter check. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 4 ++-- src/app.rs | 7 ------- src/data_tree/hardlink.rs | 7 ------- src/reporter/progress_and_error_reporter.rs | 7 ------- src/visualizer/methods/bar_table.rs | 7 ------- src/visualizer/methods/initial_table.rs | 7 ------- src/visualizer/methods/tree_table.rs | 2 +- tests/bytes_format.rs | 2 +- 8 files changed, 4 insertions(+), 39 deletions(-) diff --git a/dylint.toml b/dylint.toml index 32d7fefb..5673d2b6 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,6 +1,6 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.9" }, + { git = "https://github.com/KSXGitHub/perfectionist", rev = "93cb529c15ae9e0ac8ef093efee58219b8d68cb7" }, ] ["perfectionist::derive_ordering"] @@ -38,7 +38,7 @@ prefix = [ "LowerHex", "UpperHex", "Octal", ] -["perfectionist::single_letter_names"] +["perfectionist::single_letter_closure_param"] # Treat project-specific and third-party comparison-style helpers as if # they were standard `sort_by`-shaped methods, so the idiomatic # `|a, b| a.field.cmp(&b.field)` closure does not trip the rule. The diff --git a/src/app.rs b/src/app.rs index a2b712ef..42d6e6e3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -39,13 +39,6 @@ impl App { } /// Run the application. - #[cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::macro_argument_binding, - reason = "the unit literal `()` is the canonical trivial value, but the rule's trivial-expression grammar does not yet accept parenthesised forms; see #417", - ) - )] pub fn run(mut self) -> Result<(), RuntimeError> { // DYNAMIC DISPATCH POLICY: // diff --git a/src/data_tree/hardlink.rs b/src/data_tree/hardlink.rs index 76cab0ba..8cbf93b8 100644 --- a/src/data_tree/hardlink.rs +++ b/src/data_tree/hardlink.rs @@ -12,13 +12,6 @@ where { /// Reduce the size of the directories that have hardlinks. #[cfg_attr(not(unix), expect(unused))] - #[cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::macro_argument_binding, - reason = "binding a `debug_assert_op!` argument to a `let` forces it to run in release builds, which defeats the entire point of `debug_assert_*`; see #415", - ) - )] pub(crate) fn par_deduplicate_hardlinks(&mut self, hardlink_info: &[(Size, Vec<&Path>)]) { if hardlink_info.is_empty() { return; diff --git a/src/reporter/progress_and_error_reporter.rs b/src/reporter/progress_and_error_reporter.rs index 101d2637..04e3daca 100644 --- a/src/reporter/progress_and_error_reporter.rs +++ b/src/reporter/progress_and_error_reporter.rs @@ -79,13 +79,6 @@ where ReportError: Fn(ErrorReport) + Sync, u64: Into, { - #[cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::macro_argument_binding, - reason = "the `bump!` macro's `($field:ident += $delta:expr)` matcher uses structural assignment syntax rather than a single expression, so binding the argument to a `let` is not even syntactically possible; see #416", - ) - )] fn report(&self, event: Event) { use Event::*; let ProgressAndErrorReporter { diff --git a/src/visualizer/methods/bar_table.rs b/src/visualizer/methods/bar_table.rs index 1c71d423..1adbe225 100644 --- a/src/visualizer/methods/bar_table.rs +++ b/src/visualizer/methods/bar_table.rs @@ -12,13 +12,6 @@ pub(super) struct BarRow { pub(super) proportion_bar: ProportionBar, } -#[cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::macro_argument_binding, - reason = "the flagged sites are all `debug_assert_op!` / `debug_assert_op_expr!` calls on side-effect-free locals; binding to a `let` would defeat the debug-only contract (see #415), and the rule also miscategorises bare operator tokens like `==` as expressions (see #418)", - ) -)] pub(super) fn render_bars<'a, Name, Size>( tree_table: TreeTable<&'a Name, Size>, total: u64, diff --git a/src/visualizer/methods/initial_table.rs b/src/visualizer/methods/initial_table.rs index 1409bd56..31580115 100644 --- a/src/visualizer/methods/initial_table.rs +++ b/src/visualizer/methods/initial_table.rs @@ -37,13 +37,6 @@ impl InitialColumnWidth { pub(super) type InitialTable = Table, InitialColumnWidth>; -#[cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::macro_argument_binding, - reason = "the flagged sites are `debug_assert_op!` calls on side-effect-free locals; binding to a `let` would defeat the debug-only contract. See #415", - ) -)] pub(super) fn render_initial( visualizer: Visualizer<'_, Name, Size>, ) -> InitialTable<&'_ Name, Size> diff --git a/src/visualizer/methods/tree_table.rs b/src/visualizer/methods/tree_table.rs index b8adde15..71a7fc59 100644 --- a/src/visualizer/methods/tree_table.rs +++ b/src/visualizer/methods/tree_table.rs @@ -45,7 +45,7 @@ pub(super) type TreeTable = Table, TreeC dylint_lib = "perfectionist", expect( perfectionist::macro_argument_binding, - reason = "the flagged sites are `debug_assert_op!` / `debug_assert_op_expr!` calls; binding to a `let` would defeat the debug-only contract (see #415), and the rule miscategorises bare operator tokens like `==` and `>` as expressions (see #418)", + reason = "`Vec::len` is a pure `O(1)` read; the `debug_assert_op_expr!` invocation below intentionally keeps the call inside the assertion so it runs only in debug builds, and binding it to a `let` would lift the call into release builds for no benefit", ) )] pub(super) fn render_tree<'a, Name, Size>( diff --git a/tests/bytes_format.rs b/tests/bytes_format.rs index 675739b1..402cb1ca 100644 --- a/tests/bytes_format.rs +++ b/tests/bytes_format.rs @@ -4,7 +4,7 @@ dylint_lib = "perfectionist", expect( perfectionist::macro_argument_binding, - reason = "the `test_case!` macro uses a `name -> value in system == expected` DSL whose separators (`->`, `in`, `==`) are matcher tokens, not expression operators; binding the argument to a `let` is not even syntactically applicable. See #416. The crate-root scope is forced by an upstream late-pass anchoring quirk: violations in module-level item-position macro expansions resolve to the crate root, where finer-scoped `#[expect]` cannot reach them.", + reason = "the `test_case!` macro uses a `name -> value in system == expected` DSL whose `->` and `in` separators are not yet recognised by the rule's non-expression marker list (PR #52 added `=`, `+=`, and bare operators but not these). See #416. The crate-root scope is forced by an upstream late-pass anchoring quirk: violations in module-level item-position macro expansions resolve to the crate root, where finer-scoped `#[expect]` cannot reach them (#419).", ) )] From 71a6b666c4b6f65aec9e3dc646d223c798950a0c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 01:00:12 +0000 Subject: [PATCH 06/13] chore(deps): bump `perfectionist` to 8925331 and adopt deny/allow config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8925331 redesigns `macro_argument_binding` from "flag every macro with a non-trivial top-level argument" to a curated `AllowAndDeny` mode: only invocations on the built-in deny list (`debug_assert`, `debug_assert_eq`, `debug_assert_ne`) plus user `deny_extra` trigger the rule, and the built-in allow list silences common once-only macros (`format`, `vec`, `assert`, …). The trivial- expression grammar also gains a pure-getter postfix walker so `vec.len()`, `s.is_empty()`, `opt.as_ref()`, and the rest of the built-in zero-arg list stay trivial. Adopt the new config knobs: - `deny_extra = ["debug_assert_op", "debug_assert_op_expr"]` enrolls the `assert-cmp` family alongside the std `debug_assert*` macros so non-trivial arguments still get caught despite their conditional `cfg(debug_assertions)` expansion. - `allow_extra = ["bump", "visualize", "test_case"]` catalogues the three remaining project-defined macros that expand each top-level argument exactly once. The combination eliminates two suppressions that were necessary under PR 52's head (`93cb529`): - `src/visualizer/methods/tree_table.rs` — `intermediate_table .len()` is now trivial via the built-in pure-getter list. - `tests/bytes_format.rs` — `test_case!` is in `allow_extra`, so the rule no longer fires on its `name -> value in system == expected` DSL invocations. The crate-root suppression and the `register_tool(perfectionist)` it required are gone. The late-pass anchoring bug #419 is mechanistically unchanged: `src/rules/macro_argument_binding/late.rs` is byte-identical between `93cb529` and `8925331`. The new `allow_extra` / `deny_extra` mechanism sidesteps #419 in practice for user-defined macros (config-level allow needs no `#[expect]`), but the underlying anchoring bug for item-position macro expansions still exists. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 23 ++++++++++++++++++++++- src/visualizer/methods/tree_table.rs | 7 ------- tests/bytes_format.rs | 10 ---------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/dylint.toml b/dylint.toml index 5673d2b6..4991b25b 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,6 +1,6 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", rev = "93cb529c15ae9e0ac8ef093efee58219b8d68cb7" }, + { git = "https://github.com/KSXGitHub/perfectionist", rev = "892533129e44ef48f7be468a9804ff36325f70fa" }, ] ["perfectionist::derive_ordering"] @@ -38,6 +38,27 @@ prefix = [ "LowerHex", "UpperHex", "Octal", ] +["perfectionist::macro_argument_binding"] +# `assert_cmp::debug_assert_op!` and `debug_assert_op_expr!` expand +# conditionally on `cfg(debug_assertions)`, so they share the +# argument-evaluation hazard the built-in deny list catches for +# `debug_assert*`. Catalogue them so the rule still flags genuinely +# side-effecting arguments while accepting the pure-getter calls +# (`vec.len()`) that the trivial-method classifier already recognises. +deny_extra = ["debug_assert_op", "debug_assert_op_expr"] +# Project-defined and third-party macros that expand each top-level +# argument exactly once. Adding them to the allow list silences the +# blanket "unknown macro" flag the default `AllowAndDeny` mode +# applies to uncatalogued invocations. +allow_extra = [ + # `src/reporter/progress_and_error_reporter.rs::report`. + "bump", + # `src/app.rs::App::run`. + "visualize", + # `tests/bytes_format.rs`. + "test_case", +] + ["perfectionist::single_letter_closure_param"] # Treat project-specific and third-party comparison-style helpers as if # they were standard `sort_by`-shaped methods, so the idiomatic diff --git a/src/visualizer/methods/tree_table.rs b/src/visualizer/methods/tree_table.rs index 71a7fc59..6419ee51 100644 --- a/src/visualizer/methods/tree_table.rs +++ b/src/visualizer/methods/tree_table.rs @@ -41,13 +41,6 @@ impl TreeColumnWidth { pub(super) type TreeTable = Table, TreeColumnWidth>; -#[cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::macro_argument_binding, - reason = "`Vec::len` is a pure `O(1)` read; the `debug_assert_op_expr!` invocation below intentionally keeps the call inside the assertion so it runs only in debug builds, and binding it to a `let` would lift the call into release builds for no benefit", - ) -)] pub(super) fn render_tree<'a, Name, Size>( visualizer: Visualizer<'a, Name, Size>, initial_table: InitialTable<&'a Name, Size>, diff --git a/tests/bytes_format.rs b/tests/bytes_format.rs index 402cb1ca..12a40c3d 100644 --- a/tests/bytes_format.rs +++ b/tests/bytes_format.rs @@ -1,13 +1,3 @@ -#![cfg_attr(dylint_lib = "perfectionist", feature(register_tool))] -#![cfg_attr(dylint_lib = "perfectionist", register_tool(perfectionist))] -#![cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::macro_argument_binding, - reason = "the `test_case!` macro uses a `name -> value in system == expected` DSL whose `->` and `in` separators are not yet recognised by the rule's non-expression marker list (PR #52 added `=`, `+=`, and bare operators but not these). See #416. The crate-root scope is forced by an upstream late-pass anchoring quirk: violations in module-level item-position macro expansions resolve to the crate root, where finer-scoped `#[expect]` cannot reach them (#419).", - ) -)] - use parallel_disk_usage::bytes_format::BytesFormat; use pretty_assertions::assert_eq; From f792aa0aef0b9158b3c9bcc880bfd4a1c7dfa15b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 02:32:51 +0000 Subject: [PATCH 07/13] review feedback on PR #412 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the project owner's review comments: - Reorder the `derive_ordering` prefix so `Display` sits right after `Debug`, `SmartDefault` sits next to `Default`, and reference wrappers (`AsRef`, `AsMut`, `Deref`, `DerefMut`) come before conversions (`From`, `Into`, …). Drop the in-array group labels — the new layout no longer maps onto a clean "standard / comparison / Hash / derive_more" story and the labels were misleading. Drop the verbose prefatory comments under each `dylint.toml` table heading. Collapse `allow_extra` and `comparison_methods` to single lines. - Replace `#[non_exhaustive]` with `#[cfg_attr(dylint_lib = "perfectionist", expect(perfectionist::non_exhaustive_error, reason = "…"))]` on `args::fraction::ConversionError` and `args::fraction::FromStrError`. Both error types are explicitly exhaustive: their constructor and parser each have a closed set of failure modes that any future addition would break as an API change anyway. - Revert the closure rewrites in `tests/_utils.rs` and restore the `link!` / `symlink!` macros. The upstream `macro_argument_binding` redesign in perfectionist 8925331 silences these once they are listed in `allow_extra`, so the closures are no longer needed. - Reapply 20 derive-list reorders that the new prefix makes the rule prefer, including the `Debug, Display, Error, …` shape called out in the review for the `hardlink::hardlink_list::reflection::ConversionError` derive, and the `Debug, Display, Default, Clone, Copy, …, AsRef, Deref, Into` shape called out for the `Fraction` derive. - Refresh the CONTRIBUTING.md derive-ordering paragraph and example to match the new prefix layout. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- CONTRIBUTING.md | 4 +- dylint.toml | 72 ++---------------------- src/args.rs | 2 +- src/args/depth.rs | 4 +- src/args/fraction.rs | 22 ++++++-- src/args/threads.rs | 4 +- src/bytes_format/output.rs | 2 +- src/bytes_format/parsed_value.rs | 2 +- src/device.rs | 2 +- src/hardlink/aware.rs | 2 +- src/hardlink/hardlink_list.rs | 2 +- src/hardlink/hardlink_list/reflection.rs | 2 +- src/inode.rs | 2 +- src/json_data.rs | 2 +- src/json_data/binary_version.rs | 2 +- src/os_string_display.rs | 4 +- src/tree_builder/info.rs | 2 +- src/visualizer/methods/table.rs | 2 +- src/visualizer/proportion_bar.rs | 2 +- src/visualizer/tree.rs | 2 +- tests/_utils.rs | 68 ++++++++++------------ 21 files changed, 74 insertions(+), 132 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a267ae1..c311d57d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,12 +71,12 @@ pub use event::Event; ### Derive Macro Ordering -The order of trait names within each `#[derive(...)]` attribute is enforced automatically by the `perfectionist::derive_ordering` rule, configured for the `prefix_then_alphabetical` style. The configured `prefix` in `dylint.toml` lists the trait families in their project-preferred order: standard traits, then comparison traits, then `Hash`, then formatting / error derives, then conversions, then reference wrappers, then iteration, then arithmetic operator pairs and folds, then integer-format derives. Any trait that is not in the `prefix` (project-specific derives such as `Setters`, `SmartDefault`, and `Parser`) falls in ASCII-case-insensitive alphabetical order after the prefix entries. +The order of trait names within each `#[derive(...)]` attribute is enforced automatically by the `perfectionist::derive_ordering` rule, configured for the `prefix_then_alphabetical` style. The configured `prefix` in `dylint.toml` lists the trait families in their project-preferred order: `Debug`, formatting / error derives (`Display`, `Error`), defaults (`Default`, `SmartDefault`), `Clone` / `Copy`, comparison and `Hash`, reference wrappers (`AsRef`, `AsMut`, `Deref`, `DerefMut`), conversions (`From`, `Into`, `TryFrom`, `TryInto`, `FromStr`), iteration, arithmetic operator pairs and folds, and integer-format derives. Any trait that is not in the `prefix` (project-specific derives such as `Setters` and `Parser`) falls in ASCII-case-insensitive alphabetical order after the prefix entries. The remaining conventions are not enforced by the rule and must be applied by hand. When a type derives many traits, split them across multiple `#[derive(...)]` lines for readability, and place feature-gated derives on a separate `#[cfg_attr(...)]` line. ```rust -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Display, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(From, Into, Add, AddAssign, Sub, SubAssign, Sum)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct Bytes(u64); diff --git a/dylint.toml b/dylint.toml index 4991b25b..fb08d06e 100644 --- a/dylint.toml +++ b/dylint.toml @@ -4,84 +4,24 @@ libraries = [ ] ["perfectionist::derive_ordering"] -# Match the convention documented in `CONTRIBUTING.md`. The `prefix` -# extends the rule's default with the derive-more-style families this -# project uses, in the project-preferred logical order: formatting -# (`Display`, `Error`), conversions (`From`, `Into`, …), reference -# wrappers (`AsRef`, `AsMut`, `Deref`, `DerefMut`), iteration -# (`IntoIterator`), arithmetic operator pairs (`Add` / `AddAssign`, -# `Sub` / `SubAssign`, …), and integer-format derives (`LowerHex`, -# `UpperHex`, `Octal`). Anything not listed here (project-specific -# `Setters`, `SmartDefault`, `Parser`, etc.) falls alphabetically -# after the prefix, which matches the existing convention. style = "prefix_then_alphabetical" prefix = [ - # Standard traits. - "Debug", "Default", "Clone", "Copy", - # Comparison traits. + "Debug", "Display", "Error", + "Default", "SmartDefault", + "Clone", "Copy", "PartialEq", "Eq", "PartialOrd", "Ord", - # Hash. "Hash", - # Formatting / error. - "Display", "Error", - # Conversions. - "From", "Into", "TryFrom", "TryInto", "FromStr", - # Reference wrappers. "AsRef", "AsMut", "Deref", "DerefMut", - # Iteration. + "From", "Into", "TryFrom", "TryInto", "FromStr", "IntoIterator", - # Arithmetic operator pairs. "Add", "AddAssign", "Sub", "SubAssign", "Mul", "MulAssign", "Div", "DivAssign", - # Folds. "Sum", "Product", - # Integer-format derives. "LowerHex", "UpperHex", "Octal", ] ["perfectionist::macro_argument_binding"] -# `assert_cmp::debug_assert_op!` and `debug_assert_op_expr!` expand -# conditionally on `cfg(debug_assertions)`, so they share the -# argument-evaluation hazard the built-in deny list catches for -# `debug_assert*`. Catalogue them so the rule still flags genuinely -# side-effecting arguments while accepting the pure-getter calls -# (`vec.len()`) that the trivial-method classifier already recognises. deny_extra = ["debug_assert_op", "debug_assert_op_expr"] -# Project-defined and third-party macros that expand each top-level -# argument exactly once. Adding them to the allow list silences the -# blanket "unknown macro" flag the default `AllowAndDeny` mode -# applies to uncatalogued invocations. -allow_extra = [ - # `src/reporter/progress_and_error_reporter.rs::report`. - "bump", - # `src/app.rs::App::run`. - "visualize", - # `tests/bytes_format.rs`. - "test_case", -] +allow_extra = ["bump", "link", "symlink", "test_case", "visualize"] ["perfectionist::single_letter_closure_param"] -# Treat project-specific and third-party comparison-style helpers as if -# they were standard `sort_by`-shaped methods, so the idiomatic -# `|a, b| a.field.cmp(&b.field)` closure does not trip the rule. The -# defaults are included explicitly because setting this list replaces -# the built-in allowlist rather than extending it. -comparison_methods = [ - "sort_by", - "sort_unstable_by", - "sort_by_key", - "sort_unstable_by_key", - "min_by", - "max_by", - "min_by_key", - "max_by_key", - "binary_search_by", - "binary_search_by_key", - "cmp_by", - "partial_cmp_by", - "eq_by", - "fold", - # Project-specific helper used in integration tests. - "sort_reflection_by", - # `into-sorted` crate's consuming sort. - "into_sorted_by", -] +comparison_methods = ["sort_by", "sort_unstable_by", "sort_by_key", "sort_unstable_by_key", "min_by", "max_by", "min_by_key", "max_by_key", "binary_search_by", "binary_search_by_key", "cmp_by", "partial_cmp_by", "eq_by", "fold", "sort_reflection_by", "into_sorted_by"] diff --git a/src/args.rs b/src/args.rs index 8e822415..25eb067c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -25,7 +25,7 @@ use terminal_size::{Width, terminal_size}; use text_block_macros::text_block; /// The CLI arguments. -#[derive(Debug, Clone, Parser, Setters, SmartDefault)] +#[derive(Debug, SmartDefault, Clone, Parser, Setters)] #[clap( name = "pdu", diff --git a/src/args/depth.rs b/src/args/depth.rs index 819f73e6..375570a7 100644 --- a/src/args/depth.rs +++ b/src/args/depth.rs @@ -7,7 +7,7 @@ use std::{ const INFINITE: &str = "inf"; /// Maximum depth of the tree. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq)] pub enum Depth { #[display("{INFINITE}")] Infinite, @@ -25,7 +25,7 @@ impl Depth { } /// Error that occurs when parsing a string as [`Depth`]. -#[derive(Debug, Clone, PartialEq, Eq, Display, Error)] +#[derive(Debug, Display, Error, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum FromStrError { #[display("Value is neither {INFINITE:?} nor a positive integer: {_0}")] diff --git a/src/args/fraction.rs b/src/args/fraction.rs index 9e0e903c..021e50bd 100644 --- a/src/args/fraction.rs +++ b/src/args/fraction.rs @@ -6,12 +6,18 @@ use std::{ }; /// Floating-point value that is greater than or equal to 0 and less than 1. -#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Display, Into, AsRef, Deref)] +#[derive(Debug, Display, Default, Clone, Copy, PartialEq, PartialOrd, AsRef, Deref, Into)] pub struct Fraction(f32); /// Error that occurs when calling [`Fraction::new`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)] -#[non_exhaustive] +#[derive(Debug, Display, Error, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::non_exhaustive_error, + reason = "this error type is explicitly exhaustive: `Fraction::new` can only fail in the two ways enumerated below, and any future failure mode would be a `Fraction::new` API change worth a SemVer-major bump", + ) +)] pub enum ConversionError { /// Provided value is greater than or equal to 1. #[display("greater than or equal to 1")] @@ -43,8 +49,14 @@ impl TryFrom for Fraction { } /// Error that occurs when parsing a string as [`Fraction`]. -#[derive(Debug, Clone, PartialEq, Eq, Display, Error)] -#[non_exhaustive] +#[derive(Debug, Display, Error, Clone, PartialEq, Eq)] +#[cfg_attr( + dylint_lib = "perfectionist", + expect( + perfectionist::non_exhaustive_error, + reason = "this error type is explicitly exhaustive: `Fraction::from_str` can only fail in the two ways enumerated below, and any future failure mode would be a `Fraction::from_str` API change worth a SemVer-major bump", + ) +)] pub enum FromStrError { ParseFloatError(ParseFloatError), Conversion(ConversionError), diff --git a/src/args/threads.rs b/src/args/threads.rs index d6d7d5f4..8f058b13 100644 --- a/src/args/threads.rs +++ b/src/args/threads.rs @@ -8,7 +8,7 @@ const AUTO: &str = "auto"; const MAX: &str = "max"; /// Number of rayon threads. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display)] +#[derive(Debug, Display, Default, Clone, Copy, PartialEq, Eq)] pub enum Threads { #[default] #[display("{AUTO}")] @@ -19,7 +19,7 @@ pub enum Threads { } /// Error that occurs when parsing a string as [`Threads`]. -#[derive(Debug, Clone, PartialEq, Eq, Display, Error)] +#[derive(Debug, Display, Error, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum FromStrError { #[display("Value is neither {AUTO:?}, {MAX:?}, nor a number: {_0}")] diff --git a/src/bytes_format/output.rs b/src/bytes_format/output.rs index 7e221b71..69b47c95 100644 --- a/src/bytes_format/output.rs +++ b/src/bytes_format/output.rs @@ -2,7 +2,7 @@ use super::ParsedValue; use derive_more::Display; /// The [`DisplayOutput`](crate::size::Size::DisplayOutput) type of [`Bytes`](crate::size::Bytes). -#[derive(Debug, Clone, Copy, Display)] +#[derive(Debug, Display, Clone, Copy)] pub enum Output { /// Display the value as-is. PlainNumber(u64), diff --git a/src/bytes_format/parsed_value.rs b/src/bytes_format/parsed_value.rs index 3f99ffae..d8aad461 100644 --- a/src/bytes_format/parsed_value.rs +++ b/src/bytes_format/parsed_value.rs @@ -1,7 +1,7 @@ use derive_more::Display; /// Return value of [`Formatter::parse_value`](super::Formatter::parse_value). -#[derive(Debug, Clone, Copy, Display)] +#[derive(Debug, Display, Clone, Copy)] pub enum ParsedValue { /// When input value is less than `scale_base`. #[display("{value} ")] diff --git a/src/device.rs b/src/device.rs index 3bb63ef3..8b43d9ba 100644 --- a/src/device.rs +++ b/src/device.rs @@ -23,7 +23,7 @@ impl DeviceBoundary { /// The device number of a filesystem. #[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, Display, From, Into, LowerHex, UpperHex, Octal, + Debug, Display, Clone, Copy, PartialEq, Eq, Hash, From, Into, LowerHex, UpperHex, Octal, )] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct DeviceNumber(u64); diff --git a/src/hardlink/aware.rs b/src/hardlink/aware.rs index d5a82e94..65f2b0af 100644 --- a/src/hardlink/aware.rs +++ b/src/hardlink/aware.rs @@ -19,7 +19,7 @@ use std::{convert::Infallible, fmt::Debug, os::unix::fs::MetadataExt, path::Path /// Detect files with more than 1 links and record them. /// Deduplicate them (remove duplicated size) from total size to /// accurately reflect the real size of their containers. -#[derive(Debug, Clone, From, Into, AsRef, AsMut, SmartDefault)] +#[derive(Debug, SmartDefault, Clone, AsRef, AsMut, From, Into)] pub struct Aware { /// Map each file (identified by inode number and device number) to its size and detected paths. record: HardlinkList, diff --git a/src/hardlink/hardlink_list.rs b/src/hardlink/hardlink_list.rs index 9e8101f2..8fe6dfd7 100644 --- a/src/hardlink/hardlink_list.rs +++ b/src/hardlink/hardlink_list.rs @@ -45,7 +45,7 @@ struct Value { /// **Reflection:** `HardlinkList` does not implement `PartialEq`, `Eq`, /// `Deserialize`, and `Serialize` directly. Instead, it can be converted into a /// [`Reflection`] which implement these traits. -#[derive(Debug, Clone, SmartDefault)] +#[derive(Debug, SmartDefault, Clone)] pub struct HardlinkList( /// Map an inode key (device + inode number) to its size, number of links, and detected paths. DashMap>, diff --git a/src/hardlink/hardlink_list/reflection.rs b/src/hardlink/hardlink_list/reflection.rs index fe4d6472..7a6659c2 100644 --- a/src/hardlink/hardlink_list/reflection.rs +++ b/src/hardlink/hardlink_list/reflection.rs @@ -119,7 +119,7 @@ impl From> for Reflection { /// Error that occurs when an attempt to convert a [`Reflection`] into a /// [`HardlinkList`] fails. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)] +#[derive(Debug, Display, Error, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum ConversionError { /// When the source has a duplicated `(inode, device)` pair. diff --git a/src/inode.rs b/src/inode.rs index 87140143..3842ad51 100644 --- a/src/inode.rs +++ b/src/inode.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// The inode number of a file or directory. #[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, Display, From, Into, LowerHex, UpperHex, Octal, + Debug, Display, Clone, Copy, PartialEq, Eq, Hash, From, Into, LowerHex, UpperHex, Octal, )] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct InodeNumber(u64); diff --git a/src/json_data.rs b/src/json_data.rs index 680be09d..eed2b094 100644 --- a/src/json_data.rs +++ b/src/json_data.rs @@ -16,7 +16,7 @@ use smart_default::SmartDefault; use serde::{Deserialize, Serialize}; /// The `"shared"` field of [`JsonData`]. -#[derive(Debug, Clone, SmartDefault)] +#[derive(Debug, SmartDefault, Clone)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] #[cfg_attr(feature = "json", serde(rename_all = "kebab-case"))] pub struct JsonShared { diff --git a/src/json_data/binary_version.rs b/src/json_data/binary_version.rs index 5a99fac6..1fcb077a 100644 --- a/src/json_data/binary_version.rs +++ b/src/json_data/binary_version.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Version of the `pdu` program that created the input JSON. -#[derive(Debug, Clone, PartialEq, Eq, From, Into, FromStr, AsRef, AsMut)] +#[derive(Debug, Clone, PartialEq, Eq, AsRef, AsMut, From, Into, FromStr)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct BinaryVersion(String); diff --git a/src/os_string_display.rs b/src/os_string_display.rs index 1ee46b06..e729db56 100644 --- a/src/os_string_display.rs +++ b/src/os_string_display.rs @@ -20,12 +20,12 @@ use serde::{Deserialize, Serialize}; Eq, PartialOrd, Ord, - From, - FromStr, AsRef, AsMut, Deref, DerefMut, + From, + FromStr, )] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct OsStringDisplay(pub Inner) diff --git a/src/tree_builder/info.rs b/src/tree_builder/info.rs index 4e0e167f..c6a72d28 100644 --- a/src/tree_builder/info.rs +++ b/src/tree_builder/info.rs @@ -3,7 +3,7 @@ use derive_more::From; use smart_default::SmartDefault; /// Information to return from `get_info` of [`super::TreeBuilder`]. -#[derive(Debug, From, SmartDefault)] +#[derive(Debug, SmartDefault, From)] pub struct Info { /// Size associated with given `path`. pub size: Size, diff --git a/src/visualizer/methods/table.rs b/src/visualizer/methods/table.rs index fcc04843..fb35de63 100644 --- a/src/visualizer/methods/table.rs +++ b/src/visualizer/methods/table.rs @@ -2,7 +2,7 @@ use derive_more::{Deref, DerefMut}; use smart_default::SmartDefault; use std::collections::LinkedList; -#[derive(Deref, DerefMut, SmartDefault)] +#[derive(SmartDefault, Deref, DerefMut)] pub struct Table { #[deref] #[deref_mut] diff --git a/src/visualizer/proportion_bar.rs b/src/visualizer/proportion_bar.rs index ac6342b9..46fbc304 100644 --- a/src/visualizer/proportion_bar.rs +++ b/src/visualizer/proportion_bar.rs @@ -4,7 +4,7 @@ use fmt_iter::repeat; use std::fmt::{Display, Error, Formatter}; /// Block of proportion bar. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Into, AsRef, Deref)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, AsRef, Deref, Into)] pub struct ProportionBarBlock(char); pub const LEVEL0_BLOCK: ProportionBarBlock = ProportionBarBlock('█'); diff --git a/src/visualizer/tree.rs b/src/visualizer/tree.rs index 2a12bd4d..ba0200b4 100644 --- a/src/visualizer/tree.rs +++ b/src/visualizer/tree.rs @@ -18,7 +18,7 @@ pub struct TreeSkeletalComponent { } /// String made by calling [`TreeSkeletalComponent::visualize`](TreeSkeletalComponent). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Into, AsRef, Deref)] +#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, AsRef, Deref, Into)] pub struct TreeSkeletalComponentVisualization(&'static str); impl TreeSkeletalComponent { diff --git a/tests/_utils.rs b/tests/_utils.rs index 4de50ef7..c2ba7e8b 100644 --- a/tests/_utils.rs +++ b/tests/_utils.rs @@ -125,36 +125,22 @@ impl SampleWorkspace { .build(&temp) .expect("build the filesystem tree for the sample workspace"); - let link = |original: &str, link: &str| { - if let Err(error) = hard_link(temp.join(original), temp.join(link)) { - panic!("Failed to link {original} to {link}: {error}"); - } - }; + macro_rules! link { + ($original:literal -> $link:literal) => {{ + let original = $original; + let link = $link; + if let Err(error) = hard_link(temp.join(original), temp.join(link)) { + panic!("Failed to link {original} to {link}: {error}"); + } + }}; + } - link( - "main/sources/one-internal-hardlink.txt", - "main/internal-hardlinks/link-0.txt", - ); - link( - "main/sources/two-internal-hardlinks.txt", - "main/internal-hardlinks/link-1a.txt", - ); - link( - "main/sources/two-internal-hardlinks.txt", - "main/internal-hardlinks/link-1b.txt", - ); - link( - "main/sources/one-external-hardlink.txt", - "external-hardlinks/link-2.txt", - ); - link( - "main/sources/one-internal-one-external-hardlinks.txt", - "main/internal-hardlinks/link-3a.txt", - ); - link( - "main/sources/one-internal-one-external-hardlinks.txt", - "external-hardlinks/link-3b.txt", - ); + link!("main/sources/one-internal-hardlink.txt" -> "main/internal-hardlinks/link-0.txt"); + link!("main/sources/two-internal-hardlinks.txt" -> "main/internal-hardlinks/link-1a.txt"); + link!("main/sources/two-internal-hardlinks.txt" -> "main/internal-hardlinks/link-1b.txt"); + link!("main/sources/one-external-hardlink.txt" -> "external-hardlinks/link-2.txt"); + link!("main/sources/one-internal-one-external-hardlinks.txt" -> "main/internal-hardlinks/link-3a.txt"); + link!("main/sources/one-internal-one-external-hardlinks.txt" -> "external-hardlinks/link-3b.txt"); SampleWorkspace(temp) } @@ -163,17 +149,21 @@ impl SampleWorkspace { use std::os::unix::fs::symlink; let workspace = SampleWorkspace::simple_tree_with_some_hardlinks(sizes); - let link = |link_name: &str, target: &str| { - if let Err(error) = symlink(target, workspace.join(link_name)) { - panic!("Failed create symbolic link {link_name} pointing to {target}: {error}"); - } - }; + macro_rules! symlink { + ($link_name:literal -> $target:literal) => { + let link_name = $link_name; + let target = $target; + if let Err(error) = symlink(target, workspace.join(link_name)) { + panic!("Failed create symbolic link {link_name} pointing to {target}: {error}"); + } + }; + } - link("workspace-itself", "."); - link("main/main-itself", "."); - link("main/parent-of-main", ".."); - link("main-mirror", "./main"); - link("sources-mirror", "./main/sources"); + symlink!("workspace-itself" -> "."); + symlink!("main/main-itself" -> "."); + symlink!("main/parent-of-main" -> ".."); + symlink!("main-mirror" -> "./main"); + symlink!("sources-mirror" -> "./main/sources"); workspace } From d845841381bbbe850adecced1d07f02ee8000fe2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 02:46:48 +0000 Subject: [PATCH 08/13] review feedback on PR #412, round 2 Two more review comments: - Restore the `make_const!` macro in `visualizer::proportion_bar`. The matcher syntax (`$name:ident = $content:literal`) is now silenced by perfectionist `93cb529`'s extension of `looks_like_expression` to top-level `=` markers; adding `make_const` to `allow_extra` covers the residual unknown-macro flag from `AllowAndDeny` mode. - Drop the experiment with `perfectionist::non_exhaustive_error`. Remove the deny directive at the crate root, and remove the `#[cfg_attr(... expect(perfectionist::non_exhaustive_error, ...))]` suppressions added on `args::fraction::ConversionError` and `args::fraction::FromStrError` in the previous commit (those types are explicitly exhaustive, so the rule was telling them to add `#[non_exhaustive]` they did not want; the right resolution is to drop the rule, not work around it). https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 2 +- src/args/fraction.rs | 14 -------------- src/lib.rs | 4 ---- src/visualizer/proportion_bar.rs | 16 +++++++++++----- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/dylint.toml b/dylint.toml index fb08d06e..0059379d 100644 --- a/dylint.toml +++ b/dylint.toml @@ -21,7 +21,7 @@ prefix = [ ["perfectionist::macro_argument_binding"] deny_extra = ["debug_assert_op", "debug_assert_op_expr"] -allow_extra = ["bump", "link", "symlink", "test_case", "visualize"] +allow_extra = ["bump", "link", "make_const", "symlink", "test_case", "visualize"] ["perfectionist::single_letter_closure_param"] comparison_methods = ["sort_by", "sort_unstable_by", "sort_by_key", "sort_unstable_by_key", "min_by", "max_by", "min_by_key", "max_by_key", "binary_search_by", "binary_search_by_key", "cmp_by", "partial_cmp_by", "eq_by", "fold", "sort_reflection_by", "into_sorted_by"] diff --git a/src/args/fraction.rs b/src/args/fraction.rs index 021e50bd..8a1639fc 100644 --- a/src/args/fraction.rs +++ b/src/args/fraction.rs @@ -11,13 +11,6 @@ pub struct Fraction(f32); /// Error that occurs when calling [`Fraction::new`]. #[derive(Debug, Display, Error, Clone, Copy, PartialEq, Eq)] -#[cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::non_exhaustive_error, - reason = "this error type is explicitly exhaustive: `Fraction::new` can only fail in the two ways enumerated below, and any future failure mode would be a `Fraction::new` API change worth a SemVer-major bump", - ) -)] pub enum ConversionError { /// Provided value is greater than or equal to 1. #[display("greater than or equal to 1")] @@ -50,13 +43,6 @@ impl TryFrom for Fraction { /// Error that occurs when parsing a string as [`Fraction`]. #[derive(Debug, Display, Error, Clone, PartialEq, Eq)] -#[cfg_attr( - dylint_lib = "perfectionist", - expect( - perfectionist::non_exhaustive_error, - reason = "this error type is explicitly exhaustive: `Fraction::from_str` can only fail in the two ways enumerated below, and any future failure mode would be a `Fraction::from_str` API change worth a SemVer-major bump", - ) -)] pub enum FromStrError { ParseFloatError(ParseFloatError), Conversion(ConversionError), diff --git a/src/lib.rs b/src/lib.rs index 04ecb231..ee7ba0e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,6 @@ #![deny(warnings)] #![cfg_attr(dylint_lib = "perfectionist", feature(register_tool))] #![cfg_attr(dylint_lib = "perfectionist", register_tool(perfectionist))] -#![cfg_attr( - dylint_lib = "perfectionist", - deny(perfectionist::non_exhaustive_error) -)] #[cfg(feature = "json")] pub use serde; diff --git a/src/visualizer/proportion_bar.rs b/src/visualizer/proportion_bar.rs index 46fbc304..31a09c3c 100644 --- a/src/visualizer/proportion_bar.rs +++ b/src/visualizer/proportion_bar.rs @@ -7,11 +7,17 @@ use std::fmt::{Display, Error, Formatter}; #[derive(Debug, Display, Clone, Copy, PartialEq, Eq, AsRef, Deref, Into)] pub struct ProportionBarBlock(char); -pub const LEVEL0_BLOCK: ProportionBarBlock = ProportionBarBlock('█'); -pub const LEVEL1_BLOCK: ProportionBarBlock = ProportionBarBlock('▓'); -pub const LEVEL2_BLOCK: ProportionBarBlock = ProportionBarBlock('▒'); -pub const LEVEL3_BLOCK: ProportionBarBlock = ProportionBarBlock('░'); -pub const LEVEL4_BLOCK: ProportionBarBlock = ProportionBarBlock(' '); +macro_rules! make_const { + ($name:ident = $content:literal) => { + pub const $name: ProportionBarBlock = ProportionBarBlock($content); + }; +} + +make_const!(LEVEL0_BLOCK = '█'); +make_const!(LEVEL1_BLOCK = '▓'); +make_const!(LEVEL2_BLOCK = '▒'); +make_const!(LEVEL3_BLOCK = '░'); +make_const!(LEVEL4_BLOCK = ' '); /// Proportion bar. #[derive(Debug, Clone, Copy, PartialEq, Eq, From, Into)] From 9fb4f63e59b995e646a0f779085222ae380ad7a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 11:36:23 +0000 Subject: [PATCH 09/13] chore(deps): bump `perfectionist` to `2e94ada` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picks up everything since `8925331`: - KSXGitHub/perfectionist#52 merged with no behavioural delta from the PR-head SHA we tested earlier. - KSXGitHub/perfectionist#54 adds `into_sorted_by` / `into_sorted_unstable_by` (and the `_key` siblings) to the built-in `single_letter_closure_param` allowlist, so they no longer need to be configured per project. - KSXGitHub/perfectionist#55 switches several rules from "configure replaces the default" to "configure extends the default" (with a paired `ignore_*` knob to subtract). Closes the issue we filed as #413. - KSXGitHub/perfectionist#56 enables `deny_unknown_fields` on the config tables; typos and stale field names now fail to load rather than silently no-oping. - KSXGitHub/perfectionist#58 renames a handful of config fields: `comparison_methods` → `extra_trivial_callback_methods`, `let_binding_allowed_idents` → `allowed_idents`, and so on. - KSXGitHub/perfectionist#62 adds a `Span::in_external_macro` check on `single_letter_let_binding`'s `LetStmt` span, intending to silence proc-macro-synthesised bindings such as `clap_derive`'s `default_value_t`. It does not actually catch the `clap_derive` case in this codebase: the rule still fires on the three `default_value_t` attributes in `args::Args`, so the module-level `allow` workaround stays in place. See #410 follow-up. - KSXGitHub/perfectionist#63 teaches `single_letter_closure_param` to recognise macro-call bodies as trivial wrappers, so `.pipe(|x| vec![x])` is once again accepted. Reverts the `|x|` → `|command|` rename in `tests::_utils`. Migrate the local config: - `[perfectionist::single_letter_closure_param]` switches from `comparison_methods = []` to `extra_trivial_callback_methods = ["sort_reflection_by"]`. The 17 default entries no longer need to be listed, and `into_sorted_by` is now part of the upstream defaults so it can also be dropped. - `[perfectionist::macro_argument_binding]`'s `deny_extra` and `allow_extra` knobs are unchanged — those names did not move. - The crate-root `register_tool(perfectionist)` is preserved alongside the still-required `args.rs` workaround. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 4 ++-- src/args.rs | 2 +- tests/_utils.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dylint.toml b/dylint.toml index 0059379d..738bc0d9 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,6 +1,6 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", rev = "892533129e44ef48f7be468a9804ff36325f70fa" }, + { git = "https://github.com/KSXGitHub/perfectionist", rev = "2e94ada584fe0f2b5c35dfbad161debf618bce76" }, ] ["perfectionist::derive_ordering"] @@ -24,4 +24,4 @@ deny_extra = ["debug_assert_op", "debug_assert_op_expr"] allow_extra = ["bump", "link", "make_const", "symlink", "test_case", "visualize"] ["perfectionist::single_letter_closure_param"] -comparison_methods = ["sort_by", "sort_unstable_by", "sort_by_key", "sort_unstable_by_key", "min_by", "max_by", "min_by_key", "max_by_key", "binary_search_by", "binary_search_by_key", "cmp_by", "partial_cmp_by", "eq_by", "fold", "sort_reflection_by", "into_sorted_by"] +extra_trivial_callback_methods = ["sort_reflection_by"] diff --git a/src/args.rs b/src/args.rs index 25eb067c..616d3014 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,7 +2,7 @@ dylint_lib = "perfectionist", allow( perfectionist::single_letter_let_binding, - reason = "the `let s` bindings flagged by this lint originate in `clap_derive` macro expansion of `default_value_t` and are outside our control", + reason = "the `let s` bindings flagged by this lint originate in `clap_derive` macro expansion of `default_value_t` and are outside our control. KSXGitHub/perfectionist#62 added an `in_external_macro` check on the `LetStmt` span, but `clap_derive` attaches user-source spans to both the identifier and the surrounding statement, so the check does not catch this case", ) )] diff --git a/tests/_utils.rs b/tests/_utils.rs index c2ba7e8b..1d30da0b 100644 --- a/tests/_utils.rs +++ b/tests/_utils.rs @@ -466,7 +466,7 @@ impl<'a> Default for CommandList<'a> { /// Initialize a list with one `pdu` command. fn default() -> Self { CommandRepresentation::default() - .pipe(|command| vec![command]) + .pipe(|x| vec![x]) .pipe(CommandList) } } From a135084a12aaa3ac3ec870def93101e41489e8dc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 15:59:54 +0000 Subject: [PATCH 10/13] chore(deps): bump `perfectionist` to `3ca7f01` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KSXGitHub/perfectionist#66 extends the proc-macro-synthesised skip from `single_letter_let_binding` to the rest of the single-letter family and `arc_rc_clone`. The expanded check walks the relevant span's expansion chain rather than only calling `Span::in_external_macro`, which catches `clap_derive`'s `default_value_t` synthesis where the previous statement-span- only check did not. With that, the `clap_derive` `let s` false positive originally filed as #410 — and re-surfaced after the apparent #62 fix — is fully resolved. Drop the module-level `allow(perfectionist::single_letter_let_binding)` from `src/args.rs`. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 2 +- src/args.rs | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/dylint.toml b/dylint.toml index 738bc0d9..e1ddd49b 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,6 +1,6 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", rev = "2e94ada584fe0f2b5c35dfbad161debf618bce76" }, + { git = "https://github.com/KSXGitHub/perfectionist", rev = "3ca7f01eb5b1359f6656b383be56d3b3697b106b" }, ] ["perfectionist::derive_ordering"] diff --git a/src/args.rs b/src/args.rs index 616d3014..6ca380c5 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,11 +1,3 @@ -#![cfg_attr( - dylint_lib = "perfectionist", - allow( - perfectionist::single_letter_let_binding, - reason = "the `let s` bindings flagged by this lint originate in `clap_derive` macro expansion of `default_value_t` and are outside our control. KSXGitHub/perfectionist#62 added an `in_external_macro` check on the `LetStmt` span, but `clap_derive` attaches user-source spans to both the identifier and the surrounding statement, so the check does not catch this case", - ) -)] - pub mod depth; pub mod fraction; pub mod quantity; From e87eca5fbc763f92d84da73b121b1e8370f88be5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 13 May 2026 19:52:41 +0000 Subject: [PATCH 11/13] chore(deps): bump `perfectionist` to `0.0.0-rc.12` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five new commits since `3ca7f01`: - KSXGitHub/perfectionist#65 finishes `macro_argument_binding`'s non-expression-marker list. `looks_like_expression` now skips top-level `->` and `in` tokens alongside the `=` / `+=` / `:` / bare-operator markers added earlier. Resolves the residual case of the issue we filed as #416, where `tests::bytes_format::test_case!`'s `name -> value in system == expected` DSL was still flagged even after the `=` / `+=` fixes. - KSXGitHub/perfectionist#67 introduces a crate-wide `[perfectionist]` config table with `enable` / `disable` arrays, and shifts `non_exhaustive_error`'s default state to `Disabled` (opt-in via `[perfectionist] enable = ["non_exhaustive_error"]`). This codebase already retired the rule in d845841, so the new upstream default matches our intent — no configuration needed. - KSXGitHub/perfectionist#68, KSXGitHub/perfectionist#69, KSXGitHub/perfectionist#70 are documentation-only. Drop the entire `[perfectionist::macro_argument_binding] allow_extra` list. Every entry it carried — `bump`, `link`, `make_const`, `symlink`, `test_case`, `visualize` — is now silenced by the rule's own per-argument trivial-expression and DSL-separator checks. Concretely: - `bump!(items += 1)` — `+=` skipped (PR #52). - `make_const!(LEVEL0_BLOCK = '█')` — `=` skipped (PR #52). - `visualize!(tree, ())` — `tree` is a trivial path, `()` is the unit literal classified as trivial in PR #52. - `link!("a" -> "b")` / `symlink!("a" -> "b")` — `->` skipped (PR #65). - `test_case!(plain_number -> 65_535 in PlainNumber == "65535")` — `->` and `in` skipped (PR #65). `deny_extra` stays as-is — the `assert_cmp::debug_assert_op*` family still needs explicit denial so non-trivial arguments inside them get caught. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dylint.toml b/dylint.toml index e1ddd49b..5b55919d 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,6 +1,6 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", rev = "3ca7f01eb5b1359f6656b383be56d3b3697b106b" }, + { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.12" }, ] ["perfectionist::derive_ordering"] @@ -21,7 +21,6 @@ prefix = [ ["perfectionist::macro_argument_binding"] deny_extra = ["debug_assert_op", "debug_assert_op_expr"] -allow_extra = ["bump", "link", "make_const", "symlink", "test_case", "visualize"] ["perfectionist::single_letter_closure_param"] extra_trivial_callback_methods = ["sort_reflection_by"] From 06d832d0fbab8107a86e6992f078052199271997 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 05:34:35 +0000 Subject: [PATCH 12/13] docs(contributing): cite the dylint rules that enforce naming sections `Generic Parameter Naming` and `Variable and Closure Parameter Naming` describe rules that `perfectionist` enforces in full. Add the same "enforced by `perfectionist::`" footnote that `Module Organization` and `Derive Macro Ordering` already carry. The textual guidance and worked examples stay; readers and AI agents now have the breadcrumb to the configured rule and its relevant `dylint.toml` knobs (`short_impl_max_lines`, `allowed_idents`, `extra_trivial_callback_methods`). https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c311d57d..d6859b25 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,11 +88,11 @@ Use **descriptive names** for type parameters, not single letters: - `Size`, `Name`, `SizeGetter`, `HardlinksRecorder`, `Report` -Single-letter generics are acceptable only in very short, self-contained trait impls. +Single-letter generics are acceptable only in very short, self-contained trait impls. Enforced by `perfectionist::single_letter_generic`; the threshold for "very short" is the rule's `short_impl_max_lines` knob in `dylint.toml`. ### Variable and Closure Parameter Naming -Use **descriptive names** for variables and closure parameters by default. Single-letter names are permitted only in the specific cases listed below. +Use **descriptive names** for variables and closure parameters by default. Single-letter names are permitted only in the specific cases listed below. Enforced by `perfectionist::single_letter_let_binding`, `perfectionist::single_letter_function_param`, and `perfectionist::single_letter_closure_param`; the per-rule `allowed_idents` and `extra_trivial_callback_methods` knobs in `dylint.toml` reflect the exceptions documented here. #### When single-letter names are allowed From 811d420cce5c1f21ec9d566d14511e7b69efe5d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 06:36:15 +0000 Subject: [PATCH 13/13] chore(deps): bump `perfectionist` to `0.0.0-rc.14` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 23 commits since `0.0.0-rc.12`. The functional ones that matter for this codebase: - KSXGitHub/perfectionist#72 adds the curated `core` / `std` compile-time macros (`stringify!`, `concat!`, `cfg!`, `option_env!`, the `assert*` family, etc.) to `macro_argument_binding`'s built-in allow list. - KSXGitHub/perfectionist#82 reframes the rule's classifier from "non-trivial expressions" to "impure expressions" and renames the four `extra_trivial_*` / `ignore_trivial_*` knobs (methods and macros) to `extra_pure_*` / `ignore_pure_*`. The diagnostic text was updated to match. No effect here — we set none of those knobs. - KSXGitHub/perfectionist#84 drops the per-rule `enabled` config knob from `macro_argument_binding`, `macro_trailing_comma`, and `prefer_raw_string`. Users relying on `[perfectionist::] enabled = false` migrate to `[perfectionist] disable = [""]`. We never set that knob; nothing to change. - KSXGitHub/perfectionist#95 drops the redundant `style = "preserve"` variant from `derive_ordering`. We use `style = "prefix_then_alphabetical"`; unaffected. The rest of the delta is pages styling (#86, #87, #94, #97, #98, #99), docs (#73, #75, #76, #77, #80, #81, #83, #96), CI / dependency bumps (#79, #88-93), and the rc.13 release. Clean build against `0.0.0-rc.14`, full test suite green, no config migration needed and no source changes triggered. No new bugs surface in this codebase. https://claude.ai/code/session_01CoRidYHvni9nKNgxMPXmfQ --- dylint.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dylint.toml b/dylint.toml index 5b55919d..f792a3fb 100644 --- a/dylint.toml +++ b/dylint.toml @@ -1,6 +1,6 @@ [workspace.metadata.dylint] libraries = [ - { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.12" }, + { git = "https://github.com/KSXGitHub/perfectionist", tag = "0.0.0-rc.14" }, ] ["perfectionist::derive_ordering"]