Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions clippy_lints/src/matches/significant_drop_in_scrutinee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::FxHashSet;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::{first_line_of_span, indent_of, snippet};
use clippy_utils::ty::{for_each_top_level_late_bound_region, is_copy};
use clippy_utils::{get_attr, is_lint_allowed, sym};
use clippy_utils::{get_builtin_attr, is_lint_allowed, sym};
use itertools::Itertools;
use rustc_ast::Mutability;
use rustc_data_structures::fx::FxIndexSet;
Expand Down Expand Up @@ -183,7 +183,7 @@ impl<'a, 'tcx> SigDropChecker<'a, 'tcx> {

fn has_sig_drop_attr_impl(&mut self, ty: Ty<'tcx>) -> bool {
if let Some(adt) = ty.ty_adt_def()
&& get_attr(
&& get_builtin_attr(
self.cx.sess(),
self.cx.tcx.get_all_attrs(adt.did()),
sym::has_significant_drop,
Expand Down
4 changes: 2 additions & 2 deletions clippy_lints/src/significant_drop_tightening.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::source::{indent_of, snippet};
use clippy_utils::{expr_or_init, get_attr, path_to_local, peel_hir_expr_unary, sym};
use clippy_utils::{expr_or_init, get_builtin_attr, path_to_local, peel_hir_expr_unary, sym};
use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
Expand Down Expand Up @@ -166,7 +166,7 @@ impl<'cx, 'others, 'tcx> AttrChecker<'cx, 'others, 'tcx> {

fn has_sig_drop_attr_uncached(&mut self, ty: Ty<'tcx>, depth: usize) -> bool {
if let Some(adt) = ty.ty_adt_def() {
let mut iter = get_attr(
let mut iter = get_builtin_attr(
self.cx.sess(),
self.cx.tcx.get_all_attrs(adt.did()),
sym::has_significant_drop,
Expand Down
4 changes: 2 additions & 2 deletions clippy_lints/src/utils/author.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clippy_utils::{MaybePath, get_attr, higher, path_def_id, sym};
use clippy_utils::{MaybePath, get_builtin_attr, higher, path_def_id, sym};
use itertools::Itertools;
use rustc_ast::LitIntType;
use rustc_ast::ast::{LitFloatType, LitKind};
Expand Down Expand Up @@ -856,5 +856,5 @@ impl<'a, 'tcx> PrintVisitor<'a, 'tcx> {

fn has_attr(cx: &LateContext<'_>, hir_id: HirId) -> bool {
let attrs = cx.tcx.hir_attrs(hir_id);
get_attr(cx.sess(), attrs, sym::author).count() > 0
get_builtin_attr(cx.sess(), attrs, sym::author).count() > 0
}
4 changes: 2 additions & 2 deletions clippy_lints/src/utils/dump_hir.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clippy_utils::{get_attr, sym};
use clippy_utils::{get_builtin_attr, sym};
use hir::TraitItem;
use rustc_hir as hir;
use rustc_lint::{LateContext, LateLintPass, LintContext};
Expand Down Expand Up @@ -60,5 +60,5 @@ impl<'tcx> LateLintPass<'tcx> for DumpHir {

fn has_attr(cx: &LateContext<'_>, hir_id: hir::HirId) -> bool {
let attrs = cx.tcx.hir_attrs(hir_id);
get_attr(cx.sess(), attrs, sym::dump).count() > 0
get_builtin_attr(cx.sess(), attrs, sym::dump).count() > 0
}
212 changes: 96 additions & 116 deletions clippy_utils/src/attrs.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Utility functions for attributes, including Clippy's built-in ones

use crate::source::SpanRangeExt;
use crate::{sym, tokenize_with_text};
use rustc_ast::attr;
Expand All @@ -12,131 +14,59 @@ use rustc_session::Session;
use rustc_span::{Span, Symbol};
use std::str::FromStr;

/// Deprecation status of attributes known by Clippy.
pub enum DeprecationStatus {
/// Attribute is deprecated
Deprecated,
/// Attribute is deprecated and was replaced by the named attribute
Replaced(&'static str),
None,
}

#[rustfmt::skip]
pub const BUILTIN_ATTRIBUTES: &[(Symbol, DeprecationStatus)] = &[
(sym::author, DeprecationStatus::None),
(sym::version, DeprecationStatus::None),
(sym::cognitive_complexity, DeprecationStatus::None),
(sym::cyclomatic_complexity, DeprecationStatus::Replaced("cognitive_complexity")),
(sym::dump, DeprecationStatus::None),
(sym::msrv, DeprecationStatus::None),
// The following attributes are for the 3rd party crate authors.
// See book/src/attribs.md
(sym::has_significant_drop, DeprecationStatus::None),
(sym::format_args, DeprecationStatus::None),
];

pub struct LimitStack {
stack: Vec<u64>,
}

impl Drop for LimitStack {
fn drop(&mut self) {
assert_eq!(self.stack.len(), 1);
}
}

impl LimitStack {
#[must_use]
pub fn new(limit: u64) -> Self {
Self { stack: vec![limit] }
}
pub fn limit(&self) -> u64 {
*self.stack.last().expect("there should always be a value in the stack")
}
pub fn push_attrs(&mut self, sess: &Session, attrs: &[impl AttributeExt], name: Symbol) {
let stack = &mut self.stack;
parse_attrs(sess, attrs, name, |val| stack.push(val));
}
pub fn pop_attrs(&mut self, sess: &Session, attrs: &[impl AttributeExt], name: Symbol) {
let stack = &mut self.stack;
parse_attrs(sess, attrs, name, |val| assert_eq!(stack.pop(), Some(val)));
}
}

pub fn get_attr<'a, A: AttributeExt + 'a>(
/// Given `attrs`, extract all the instances of a built-in Clippy attribute called `name`
pub fn get_builtin_attr<'a, A: AttributeExt + 'a>(
sess: &'a Session,
attrs: &'a [A],
name: Symbol,
) -> impl Iterator<Item = &'a A> {
attrs.iter().filter(move |attr| {
let Some(attr_segments) = attr.ident_path() else {
return false;
};
if let Some([clippy, segment2]) = attr.ident_path().as_deref()
&& clippy.name == sym::clippy
{
let new_name = match segment2.name {
sym::cyclomatic_complexity => Some("cognitive_complexity"),
sym::author
| sym::version
| sym::cognitive_complexity
| sym::dump
| sym::msrv
// The following attributes are for the 3rd party crate authors.
// See book/src/attribs.md
| sym::has_significant_drop
| sym::format_args => None,
_ => {
sess.dcx().span_err(segment2.span, "usage of unknown attribute");
return false;
},
};

if attr_segments.len() == 2 && attr_segments[0].name == sym::clippy {
BUILTIN_ATTRIBUTES
.iter()
.find_map(|(builtin_name, deprecation_status)| {
if attr_segments[1].name == *builtin_name {
Some(deprecation_status)
} else {
None
}
})
.map_or_else(
|| {
sess.dcx().span_err(attr_segments[1].span, "usage of unknown attribute");
false
},
|deprecation_status| {
let mut diag = sess
.dcx()
.struct_span_err(attr_segments[1].span, "usage of deprecated attribute");
match *deprecation_status {
DeprecationStatus::Deprecated => {
diag.emit();
false
},
DeprecationStatus::Replaced(new_name) => {
diag.span_suggestion(
attr_segments[1].span,
"consider using",
new_name,
Applicability::MachineApplicable,
);
diag.emit();
false
},
DeprecationStatus::None => {
diag.cancel();
attr_segments[1].name == name
},
}
},
)
match new_name {
Some(new_name) => {
sess.dcx()
.struct_span_err(segment2.span, "usage of deprecated attribute")
.with_span_suggestion(
segment2.span,
"consider using",
new_name,
Applicability::MachineApplicable,
)
.emit();
false
},
None => segment2.name == name,
}
} else {
false
}
})
}

fn parse_attrs<F: FnMut(u64)>(sess: &Session, attrs: &[impl AttributeExt], name: Symbol, mut f: F) {
for attr in get_attr(sess, attrs, name) {
if let Some(value) = attr.value_str() {
if let Ok(value) = FromStr::from_str(value.as_str()) {
f(value);
} else {
sess.dcx().span_err(attr.span(), "not a number");
}
} else {
sess.dcx().span_err(attr.span(), "bad clippy attribute");
}
}
}

pub fn get_unique_attr<'a, A: AttributeExt>(sess: &'a Session, attrs: &'a [A], name: Symbol) -> Option<&'a A> {
/// If `attrs` contain exactly one instance of a built-in Clippy attribute called `name`,
/// returns that attribute, and `None` otherwise
pub fn get_unique_builtin_attr<'a, A: AttributeExt>(sess: &'a Session, attrs: &'a [A], name: Symbol) -> Option<&'a A> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're at it, I wonder whether this function should even exist as it is now. It emits a warning on duplicate attributes, but that should already be covered by duplicated_attributes; well, maybe apart for #[cognitive_complexity(N)] -- if the N is different, that lint wouldn't fire, but this check will.

So maybe its uses could be simplified to just get_builtin_attr.next_back()

Copy link
Contributor

@Jarcho Jarcho Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be better, but I think this also warns on the renamed attributes when both names appear. I would be surprised if that ever actually happens in practice.

let mut unique_attr: Option<&A> = None;
for attr in get_attr(sess, attrs, name) {
for attr in get_builtin_attr(sess, attrs, name) {
if let Some(duplicate) = unique_attr {
sess.dcx()
.struct_span_err(attr.span(), format!("`{name}` is defined multiple times"))
Expand All @@ -149,13 +79,13 @@ pub fn get_unique_attr<'a, A: AttributeExt>(sess: &'a Session, attrs: &'a [A], n
unique_attr
}

/// Returns true if the attributes contain any of `proc_macro`,
/// `proc_macro_derive` or `proc_macro_attribute`, false otherwise
/// Checks whether `attrs` contain any of `proc_macro`, `proc_macro_derive` or
/// `proc_macro_attribute`
pub fn is_proc_macro(attrs: &[impl AttributeExt]) -> bool {
attrs.iter().any(AttributeExt::is_proc_macro_attr)
}

/// Returns true if the attributes contain `#[doc(hidden)]`
/// Checks whether `attrs` contain `#[doc(hidden)]`
pub fn is_doc_hidden(attrs: &[impl AttributeExt]) -> bool {
attrs
.iter()
Expand All @@ -164,6 +94,7 @@ pub fn is_doc_hidden(attrs: &[impl AttributeExt]) -> bool {
.any(|l| attr::list_contains_name(&l, sym::hidden))
}

/// Checks whether the given ADT, or any of its fields/variants, are marked as `#[non_exhaustive]`
pub fn has_non_exhaustive_attr(tcx: TyCtxt<'_>, adt: AdtDef<'_>) -> bool {
adt.is_variant_list_non_exhaustive()
|| find_attr!(tcx.get_all_attrs(adt.did()), AttributeKind::NonExhaustive(..))
Expand All @@ -176,7 +107,7 @@ pub fn has_non_exhaustive_attr(tcx: TyCtxt<'_>, adt: AdtDef<'_>) -> bool {
.any(|field_def| find_attr!(tcx.get_all_attrs(field_def.did), AttributeKind::NonExhaustive(..)))
}

/// Checks if the given span contains a `#[cfg(..)]` attribute
/// Checks whether the given span contains a `#[cfg(..)]` attribute
pub fn span_contains_cfg(cx: &LateContext<'_>, s: Span) -> bool {
s.check_source_text(cx, |src| {
let mut iter = tokenize_with_text(src);
Expand All @@ -198,3 +129,52 @@ pub fn span_contains_cfg(cx: &LateContext<'_>, s: Span) -> bool {
false
})
}

/// Currently used to keep track of the current value of `#[clippy::cognitive_complexity(N)]`
pub struct LimitStack {
default: u64,
stack: Vec<u64>,
}

impl Drop for LimitStack {
fn drop(&mut self) {
debug_assert_eq!(self.stack, Vec::<u64>::new()); // avoid `.is_empty()`, for a nicer error message
}
}

#[expect(missing_docs, reason = "they're all trivial...")]
impl LimitStack {
#[must_use]
/// Initialize the stack starting with a default value, which usually comes from configuration
pub fn new(limit: u64) -> Self {
Self {
default: limit,
stack: vec![],
}
}
pub fn limit(&self) -> u64 {
self.stack.last().copied().unwrap_or(self.default)
}
pub fn push_attrs(&mut self, sess: &Session, attrs: &[impl AttributeExt], name: Symbol) {
let stack = &mut self.stack;
parse_attrs(sess, attrs, name, |val| stack.push(val));
}
pub fn pop_attrs(&mut self, sess: &Session, attrs: &[impl AttributeExt], name: Symbol) {
let stack = &mut self.stack;
parse_attrs(sess, attrs, name, |val| debug_assert_eq!(stack.pop(), Some(val)));
}
}

fn parse_attrs<F: FnMut(u64)>(sess: &Session, attrs: &[impl AttributeExt], name: Symbol, mut f: F) {
for attr in get_builtin_attr(sess, attrs, name) {
let Some(value) = attr.value_str() else {
sess.dcx().span_err(attr.span(), "bad clippy attribute");
continue;
};
let Ok(value) = u64::from_str(value.as_str()) else {
sess.dcx().span_err(attr.span(), "not a number");
continue;
};
f(value);
}
}
1 change: 1 addition & 0 deletions clippy_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ extern crate rustc_trait_selection;
extern crate smallvec;

pub mod ast_utils;
#[deny(missing_docs)]
pub mod attrs;
mod check_proc_macro;
pub mod comparisons;
Expand Down
4 changes: 2 additions & 2 deletions clippy_utils/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::sync::{Arc, OnceLock};

use crate::visitors::{Descend, for_each_expr_without_closures};
use crate::{get_unique_attr, sym};
use crate::{get_unique_builtin_attr, sym};

use arrayvec::ArrayVec;
use rustc_ast::{FormatArgs, FormatArgument, FormatPlaceholder};
Expand Down Expand Up @@ -42,7 +42,7 @@ pub fn is_format_macro(cx: &LateContext<'_>, macro_def_id: DefId) -> bool {
} else {
// Allow users to tag any macro as being format!-like
// TODO: consider deleting FORMAT_MACRO_DIAG_ITEMS and using just this method
get_unique_attr(cx.sess(), cx.tcx.get_all_attrs(macro_def_id), sym::format_args).is_some()
get_unique_builtin_attr(cx.sess(), cx.tcx.get_all_attrs(macro_def_id), sym::format_args).is_some()
}
}

Expand Down