|
| 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