From e9146b239aefc7bd64652d518bc93cb7d1f8455b Mon Sep 17 00:00:00 2001 From: Samuel Tardieu Date: Wed, 10 Jun 2026 14:31:26 +0200 Subject: [PATCH 1/3] Cleanup remaining attributes in Clippy test files This is needed to implement the new test which checks top-level inner attributes ordering and structure. --- tests/ui/assert_is_empty.fixed | 4 ++-- tests/ui/assert_is_empty.rs | 4 ++-- tests/ui/block_scrutinee.1.fixed | 1 - tests/ui/block_scrutinee.2.fixed | 1 - tests/ui/block_scrutinee.rs | 1 - tests/ui/block_scrutinee.stderr | 8 ++++---- tests/ui/rest_pattern_accessible_field.fixed | 2 +- tests/ui/rest_pattern_accessible_field.rs | 2 +- tests/ui/with_capacity_zero.fixed | 1 - tests/ui/with_capacity_zero.rs | 1 - tests/ui/with_capacity_zero.stderr | 18 +++++++++--------- 11 files changed, 19 insertions(+), 24 deletions(-) diff --git a/tests/ui/assert_is_empty.fixed b/tests/ui/assert_is_empty.fixed index 4f0bbb73535f..4b15274d41ed 100644 --- a/tests/ui/assert_is_empty.fixed +++ b/tests/ui/assert_is_empty.fixed @@ -1,6 +1,6 @@ #![warn(clippy::assert_is_empty)] -#![allow(clippy::useless_vec)] -#![allow(clippy::const_is_empty, clippy::needless_ifs)] +#![expect(clippy::needless_ifs, clippy::useless_vec)] +#![allow(clippy::const_is_empty)] fn main() { let vec = vec![1, 2, 3]; diff --git a/tests/ui/assert_is_empty.rs b/tests/ui/assert_is_empty.rs index f20431f2a01a..8545e1a18635 100644 --- a/tests/ui/assert_is_empty.rs +++ b/tests/ui/assert_is_empty.rs @@ -1,6 +1,6 @@ #![warn(clippy::assert_is_empty)] -#![allow(clippy::useless_vec)] -#![allow(clippy::const_is_empty, clippy::needless_ifs)] +#![expect(clippy::needless_ifs, clippy::useless_vec)] +#![allow(clippy::const_is_empty)] fn main() { let vec = vec![1, 2, 3]; diff --git a/tests/ui/block_scrutinee.1.fixed b/tests/ui/block_scrutinee.1.fixed index a5da489f24d9..3f123e54783f 100644 --- a/tests/ui/block_scrutinee.1.fixed +++ b/tests/ui/block_scrutinee.1.fixed @@ -1,7 +1,6 @@ //@ edition: 2021 #![warn(clippy::block_scrutinee)] #![allow(clippy::blocks_in_conditions)] -#![allow(clippy::let_and_return)] fn my_function() -> Option { Some(1) diff --git a/tests/ui/block_scrutinee.2.fixed b/tests/ui/block_scrutinee.2.fixed index c771895cbc7a..6eed53831bf6 100644 --- a/tests/ui/block_scrutinee.2.fixed +++ b/tests/ui/block_scrutinee.2.fixed @@ -1,7 +1,6 @@ //@ edition: 2021 #![warn(clippy::block_scrutinee)] #![allow(clippy::blocks_in_conditions)] -#![allow(clippy::let_and_return)] fn my_function() -> Option { Some(1) diff --git a/tests/ui/block_scrutinee.rs b/tests/ui/block_scrutinee.rs index 07b763baaee1..1f9ad4d3015e 100644 --- a/tests/ui/block_scrutinee.rs +++ b/tests/ui/block_scrutinee.rs @@ -1,7 +1,6 @@ //@ edition: 2021 #![warn(clippy::block_scrutinee)] #![allow(clippy::blocks_in_conditions)] -#![allow(clippy::let_and_return)] fn my_function() -> Option { Some(1) diff --git a/tests/ui/block_scrutinee.stderr b/tests/ui/block_scrutinee.stderr index b44f742af22f..bb3b7e2a4e26 100644 --- a/tests/ui/block_scrutinee.stderr +++ b/tests/ui/block_scrutinee.stderr @@ -1,5 +1,5 @@ error: this scrutinee is wrapped in a block - --> tests/ui/block_scrutinee.rs:11:22 + --> tests/ui/block_scrutinee.rs:10:22 | LL | if let Some(x) = { my_function() } { | ^^^^^^^^^^^^^^^^^ @@ -20,7 +20,7 @@ LL ~ if let Some(x) = res { | error: this scrutinee is wrapped in a block - --> tests/ui/block_scrutinee.rs:16:11 + --> tests/ui/block_scrutinee.rs:15:11 | LL | match { my_function() } { | ^^^^^^^^^^^^^^^^^ @@ -39,7 +39,7 @@ LL ~ match res { | error: this scrutinee is wrapped in a block - --> tests/ui/block_scrutinee.rs:24:25 + --> tests/ui/block_scrutinee.rs:23:25 | LL | while let Some(x) = { v.pop() } { | ^^^^^^^^^^^ @@ -58,7 +58,7 @@ LL ~ while let Some(x) = res { | error: this scrutinee is wrapped in a block - --> tests/ui/block_scrutinee.rs:41:22 + --> tests/ui/block_scrutinee.rs:40:22 | LL | if let Some(x) = { | ______________________^ diff --git a/tests/ui/rest_pattern_accessible_field.fixed b/tests/ui/rest_pattern_accessible_field.fixed index 0ccfb551aa28..73d240bb2641 100644 --- a/tests/ui/rest_pattern_accessible_field.fixed +++ b/tests/ui/rest_pattern_accessible_field.fixed @@ -1,7 +1,7 @@ //@aux-build:proc_macros.rs //@aux-build:non-exhaustive-struct.rs #![warn(clippy::rest_pattern_accessible_field)] -#![warn(clippy::unneeded_wildcard_pattern)] +#![deny(clippy::unneeded_wildcard_pattern)] use non_exhaustive_struct::{NonExhaustiveStruct, NonExhaustiveStructNoPrivateFields}; diff --git a/tests/ui/rest_pattern_accessible_field.rs b/tests/ui/rest_pattern_accessible_field.rs index 9a692b741785..b08563b6b086 100644 --- a/tests/ui/rest_pattern_accessible_field.rs +++ b/tests/ui/rest_pattern_accessible_field.rs @@ -1,7 +1,7 @@ //@aux-build:proc_macros.rs //@aux-build:non-exhaustive-struct.rs #![warn(clippy::rest_pattern_accessible_field)] -#![warn(clippy::unneeded_wildcard_pattern)] +#![deny(clippy::unneeded_wildcard_pattern)] use non_exhaustive_struct::{NonExhaustiveStruct, NonExhaustiveStructNoPrivateFields}; diff --git a/tests/ui/with_capacity_zero.fixed b/tests/ui/with_capacity_zero.fixed index 6da614d00024..3e9e12ac5791 100644 --- a/tests/ui/with_capacity_zero.fixed +++ b/tests/ui/with_capacity_zero.fixed @@ -1,5 +1,4 @@ #![warn(clippy::with_capacity_zero)] -#![allow(unused)] #![allow(clippy::eq_op)] use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; diff --git a/tests/ui/with_capacity_zero.rs b/tests/ui/with_capacity_zero.rs index 0f8bfb12c34f..6fb6a11b61d1 100644 --- a/tests/ui/with_capacity_zero.rs +++ b/tests/ui/with_capacity_zero.rs @@ -1,5 +1,4 @@ #![warn(clippy::with_capacity_zero)] -#![allow(unused)] #![allow(clippy::eq_op)] use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque}; diff --git a/tests/ui/with_capacity_zero.stderr b/tests/ui/with_capacity_zero.stderr index 5fce9d90e036..6033845d59fc 100644 --- a/tests/ui/with_capacity_zero.stderr +++ b/tests/ui/with_capacity_zero.stderr @@ -1,5 +1,5 @@ error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:19:23 + --> tests/ui/with_capacity_zero.rs:18:23 | LL | let v: Vec = Vec::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^ @@ -13,7 +13,7 @@ LL + let v: Vec = Vec::new(); | error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:21:13 + --> tests/ui/with_capacity_zero.rs:20:13 | LL | let s = String::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -25,7 +25,7 @@ LL + let s = String::new(); | error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:23:14 + --> tests/ui/with_capacity_zero.rs:22:14 | LL | let v2 = std::vec::Vec::::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -37,7 +37,7 @@ LL + let v2 = std::vec::Vec::::new(); | error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:25:15 + --> tests/ui/with_capacity_zero.rs:24:15 | LL | let map = HashMap::::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -49,7 +49,7 @@ LL + let map = HashMap::::new(); | error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:27:15 + --> tests/ui/with_capacity_zero.rs:26:15 | LL | let set = HashSet::::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -61,7 +61,7 @@ LL + let set = HashSet::::new(); | error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:29:17 + --> tests/ui/with_capacity_zero.rs:28:17 | LL | let deque = VecDeque::::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -73,7 +73,7 @@ LL + let deque = VecDeque::::new(); | error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:31:16 + --> tests/ui/with_capacity_zero.rs:30:16 | LL | let heap = BinaryHeap::::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -85,7 +85,7 @@ LL + let heap = BinaryHeap::::new(); | error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:33:16 + --> tests/ui/with_capacity_zero.rs:32:16 | LL | let path = PathBuf::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -97,7 +97,7 @@ LL + let path = PathBuf::new(); | error: calling `with_capacity(0)` is equivalent to `new()` - --> tests/ui/with_capacity_zero.rs:35:18 + --> tests/ui/with_capacity_zero.rs:34:18 | LL | let os_str = OsString::with_capacity(0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^ From 507e5501096f6445b0814cf1c8a8a1c29384ef86 Mon Sep 17 00:00:00 2001 From: Samuel Tardieu Date: Wed, 27 May 2026 12:08:51 +0200 Subject: [PATCH 2/3] Cleanup attributes in Clippy source files This is needed to implement the new test which checks top-level inner attributes ordering and structure. --- clippy_config/src/lib.rs | 4 ++-- clippy_dev/src/lib.rs | 4 ++-- clippy_lints/src/doc/mod.rs | 2 -- clippy_lints/src/lib.rs | 8 ++------ clippy_lints/src/operators/const_comparisons.rs | 2 +- clippy_lints/src/suspicious_operation_groupings.rs | 2 +- clippy_lints/src/unnested_or_patterns.rs | 2 +- clippy_lints_internal/src/lib.rs | 9 +++------ clippy_utils/src/ast_utils/mod.rs | 2 +- clippy_utils/src/lib.rs | 4 ++-- clippy_utils/src/macros.rs | 2 +- clippy_utils/src/source.rs | 2 +- clippy_utils/src/sym.rs | 2 +- clippy_utils/src/ty/mod.rs | 2 +- lintcheck/src/main.rs | 10 ++++------ src/driver.rs | 9 +++++---- 16 files changed, 28 insertions(+), 38 deletions(-) diff --git a/clippy_config/src/lib.rs b/clippy_config/src/lib.rs index 0a891e75eb3e..3dac01ae19d5 100644 --- a/clippy_config/src/lib.rs +++ b/clippy_config/src/lib.rs @@ -1,12 +1,12 @@ #![feature(rustc_private)] #![warn( + rust_2018_idioms, trivial_casts, trivial_numeric_casts, - rust_2018_idioms, unused_lifetimes, unused_qualifications )] -#![allow(clippy::must_use_candidate, clippy::missing_panics_doc)] +#![expect(clippy::must_use_candidate)] extern crate rustc_data_structures; extern crate rustc_errors; diff --git a/clippy_dev/src/lib.rs b/clippy_dev/src/lib.rs index b707c2cea6e5..70783118efed 100644 --- a/clippy_dev/src/lib.rs +++ b/clippy_dev/src/lib.rs @@ -7,13 +7,13 @@ rustc_private )] #![warn( + rust_2018_idioms, trivial_casts, trivial_numeric_casts, - rust_2018_idioms, unused_lifetimes, unused_qualifications )] -#![allow(clippy::case_sensitive_file_extension_comparisons, clippy::missing_panics_doc)] +#![expect(clippy::missing_panics_doc)] extern crate rustc_arena; extern crate rustc_data_structures; diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs index 97b437965513..3bdd9fb3b1aa 100644 --- a/clippy_lints/src/doc/mod.rs +++ b/clippy_lints/src/doc/mod.rs @@ -1,5 +1,3 @@ -#![allow(clippy::lint_without_lint_pass)] - use clippy_config::Conf; use clippy_utils::attrs::is_doc_hidden; use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_then}; diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 0f7ec0ef1b91..5910c1ee8622 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -11,15 +11,11 @@ #![feature(stmt_expr_attributes)] #![feature(unwrap_infallible)] #![recursion_limit = "512"] -#![allow( - clippy::missing_docs_in_private_items, - clippy::must_use_candidate, - clippy::literal_string_with_formatting_args -)] +#![expect(clippy::literal_string_with_formatting_args, clippy::must_use_candidate)] #![warn( + rust_2018_idioms, trivial_casts, trivial_numeric_casts, - rust_2018_idioms, unused_lifetimes, unused_qualifications, rustc::internal diff --git a/clippy_lints/src/operators/const_comparisons.rs b/clippy_lints/src/operators/const_comparisons.rs index 2d01bccc0fa6..8891577fc975 100644 --- a/clippy_lints/src/operators/const_comparisons.rs +++ b/clippy_lints/src/operators/const_comparisons.rs @@ -1,4 +1,4 @@ -#![allow(clippy::match_same_arms)] +#![expect(clippy::match_same_arms)] use std::cmp::Ordering; diff --git a/clippy_lints/src/suspicious_operation_groupings.rs b/clippy_lints/src/suspicious_operation_groupings.rs index 02565198e4d2..57f5fc903956 100644 --- a/clippy_lints/src/suspicious_operation_groupings.rs +++ b/clippy_lints/src/suspicious_operation_groupings.rs @@ -539,7 +539,7 @@ fn ident_difference_expr_with_base_location( // then we should be able to change this function to do the correct traversal, // without needing to change the rest of the code. - #![allow(clippy::enum_glob_use)] + #[allow(clippy::enum_glob_use)] use ExprKind::*; match ( diff --git a/clippy_lints/src/unnested_or_patterns.rs b/clippy_lints/src/unnested_or_patterns.rs index 26b666e58b01..153e151acdd1 100644 --- a/clippy_lints/src/unnested_or_patterns.rs +++ b/clippy_lints/src/unnested_or_patterns.rs @@ -1,4 +1,4 @@ -#![allow(clippy::wildcard_imports, clippy::enum_glob_use)] +#![allow(clippy::enum_glob_use, clippy::wildcard_imports)] use clippy_config::Conf; use clippy_utils::ast_utils::{eq_field_pat, eq_id, eq_maybe_qself, eq_pat, eq_path}; diff --git a/clippy_lints_internal/src/lib.rs b/clippy_lints_internal/src/lib.rs index 4931f8765bdd..cda5d9df3f4b 100644 --- a/clippy_lints_internal/src/lib.rs +++ b/clippy_lints_internal/src/lib.rs @@ -1,21 +1,18 @@ #![feature(rustc_private)] -#![allow( - clippy::missing_docs_in_private_items, - clippy::must_use_candidate, +#![expect( + clippy::missing_clippy_version_attribute, // None of these lints need a version. clippy::symbol_as_str )] #![warn( + rust_2018_idioms, trivial_casts, trivial_numeric_casts, - rust_2018_idioms, unused_lifetimes, unused_qualifications, rustc::internal )] // Disable this rustc lint for now, as it was also done in rustc #![allow(rustc::potential_query_instability)] -// None of these lints need a version. -#![allow(clippy::missing_clippy_version_attribute)] extern crate rustc_ast; extern crate rustc_attr_parsing; diff --git a/clippy_utils/src/ast_utils/mod.rs b/clippy_utils/src/ast_utils/mod.rs index 85631d526550..472bbc1d92be 100644 --- a/clippy_utils/src/ast_utils/mod.rs +++ b/clippy_utils/src/ast_utils/mod.rs @@ -2,7 +2,7 @@ //! //! - The `eq_foobar` functions test for semantic equality but ignores `NodeId`s and `Span`s. -#![allow(clippy::wildcard_imports, clippy::enum_glob_use)] +#![allow(clippy::enum_glob_use, clippy::wildcard_imports)] use crate::{both, over}; use rustc_ast::attr::data_structures::CfgEntry; diff --git a/clippy_utils/src/lib.rs b/clippy_utils/src/lib.rs index b352529078f7..bb8f8e56605e 100644 --- a/clippy_utils/src/lib.rs +++ b/clippy_utils/src/lib.rs @@ -3,11 +3,11 @@ #![feature(rustc_private)] #![feature(unwrap_infallible)] #![recursion_limit = "512"] -#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc, clippy::must_use_candidate)] +#![expect(clippy::missing_errors_doc, clippy::missing_panics_doc, clippy::must_use_candidate)] #![warn( + rust_2018_idioms, trivial_casts, trivial_numeric_casts, - rust_2018_idioms, unused_lifetimes, unused_qualifications, rustc::internal diff --git a/clippy_utils/src/macros.rs b/clippy_utils/src/macros.rs index 3c000b420b9d..c0b4a62cdb0b 100644 --- a/clippy_utils/src/macros.rs +++ b/clippy_utils/src/macros.rs @@ -1,4 +1,4 @@ -#![allow(clippy::similar_names)] // `expr` and `expn` +#![expect(clippy::similar_names)] // `expr` and `expn` use std::cell::Cell; use std::sync::{Arc, OnceLock}; diff --git a/clippy_utils/src/source.rs b/clippy_utils/src/source.rs index 77e0d1000f95..9110f6cbf6b4 100644 --- a/clippy_utils/src/source.rs +++ b/clippy_utils/src/source.rs @@ -1,6 +1,6 @@ //! Utils for extracting, inspecting or transforming source code -#![allow(clippy::module_name_repetitions)] +#![expect(clippy::module_name_repetitions)] use std::sync::Arc; diff --git a/clippy_utils/src/sym.rs b/clippy_utils/src/sym.rs index a4c0a75ec3be..aaaf91f363ab 100644 --- a/clippy_utils/src/sym.rs +++ b/clippy_utils/src/sym.rs @@ -1,4 +1,4 @@ -#![allow(non_upper_case_globals)] +#![expect(non_upper_case_globals)] use rustc_span::symbol::PREDEFINED_SYMBOLS_COUNT; diff --git a/clippy_utils/src/ty/mod.rs b/clippy_utils/src/ty/mod.rs index 467a4f4de07d..5d22a7b0c207 100644 --- a/clippy_utils/src/ty/mod.rs +++ b/clippy_utils/src/ty/mod.rs @@ -1,6 +1,6 @@ //! Util methods for [`rustc_middle::ty`] -#![allow(clippy::module_name_repetitions)] +#![expect(clippy::module_name_repetitions)] use core::ops::ControlFlow; use itertools::Itertools as _; diff --git a/lintcheck/src/main.rs b/lintcheck/src/main.rs index 6a9e6cab8957..55eda5b4c33a 100644 --- a/lintcheck/src/main.rs +++ b/lintcheck/src/main.rs @@ -7,17 +7,15 @@ #![feature(iter_collect_into)] #![warn( + rust_2018_idioms, trivial_casts, trivial_numeric_casts, - rust_2018_idioms, unused_lifetimes, unused_qualifications )] -#![allow( - clippy::collapsible_else_if, - clippy::needless_borrows_for_generic_args, - clippy::module_name_repetitions, - clippy::literal_string_with_formatting_args +#![expect( + clippy::literal_string_with_formatting_args, + clippy::needless_borrows_for_generic_args )] mod config; diff --git a/src/driver.rs b/src/driver.rs index 890b15fe9e12..7b8e99698524 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -1,8 +1,9 @@ #![feature(rustc_private)] -// warn on lints, that are included in `rust-lang/rust`s bootstrap -#![warn(rust_2018_idioms, unused_lifetimes)] -// warn on rustc internal lints -#![warn(rustc::internal)] +#![warn( + // warn on lints, that are included in `rust-lang/rust`s bootstrap + rust_2018_idioms, unused_lifetimes, + // and on rustc internal lints + rustc::internal)] // FIXME: switch to something more ergonomic here, once available. // (Currently there is no way to opt into sysroot crates without `extern crate`.) From 84c4bbf05d55d8e26e24356bec04a7fd08f2a343 Mon Sep 17 00:00:00 2001 From: Samuel Tardieu Date: Wed, 27 May 2026 12:08:51 +0200 Subject: [PATCH 3/3] New Clippy source test: lint-attributes-organization --- tests/lint-attributes-organization.rs | 130 ++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/lint-attributes-organization.rs diff --git a/tests/lint-attributes-organization.rs b/tests/lint-attributes-organization.rs new file mode 100644 index 000000000000..80c1fa2a0a46 --- /dev/null +++ b/tests/lint-attributes-organization.rs @@ -0,0 +1,130 @@ +//! This test checks that Clippy files (source and tests): +//! +//! - only have one top-level lint attribute of each kind (`allow`/`deny`/`expect`/`forbid`/`warn`) +//! - put unqualified lint names before the qualified ones +//! - use alphabetical order inside each qualification +//! +//! This test is disabled when ran as part of the compiler test suite, to prevent +//! changes from being difficult to make there. Incorrect organizations will be +//! fixed during the merge process. +//! +//! This test may eventually be replaced by a restriction lint which imposes +//! constraints on attributes. Right now, this regex-based one does the job +//! on Clippy test files. It only checks top-level beginning-of-the-line attributes, +//! so that it won't trigger on a `#![cfg_attr(…, warn(…))]` which can legitimately be +//! used in addition to another `#![warn(…)]` attribute. + +use regex::Regex; +use std::collections::HashSet; +use std::{fs, iter}; +use test_utils::IS_RUSTC_TEST_SUITE; +use walkdir::{DirEntry, WalkDir}; + +mod test_utils; + +const SKIPPED_FILES: [&str; 7] = [ + "./tests/lint-attributes-organization.rs", // this file, for the sanity checks + "./tests/ui/blanket_clippy_restriction_lints.rs", // separate lines are better + "./tests/ui/deprecated.rs", // generated + "./tests/ui/duplicated_attributes.rs", // obviously + "./tests/ui/rename.rs", // generated + "./tests/ui/unknown_clippy_lints.rs", // separate lines are better + "./target/", // generated files +]; + +#[test] +fn lint_attribute_organization() { + if IS_RUSTC_TEST_SUITE { + return; + } + let attribute_regex = attribute_regex(); + let mut problem_found = false; + for path in WalkDir::new(".") + .into_iter() + .flatten() + .map(DirEntry::into_path) + .filter(|p| { + p.extension().is_some_and(|ext| ext == "rs") && !SKIPPED_FILES.iter().any(|&skipped| p.starts_with(skipped)) + }) + { + for diag in error_for_content(&fs::read_to_string(&path).unwrap(), &attribute_regex) { + eprintln!("ERROR: {}: {diag}", path.display()); + problem_found = true; + } + } + assert!( + !problem_found, + "some lint attributes do not meet the attributes organization requirements" + ); +} + +fn attribute_regex() -> Regex { + Regex::new(r"(?ms)^#!\[(allow|deny|expect|forbid|warn)\((.+?)\)\]").unwrap() +} + +// Check if file content contains only ordered lint names in top-level inner attributes, and +// also that every attribute is present only once. +fn error_for_content(content: &str, attribute_regex: &Regex) -> Vec { + let mut diags = vec![]; + let mut attributes = HashSet::new(); + for cap in attribute_regex.captures_iter(content) { + let attribute = &cap[1]; + if !attributes.insert(attribute.to_owned()) { + diags.push(format!("duplicate top-level `#![{attribute}]` attribute")); + } + let lint_names = cap[2] + .lines() + // Split off any trailing comments + .map(|l| l.split_once("//").map_or(l, |(b, _)| b).trim()) + .flat_map(|l| l.split_whitespace().map(|s| s.strip_suffix(',').unwrap_or(s))) + .collect::>(); + for [a, b] in lint_names.array_windows() { + if a.contains("::") && !b.contains("::") { + diags.push(format!( + "qualified lint names (`{a}`) must come after unqualified ones (`{b}`)" + )); + } else if !a.contains("::") && b.contains("::") { + } else if a > b { + diags.push(format!("lint names must be ordered: `{b}` must come before `{a}`")); + } + } + } + diags +} + +#[test] +fn internal_sanity_check() { + let attribute_regex = attribute_regex(); + check_errors( + " +#![allow( + clippy::def, // Comment + clippy::abc +)] +#![allow(clippy::zyx, unknown)]", + vec![ + "lint names must be ordered: `clippy::abc` must come before `clippy::def`", + "duplicate top-level `#![allow]` attribute", + "qualified lint names (`clippy::zyx`) must come after unqualified ones (`unknown`)", + ], + &attribute_regex, + ); + check_errors( + "#![warn(before, after)]", + vec!["lint names must be ordered: `after` must come before `before`"], + &attribute_regex, + ); + check_errors( + "#![deny(clippy::abc, clippy::def, clippy::zyx)]", + vec![], + &attribute_regex, + ); +} + +fn check_errors(content: &str, expected: Vec<&str>, attribute_regex: &Regex) { + let errors = error_for_content(content, attribute_regex); + assert_eq!(errors.len(), expected.len()); + for (i, (error, expected)) in iter::zip(errors, expected).enumerate() { + assert_eq!(&error, expected, "the {}-th error differs from the expectation", i + 1); + } +}