fix: allow_attributes false negative on attributes with whitespace (#16497)

The [`allow_attributes`] lint false-negatived (failed to trigger) on
attributes that contained internal whitespace, such as `#[ allow (
dead_code ) ]`.

This happened because `clippy_utils::is_from_proc_macro` relied on
strict string matching (e.g., expecting exactly `#[allow`), which fails
if there are spaces.

#### Solution:
I have updated `clippy_utils::is_from_proc_macro` to support flexible
whitespace matching.
1. Added `Pat::Attr(Symbol)`.
2. Updated `span_matches_pat` to strip `#[` and trim validation
whitespace before checking the name.

Verified with a new test case in `tests/ui/allow_attributes.rs`.

Fixes rust-lang/rust-clippy#16491

----

changelog: [`allow_attributes`]: correctly detect attributes with
internal whitespace
This commit is contained in:
llogiq 2026-02-02 08:06:54 +00:00 committed by GitHub
commit 301aae6ac8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 28 additions and 13 deletions

View file

@ -45,6 +45,8 @@ pub enum Pat {
Sym(Symbol),
/// Any decimal or hexadecimal digit depending on the location.
Num,
/// An attribute.
Attr(Symbol),
}
/// Checks if the start and the end of the span's text matches the patterns. This will return false
@ -65,12 +67,20 @@ fn span_matches_pat(sess: &Session, span: Span, start_pat: Pat, end_pat: Pat) ->
Pat::OwnedMultiStr(texts) => texts.iter().any(|s| start_str.starts_with(s)),
Pat::Sym(sym) => start_str.starts_with(sym.as_str()),
Pat::Num => start_str.as_bytes().first().is_some_and(u8::is_ascii_digit),
Pat::Attr(sym) => {
let start_str = start_str
.strip_prefix("#[")
.or_else(|| start_str.strip_prefix("#!["))
.unwrap_or(start_str);
start_str.trim_start().starts_with(sym.as_str())
},
} && match end_pat {
Pat::Str(text) => end_str.ends_with(text),
Pat::MultiStr(texts) => texts.iter().any(|s| end_str.ends_with(s)),
Pat::OwnedMultiStr(texts) => texts.iter().any(|s| end_str.ends_with(s)),
Pat::Sym(sym) => end_str.ends_with(sym.as_str()),
Pat::Num => end_str.as_bytes().last().is_some_and(u8::is_ascii_hexdigit),
Pat::Attr(_) => false,
})
})
}
@ -350,18 +360,7 @@ fn attr_search_pat(attr: &Attribute) -> (Pat, Pat) {
AttrKind::Normal(..) => {
if let Some(name) = attr.name() {
// NOTE: This will likely have false positives, like `allow = 1`
let ident_string = name.to_string();
if attr.style == AttrStyle::Outer {
(
Pat::OwnedMultiStr(vec!["#[".to_owned() + &ident_string, ident_string]),
Pat::Str(""),
)
} else {
(
Pat::OwnedMultiStr(vec!["#![".to_owned() + &ident_string, ident_string]),
Pat::Str(""),
)
}
(Pat::Attr(name), Pat::Str(""))
} else {
(Pat::Str("#"), Pat::Str("]"))
}

View file

@ -63,6 +63,11 @@ fn msrv_1_80() {
let x = 1;
}
#[rustfmt::skip]
#[ expect ( dead_code ) ]
//~^ allow_attributes
struct Spaced;
#[deny(clippy::allow_attributes)]
fn deny_allow_attributes() -> Option<u8> {
let allow = None;

View file

@ -63,6 +63,11 @@ fn msrv_1_80() {
let x = 1;
}
#[rustfmt::skip]
#[ allow ( dead_code ) ]
//~^ allow_attributes
struct Spaced;
#[deny(clippy::allow_attributes)]
fn deny_allow_attributes() -> Option<u8> {
let allow = None;

View file

@ -19,5 +19,11 @@ error: #[allow] attribute found
LL | #[allow(unused)]
| ^^^^^ help: replace it with: `expect`
error: aborting due to 3 previous errors
error: #[allow] attribute found
--> tests/ui/allow_attributes.rs:67:4
|
LL | #[ allow ( dead_code ) ]
| ^^^^^ help: replace it with: `expect`
error: aborting due to 4 previous errors