Skip to content

Commit 4a8f5d0

Browse files
committed
New Clippy source test: lint-attributes-organization
1 parent 50119f3 commit 4a8f5d0

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//! This test checks that Clippy files (source and tests):
2+
//!
3+
//! - only have one top-level lint attribute of each kind (`allow`/`deny`/`expect`/`forbid`/`warn`)
4+
//! - put unqualified lint names before the qualified ones
5+
//! - use alphabetical order inside each qualification
6+
//!
7+
//! This test is disabled when ran as part of the compiler test suite, to prevent
8+
//! changes from being difficult to make there. Incorrect organizations will be
9+
//! fixed during the merge process.
10+
//!
11+
//! This test may eventually be replaced by a restriction lint which imposes
12+
//! constraints on attributes. Right now, this regex-based one does the job
13+
//! on Clippy test files. It only checks top-level beginning-of-the-line attributes,
14+
//! so that it won't trigger on a `#![cfg_attr(…, warn(…))]` which can legitimately be
15+
//! used in addition to another `#![warn(…)]` attribute.
16+
17+
use regex::Regex;
18+
use std::collections::HashSet;
19+
use std::fs;
20+
use test_utils::IS_RUSTC_TEST_SUITE;
21+
use walkdir::{DirEntry, WalkDir};
22+
23+
mod test_utils;
24+
25+
const SKIPPED_FILES: [&str; 7] = [
26+
"./tests/lint-attributes-organization.rs", // this file, for the sanity checks
27+
"./tests/ui/blanket_clippy_restriction_lints.rs", // separate lines are better
28+
"./tests/ui/deprecated.rs", // generated
29+
"./tests/ui/duplicated_attributes.rs", // obviously
30+
"./tests/ui/rename.rs", // generated
31+
"./tests/ui/unknown_clippy_lints.rs", // separate lines are better
32+
"./target/", // generated files
33+
];
34+
35+
#[test]
36+
fn lint_attribute_organization() {
37+
if IS_RUSTC_TEST_SUITE {
38+
return;
39+
}
40+
let attribute_regex = attribute_regex();
41+
let mut problem_found = false;
42+
for path in WalkDir::new(".")
43+
.into_iter()
44+
.flatten()
45+
.map(DirEntry::into_path)
46+
.filter(|p| {
47+
p.extension().is_some_and(|ext| ext == "rs") && !SKIPPED_FILES.iter().any(|&skipped| p.starts_with(skipped))
48+
})
49+
{
50+
for diag in error_for_content(&fs::read_to_string(&path).unwrap(), &attribute_regex) {
51+
eprintln!("ERROR: {}: {diag}", path.to_str().unwrap());
52+
problem_found = true;
53+
}
54+
}
55+
assert!(
56+
!problem_found,
57+
"some lint attributes do not meet the attributes organization requirements"
58+
);
59+
}
60+
61+
fn attribute_regex() -> Regex {
62+
Regex::new(r"(?ms)^#!\[(allow|deny|expect|forbid|warn)\((.+?)\)\]").unwrap()
63+
}
64+
65+
// Check if file content contains only ordered lint names in top-level inner attributes, and
66+
// also that every attribute is present only once.
67+
fn error_for_content(content: &str, attribute_regex: &Regex) -> Vec<String> {
68+
let mut diags = vec![];
69+
let mut attributes = HashSet::new();
70+
for cap in attribute_regex.captures_iter(content) {
71+
let attribute = &cap[1];
72+
if !attributes.insert(attribute.to_owned()) {
73+
diags.push(format!("duplicate top-level `#![{attribute}]` attribute"));
74+
}
75+
let lint_names = cap[2]
76+
.lines()
77+
.map(|l| l.split_once("//").map_or(l, |(b, _)| b).trim())
78+
.flat_map(|l| l.split_whitespace().map(|s| s.strip_suffix(',').unwrap_or(s)))
79+
.collect::<Vec<_>>();
80+
for (&a, &b) in lint_names.iter().zip(lint_names.iter().skip(1)) {
81+
if a.contains("::") && !b.contains("::") {
82+
diags.push(format!(
83+
"qualified lint names (`{a}`) must come after unqualified ones (`{b}`)"
84+
));
85+
} else if !a.contains("::") && b.contains("::") {
86+
} else if a > b {
87+
diags.push(format!("lint names must be ordered: `{b}` must come before `{a}`"));
88+
}
89+
}
90+
}
91+
diags
92+
}
93+
94+
#[test]
95+
fn internal_sanity_check() {
96+
let attribute_regex = attribute_regex();
97+
check_errors(
98+
"
99+
#![allow(
100+
clippy::def, // Comment
101+
clippy::abc
102+
)]
103+
#![allow(clippy::zyx, unknown)]",
104+
vec![
105+
"lint names must be ordered: `clippy::abc` must come before `clippy::def`",
106+
"duplicate top-level `#![allow]` attribute",
107+
"qualified lint names (`clippy::zyx`) must come after unqualified ones (`unknown`)",
108+
],
109+
&attribute_regex,
110+
);
111+
check_errors(
112+
"#![warn(before, after)]",
113+
vec!["lint names must be ordered: `after` must come before `before`"],
114+
&attribute_regex,
115+
);
116+
check_errors(
117+
"#![deny(clippy::abc, clippy::def, clippy::zyx)]",
118+
vec![],
119+
&attribute_regex,
120+
);
121+
}
122+
123+
fn check_errors(content: &str, expected: Vec<&str>, attribute_regex: &Regex) {
124+
let errors = error_for_content(content, attribute_regex);
125+
assert_eq!(errors.len(), expected.len());
126+
for (error, expected) in errors.into_iter().zip(expected) {
127+
assert_eq!(&error, expected);
128+
}
129+
}

0 commit comments

Comments
 (0)