From d017b2c4fd5fc8b5e420306582db2a4ec147d9d7 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 11 Dec 2025 21:08:01 +0100 Subject: [PATCH] Add new `disallowed_fields` lint --- CHANGELOG.md | 2 + book/src/lint_configuration.md | 17 +++ clippy_config/src/conf.rs | 11 ++ clippy_lints/src/declared_lints.rs | 1 + clippy_lints/src/disallowed_fields.rs | 113 ++++++++++++++++++ clippy_lints/src/lib.rs | 2 + clippy_utils/src/paths.rs | 12 ++ .../toml_disallowed_fields/clippy.toml | 12 ++ .../conf_disallowed_fields.rs | 29 +++++ .../conf_disallowed_fields.stderr | 37 ++++++ .../toml_unknown_key/conf_unknown_key.stderr | 3 + 11 files changed, 239 insertions(+) create mode 100644 clippy_lints/src/disallowed_fields.rs create mode 100644 tests/ui-toml/toml_disallowed_fields/clippy.toml create mode 100644 tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs create mode 100644 tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f666caf306f..56ce13127110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6374,6 +6374,7 @@ Released 2018-09-13 [`derive_ord_xor_partial_ord`]: https://rust-lang.github.io/rust-clippy/master/index.html#derive_ord_xor_partial_ord [`derive_partial_eq_without_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#derive_partial_eq_without_eq [`derived_hash_with_manual_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#derived_hash_with_manual_eq +[`disallowed_fields`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_fields [`disallowed_macros`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros [`disallowed_method`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_method [`disallowed_methods`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods @@ -7189,6 +7190,7 @@ Released 2018-09-13 [`check-private-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#check-private-items [`cognitive-complexity-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#cognitive-complexity-threshold [`const-literal-digits-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#const-literal-digits-threshold +[`disallowed-fields`]: https://doc.rust-lang.org/clippy/lint_configuration.html#disallowed-fields [`disallowed-macros`]: https://doc.rust-lang.org/clippy/lint_configuration.html#disallowed-macros [`disallowed-methods`]: https://doc.rust-lang.org/clippy/lint_configuration.html#disallowed-methods [`disallowed-names`]: https://doc.rust-lang.org/clippy/lint_configuration.html#disallowed-names diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md index a1c079898594..f5a341941d42 100644 --- a/book/src/lint_configuration.md +++ b/book/src/lint_configuration.md @@ -505,6 +505,23 @@ The minimum digits a const float literal must have to supress the `excessive_pre * [`excessive_precision`](https://rust-lang.github.io/rust-clippy/master/index.html#excessive_precision) +## `disallowed-fields` +The list of disallowed fields, written as fully qualified paths. + +**Fields:** +- `path` (required): the fully qualified path to the field that should be disallowed +- `reason` (optional): explanation why this field is disallowed +- `replacement` (optional): suggested alternative method +- `allow-invalid` (optional, `false` by default): when set to `true`, it will ignore this entry + if the path doesn't exist, instead of emitting an error + +**Default Value:** `[]` + +--- +**Affected lints:** +* [`disallowed_fields`](https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_fields) + + ## `disallowed-macros` The list of disallowed macros, written as fully qualified paths. diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs index e1d7c1d88eb9..7c06906e3b34 100644 --- a/clippy_config/src/conf.rs +++ b/clippy_config/src/conf.rs @@ -581,6 +581,17 @@ define_Conf! { /// Use the Cognitive Complexity lint instead. #[conf_deprecated("Please use `cognitive-complexity-threshold` instead", cognitive_complexity_threshold)] cyclomatic_complexity_threshold: u64 = 25, + /// The list of disallowed fields, written as fully qualified paths. + /// + /// **Fields:** + /// - `path` (required): the fully qualified path to the field that should be disallowed + /// - `reason` (optional): explanation why this field is disallowed + /// - `replacement` (optional): suggested alternative method + /// - `allow-invalid` (optional, `false` by default): when set to `true`, it will ignore this entry + /// if the path doesn't exist, instead of emitting an error + #[disallowed_paths_allow_replacements = true] + #[lints(disallowed_fields)] + disallowed_fields: Vec = Vec::new(), /// The list of disallowed macros, written as fully qualified paths. /// /// **Fields:** diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 87d75234ebc0..5020df2269c5 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -105,6 +105,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::derive::DERIVE_PARTIAL_EQ_WITHOUT_EQ_INFO, crate::derive::EXPL_IMPL_CLONE_ON_COPY_INFO, crate::derive::UNSAFE_DERIVE_DESERIALIZE_INFO, + crate::disallowed_fields::DISALLOWED_FIELDS_INFO, crate::disallowed_macros::DISALLOWED_MACROS_INFO, crate::disallowed_methods::DISALLOWED_METHODS_INFO, crate::disallowed_names::DISALLOWED_NAMES_INFO, diff --git a/clippy_lints/src/disallowed_fields.rs b/clippy_lints/src/disallowed_fields.rs new file mode 100644 index 000000000000..6bd68f2937d5 --- /dev/null +++ b/clippy_lints/src/disallowed_fields.rs @@ -0,0 +1,113 @@ +use clippy_config::Conf; +use clippy_config::types::{DisallowedPath, create_disallowed_map}; +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::paths::PathNS; +use rustc_hir::def::{DefKind, Res}; +use rustc_hir::def_id::DefIdMap; +use rustc_hir::{Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty::{Adt, TyCtxt}; +use rustc_session::impl_lint_pass; + +declare_clippy_lint! { + /// ### What it does + /// Denies the configured fields in clippy.toml + /// + /// Note: Even though this lint is warn-by-default, it will only trigger if + /// fields are defined in the clippy.toml file. + /// + /// ### Why is this bad? + /// Some fields are undesirable in certain contexts, and it's beneficial to + /// lint for them as needed. + /// + /// ### Example + /// An example clippy.toml configuration: + /// ```toml + /// # clippy.toml + /// disallowed-fields = [ + /// # Can use a string as the path of the disallowed field. + /// "std::ops::Range::start", + /// # Can also use an inline table with a `path` key. + /// { path = "std::ops::Range::start" }, + /// # When using an inline table, can add a `reason` for why the field + /// # is disallowed. + /// { path = "std::ops::Range::start", reason = "The start of the range is not used" }, + /// ] + /// ``` + /// + /// ```rust + /// use std::ops::Range; + /// + /// let range = Range { start: 0, end: 1 }; + /// println!("{}", range.start); // `start` is disallowed in the config. + /// ``` + /// + /// Use instead: + /// ```rust + /// use std::ops::Range; + /// + /// let range = Range { start: 0, end: 1 }; + /// println!("{}", range.end); // `end` is _not_ disallowed in the config. + /// ``` + #[clippy::version = "1.93.0"] + pub DISALLOWED_FIELDS, + style, + "declaration of a disallowed field use" +} + +pub struct DisallowedFields { + disallowed: DefIdMap<(&'static str, &'static DisallowedPath)>, +} + +impl DisallowedFields { + pub fn new(tcx: TyCtxt<'_>, conf: &'static Conf) -> Self { + let (disallowed, _) = create_disallowed_map( + tcx, + &conf.disallowed_fields, + PathNS::Value, + |def_kind| matches!(def_kind, DefKind::Field), + "field", + false, + ); + Self { disallowed } + } +} + +impl_lint_pass!(DisallowedFields => [DISALLOWED_FIELDS]); + +impl<'tcx> LateLintPass<'tcx> for DisallowedFields { + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { + let (id, span) = match &expr.kind { + ExprKind::Path(path) if let Res::Def(_, id) = cx.qpath_res(path, expr.hir_id) => (id, expr.span), + ExprKind::Field(e, ident) => { + // Very round-about way to get the field `DefId` from the expr: first we get its + // parent `Ty`. Then we go through all its fields to find the one with the expected + // name and get the `DefId` from it. + if let Some(parent_ty) = cx.typeck_results().expr_ty_opt(e) + && let Adt(adt_def, ..) = parent_ty.kind() + && let Some(field_def_id) = adt_def.all_fields().find_map(|field| { + if field.name == ident.name { + Some(field.did) + } else { + None + } + }) + { + (field_def_id, ident.span) + } else { + return; + } + }, + _ => return, + }; + if let Some(&(path, disallowed_path)) = self.disallowed.get(&id) { + span_lint_and_then( + cx, + DISALLOWED_FIELDS, + span, + format!("use of a disallowed field `{path}`"), + disallowed_path.diag_amendment(span), + ); + } + } +} diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 5b39d8844797..2ca7fdabcc3d 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -104,6 +104,7 @@ mod default_union_representation; mod dereference; mod derivable_impls; mod derive; +mod disallowed_fields; mod disallowed_macros; mod disallowed_methods; mod disallowed_names; @@ -851,6 +852,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co Box::new(|_| Box::new(toplevel_ref_arg::ToplevelRefArg)), Box::new(|_| Box::new(volatile_composites::VolatileComposites)), Box::new(|_| Box::::default()), + Box::new(move |tcx| Box::new(disallowed_fields::DisallowedFields::new(tcx, conf))), Box::new(move |_| Box::new(manual_ilog2::ManualIlog2::new(conf))), // add late passes here, used by `cargo dev new_lint` ]; diff --git a/clippy_utils/src/paths.rs b/clippy_utils/src/paths.rs index 8aa663163caf..b8e3f6fc3a65 100644 --- a/clippy_utils/src/paths.rs +++ b/clippy_utils/src/paths.rs @@ -318,6 +318,13 @@ fn local_item_child_by_name(tcx: TyCtxt<'_>, local_id: LocalDefId, ns: PathNS, n .filter_by_name_unhygienic(name) .find(|assoc_item| ns.matches(Some(assoc_item.namespace()))) .map(|assoc_item| assoc_item.def_id), + ItemKind::Struct(_, _, rustc_hir::VariantData::Struct { fields, .. }) => fields.iter().find_map(|field| { + if field.ident.name == name { + Some(field.def_id.to_def_id()) + } else { + None + } + }), _ => None, } } @@ -336,6 +343,11 @@ fn non_local_item_child_by_name(tcx: TyCtxt<'_>, def_id: DefId, ns: PathNS, name .iter() .copied() .find(|assoc_def_id| tcx.item_name(*assoc_def_id) == name && ns.matches(tcx.def_kind(assoc_def_id).ns())), + DefKind::Struct => tcx + .associated_item_def_ids(def_id) + .iter() + .copied() + .find(|assoc_def_id| tcx.item_name(*assoc_def_id) == name), _ => None, } } diff --git a/tests/ui-toml/toml_disallowed_fields/clippy.toml b/tests/ui-toml/toml_disallowed_fields/clippy.toml new file mode 100644 index 000000000000..0dc494815a34 --- /dev/null +++ b/tests/ui-toml/toml_disallowed_fields/clippy.toml @@ -0,0 +1,12 @@ +disallowed-fields = [ + # just a string is shorthand for path only + "std::ops::Range::start", + # can give path and reason with an inline table + { path = "std::ops::Range::end", reason = "no end allowed" }, + # can use an inline table but omit reason + { path = "std::ops::RangeTo::end" }, + # local paths + "conf_disallowed_fields::X::y", + # re-exports + "conf_disallowed_fields::Y::y", +] diff --git a/tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs b/tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs new file mode 100644 index 000000000000..df371c689e23 --- /dev/null +++ b/tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs @@ -0,0 +1,29 @@ +#![warn(clippy::disallowed_fields)] + +use std::ops::{Range, RangeTo}; + +struct X { + y: u32, +} + +use crate::X as Y; + +fn main() { + let x = X { y: 0 }; + let _ = x.y; + //~^ disallowed_fields + + let x = Y { y: 0 }; + let _ = x.y; + //~^ disallowed_fields + + let x = Range { start: 0, end: 0 }; + let _ = x.start; + //~^ disallowed_fields + let _ = x.end; + //~^ disallowed_fields + + let x = RangeTo { end: 0 }; + let _ = x.end; + //~^ disallowed_fields +} diff --git a/tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.stderr b/tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.stderr new file mode 100644 index 000000000000..bfe7450b946f --- /dev/null +++ b/tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.stderr @@ -0,0 +1,37 @@ +error: use of a disallowed field `conf_disallowed_fields::Y::y` + --> tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs:13:15 + | +LL | let _ = x.y; + | ^ + | + = note: `-D clippy::disallowed-fields` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::disallowed_fields)]` + +error: use of a disallowed field `conf_disallowed_fields::Y::y` + --> tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs:17:15 + | +LL | let _ = x.y; + | ^ + +error: use of a disallowed field `std::ops::Range::start` + --> tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs:21:15 + | +LL | let _ = x.start; + | ^^^^^ + +error: use of a disallowed field `std::ops::Range::end` + --> tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs:23:15 + | +LL | let _ = x.end; + | ^^^ + | + = note: no end allowed + +error: use of a disallowed field `std::ops::RangeTo::end` + --> tests/ui-toml/toml_disallowed_fields/conf_disallowed_fields.rs:27:15 + | +LL | let _ = x.end; + | ^^^ + +error: aborting due to 5 previous errors + diff --git a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr index e208bd510657..eea1f4f00e11 100644 --- a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr +++ b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr @@ -37,6 +37,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect check-private-items cognitive-complexity-threshold const-literal-digits-threshold + disallowed-fields disallowed-macros disallowed-methods disallowed-names @@ -136,6 +137,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect check-private-items cognitive-complexity-threshold const-literal-digits-threshold + disallowed-fields disallowed-macros disallowed-methods disallowed-names @@ -235,6 +237,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni check-private-items cognitive-complexity-threshold const-literal-digits-threshold + disallowed-fields disallowed-macros disallowed-methods disallowed-names