diff --git a/CHANGELOG.md b/CHANGELOG.md index 30781d3d33fb..fffbc4c9a182 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6863,8 +6863,10 @@ Released 2018-09-13 [`const-literal-digits-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#const-literal-digits-threshold [`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-methods-profiles`]: https://doc.rust-lang.org/clippy/lint_configuration.html#disallowed-methods-profiles [`disallowed-names`]: https://doc.rust-lang.org/clippy/lint_configuration.html#disallowed-names [`disallowed-types`]: https://doc.rust-lang.org/clippy/lint_configuration.html#disallowed-types +[`disallowed-types-profiles`]: https://doc.rust-lang.org/clippy/lint_configuration.html#disallowed-types-profiles [`doc-valid-idents`]: https://doc.rust-lang.org/clippy/lint_configuration.html#doc-valid-idents [`enable-raw-pointer-heuristic-for-send`]: https://doc.rust-lang.org/clippy/lint_configuration.html#enable-raw-pointer-heuristic-for-send [`enforce-iter-loop-reborrow`]: https://doc.rust-lang.org/clippy/lint_configuration.html#enforce-iter-loop-reborrow diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md index 23d231069b74..7d1220a50f71 100644 --- a/book/src/lint_configuration.md +++ b/book/src/lint_configuration.md @@ -529,6 +529,30 @@ The list of disallowed methods, written as fully qualified paths. * [`disallowed_methods`](https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods) +## `disallowed-methods-profiles` +Named profiles of disallowed methods, keyed by profile name. + +#### Example + +```toml +[disallowed-methods-profiles.forward_pass] +paths = [ + { path = "crate::io::DeviceBuffer::copy_to_host", reason = "Forward code stays on the device" } +] + +[disallowed-methods-profiles.export] +paths = [ + { path = "crate::io::DeviceBuffer::into_host_slice" } +] +``` + +**Default Value:** `{}` + +--- +**Affected lints:** +* [`disallowed_methods`](https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods) + + ## `disallowed-names` The list of disallowed names to lint about. NB: `bar` is not here since it has legitimate uses. The value `".."` can be used as part of the list to indicate that the configured values should be appended to the @@ -558,6 +582,25 @@ The list of disallowed types, written as fully qualified paths. * [`disallowed_types`](https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types) +## `disallowed-types-profiles` +Named profiles of disallowed types, keyed by profile name. + +#### Example + +```toml +[disallowed-types-profiles.forward_pass] +paths = [ + { path = "crate::io::HostBuffer", reason = "Prefer device buffers" } +] +``` + +**Default Value:** `{}` + +--- +**Affected lints:** +* [`disallowed_types`](https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types) + + ## `doc-valid-idents` The list of words this lint should not consider as identifiers needing ticks. The value `".."` can be used as part of the list to indicate, that the configured values should be appended to the diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs index 6849f58f96ff..32d4c9e4b014 100644 --- a/clippy_config/src/conf.rs +++ b/clippy_config/src/conf.rs @@ -7,6 +7,7 @@ use crate::types::{ }; use clippy_utils::msrvs::Msrv; use itertools::Itertools; +use rustc_data_structures::fx::FxHashMap; use rustc_errors::Applicability; use rustc_session::Session; use rustc_span::edit_distance::edit_distance; @@ -221,12 +222,71 @@ macro_rules! deserialize { }}; } +macro_rules! parse_conf_value { + ( + $map:expr, + $ty:ty, + $errors:expr, + $file:expr, + $field_span:expr, + profile @[$($profile:expr)?], + disallowed @[$($disallowed:expr)?] + ) => { + parse_conf_value_impl!( + $map, + $ty, + $errors, + $file, + $field_span, + ($($profile)?), + ($($disallowed)?) + ) + }; +} + +macro_rules! parse_conf_value_impl { + ($map:expr, $ty:ty, $errors:expr, $file:expr, $field_span:expr, (), ()) => { + deserialize!($map, $ty, $errors, $file) + }; + ($map:expr, $ty:ty, $errors:expr, $file:expr, $field_span:expr, ($profile:expr), ()) => { + deserialize_profiles!($map, $errors, $file, $field_span, $profile) + }; + ($map:expr, $ty:ty, $errors:expr, $file:expr, $field_span:expr, (), ($disallowed:expr)) => { + deserialize!($map, $ty, $errors, $file, $disallowed) + }; + ($map:expr, $ty:ty, $errors:expr, $file:expr, $field_span:expr, ($profile:expr), ($disallowed:expr)) => { + compile_error!("field cannot specify both disallowed profile table and disallowed path attributes") + }; +} + +macro_rules! deserialize_profiles { + ($map:expr, $errors:expr, $file:expr, $field_span:expr, $replacements_allowed:expr) => {{ + let raw_value = $map.next_value::()?; + let value_span = $field_span.clone(); + let toml::Value::Table(table) = raw_value else { + $errors.push(ConfError::spanned( + $file, + "expected table with named profiles", + None, + value_span.clone(), + )); + continue; + }; + + let map = + parse_disallowed_profiles::<{ $replacements_allowed }>(table, $file, value_span.clone(), &mut $errors); + + (map, value_span) + }}; +} + macro_rules! define_Conf { ($( $(#[doc = $doc:literal])+ $(#[conf_deprecated($dep:literal, $new_conf:ident)])? $(#[default_text = $default_text:expr])? $(#[disallowed_paths_allow_replacements = $replacements_allowed:expr])? + $(#[disallowed_paths_profile(replacements_allowed = $profile_replacements_allowed:expr)])? $(#[lints($($for_lints:ident),* $(,)?)])? $name:ident: $ty:ty = $default:expr, )*) => { @@ -281,10 +341,18 @@ macro_rules! define_Conf { match field { $(Field::$name => { + let _field_span = name.span(); // Is this a deprecated field, i.e., is `$dep` set? If so, push a warning. $(warnings.push(ConfError::spanned(self.0, format!("deprecated field `{}`. {}", name.get_ref(), $dep), None, name.span()));)? - let (value, value_span) = - deserialize!(map, $ty, errors, self.0 $(, $replacements_allowed)?); + let (value, value_span) = parse_conf_value!( + map, + $ty, + errors, + self.0, + _field_span, + profile @[$($profile_replacements_allowed)?], + disallowed @[$($replacements_allowed)?] + ); // Was this field set previously? if $name.is_some() { errors.push(ConfError::spanned(self.0, format!("duplicate field `{}`", name.get_ref()), None, name.span())); @@ -341,6 +409,81 @@ fn span_from_toml_range(file: &SourceFile, span: Range) -> Span { ) } +fn parse_disallowed_profiles( + table: toml::value::Table, + file: &SourceFile, + value_span: Range, + errors: &mut Vec, +) -> FxHashMap>> { + let mut profiles = FxHashMap::default(); + let config_span = span_from_toml_range(file, value_span.clone()); + + for (profile_name, profile_value) in table { + let toml::Value::Table(mut profile_table) = profile_value else { + errors.push(ConfError::spanned( + file, + format!("invalid profile `{profile_name}`: expected table with `paths` entry"), + None, + value_span.clone(), + )); + continue; + }; + + let Some(paths_value) = profile_table.remove("paths") else { + errors.push(ConfError::spanned( + file, + format!("profile `{profile_name}` missing `paths` entry"), + None, + value_span.clone(), + )); + continue; + }; + + if !profile_table.is_empty() { + let keys = profile_table.keys().map(String::as_str).collect::>().join(", "); + errors.push(ConfError::spanned( + file, + format!("profile `{profile_name}` has unknown keys: {keys}"), + None, + value_span.clone(), + )); + } + + let toml::Value::Array(entries) = paths_value else { + errors.push(ConfError::spanned( + file, + format!("profile `{profile_name}`: `paths` must be an array"), + None, + value_span.clone(), + )); + continue; + }; + + let mut disallowed = Vec::with_capacity(entries.len()); + for entry in entries { + match DisallowedPath::::deserialize(entry.clone()) { + Ok(mut path) => { + path.set_span(config_span); + disallowed.push(path); + }, + Err(err) => errors.push(ConfError::spanned( + file, + format!( + "profile `{profile_name}`: {}", + err.to_string().replace('\n', " ").trim() + ), + None, + value_span.clone(), + )), + } + } + + profiles.insert(profile_name, disallowed); + } + + profiles +} + define_Conf! { /// Which crates to allow absolute paths from #[lints(absolute_paths)] @@ -600,6 +743,24 @@ define_Conf! { #[disallowed_paths_allow_replacements = true] #[lints(disallowed_methods)] disallowed_methods: Vec = Vec::new(), + /// Named profiles of disallowed methods, keyed by profile name. + /// + /// #### Example + /// + /// ```toml + /// [disallowed-methods-profiles.forward_pass] + /// paths = [ + /// { path = "crate::io::DeviceBuffer::copy_to_host", reason = "Forward code stays on the device" } + /// ] + /// + /// [disallowed-methods-profiles.export] + /// paths = [ + /// { path = "crate::io::DeviceBuffer::into_host_slice" } + /// ] + /// ``` + #[disallowed_paths_profile(replacements_allowed = true)] + #[lints(disallowed_methods)] + disallowed_methods_profiles: FxHashMap> = FxHashMap::default(), /// The list of disallowed names to lint about. NB: `bar` is not here since it has legitimate uses. The value /// `".."` can be used as part of the list to indicate that the configured values should be appended to the /// default configuration of Clippy. By default, any configuration will replace the default value. @@ -616,6 +777,19 @@ define_Conf! { #[disallowed_paths_allow_replacements = true] #[lints(disallowed_types)] disallowed_types: Vec = Vec::new(), + /// Named profiles of disallowed types, keyed by profile name. + /// + /// #### Example + /// + /// ```toml + /// [disallowed-types-profiles.forward_pass] + /// paths = [ + /// { path = "crate::io::HostBuffer", reason = "Prefer device buffers" } + /// ] + /// ``` + #[disallowed_paths_profile(replacements_allowed = true)] + #[lints(disallowed_types)] + disallowed_types_profiles: FxHashMap> = FxHashMap::default(), /// The list of words this lint should not consider as identifiers needing ticks. The value /// `".."` can be used as part of the list to indicate, that the configured values should be appended to the /// default configuration of Clippy. By default, any configuration will replace the default value. For example: diff --git a/clippy_lints/src/disallowed_methods.rs b/clippy_lints/src/disallowed_methods.rs index 8c067432cb4e..3ac2b06d41b8 100644 --- a/clippy_lints/src/disallowed_methods.rs +++ b/clippy_lints/src/disallowed_methods.rs @@ -1,13 +1,18 @@ use clippy_config::Conf; use clippy_config::types::{DisallowedPath, create_disallowed_map}; use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::disallowed_profiles::{ProfileEntry, ProfileResolver}; use clippy_utils::paths::PathNS; +use clippy_utils::sym; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_hir::def::{CtorKind, DefKind, Res}; use rustc_hir::def_id::DefIdMap; use rustc_hir::{Expr, ExprKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::TyCtxt; use rustc_session::impl_lint_pass; +use rustc_span::{Span, Symbol}; +use smallvec::SmallVec; declare_clippy_lint! { /// ### What it does @@ -55,6 +60,21 @@ declare_clippy_lint! { /// let mut xs = Vec::new(); // Vec::new is _not_ disallowed in the config. /// xs.push(123); // Vec::push is _not_ disallowed in the config. /// ``` + /// + /// Profiles allow scoping different disallow lists: + /// ```toml + /// [disallowed-methods-profiles.forward_pass] + /// paths = [ + /// { path = "crate::devices::Buffer::copy_to_host", reason = "Forward code must not touch host buffers" } + /// ] + /// ``` + /// + /// ```rust,ignore + /// #[clippy::disallowed_profile("forward_pass")] + /// fn evaluate() { + /// // Method calls in this function use the `forward_pass` profile. + /// } + /// ``` #[clippy::version = "1.49.0"] pub DISALLOWED_METHODS, style, @@ -62,12 +82,17 @@ declare_clippy_lint! { } pub struct DisallowedMethods { - disallowed: DefIdMap<(&'static str, &'static DisallowedPath)>, + default: DefIdMap<(&'static str, &'static DisallowedPath)>, + profiles: FxHashMap>, + known_profiles: FxHashSet, + profile_cache: ProfileResolver, + warned_unknown_profiles: FxHashSet, } impl DisallowedMethods { + #[allow(rustc::potential_query_instability)] // Profiles are sorted for deterministic iteration. pub fn new(tcx: TyCtxt<'_>, conf: &'static Conf) -> Self { - let (disallowed, _) = create_disallowed_map( + let (default, _) = create_disallowed_map( tcx, &conf.disallowed_methods, PathNS::Value, @@ -80,7 +105,69 @@ impl DisallowedMethods { "function", false, ); - Self { disallowed } + + let mut profiles = FxHashMap::default(); + let mut names: Vec<_> = conf.disallowed_methods_profiles.keys().collect(); + names.sort(); + for name in names { + let symbol = Symbol::intern(name.as_str()); + let paths = conf + .disallowed_methods_profiles + .get(name) + .expect("profile entry must exist"); + let (map, _) = create_disallowed_map( + tcx, + paths, + PathNS::Value, + |def_kind| { + matches!( + def_kind, + DefKind::Fn | DefKind::Ctor(_, CtorKind::Fn) | DefKind::AssocFn + ) + }, + "function", + false, + ); + profiles.insert(symbol, map); + } + + let mut known_profiles = FxHashSet::default(); + for name in conf + .disallowed_methods_profiles + .keys() + .chain(conf.disallowed_types_profiles.keys()) + { + known_profiles.insert(Symbol::intern(name.as_str())); + } + + Self { + default, + profiles, + known_profiles, + profile_cache: ProfileResolver::default(), + warned_unknown_profiles: FxHashSet::default(), + } + } + + fn warn_unknown_profile(&mut self, cx: &LateContext<'_>, entry: &ProfileEntry) { + if self.warned_unknown_profiles.insert(entry.span) { + let attr_name = if entry.attr_name == sym::disallowed_profiles { + "clippy::disallowed_profiles" + } else { + "clippy::disallowed_profile" + }; + cx.tcx + .sess + .dcx() + .struct_span_warn( + entry.span, + format!( + "`{attr_name}` references unknown profile `{}` for `clippy::disallowed_methods`", + entry.name + ), + ) + .emit(); + } } } @@ -95,7 +182,36 @@ impl<'tcx> LateLintPass<'tcx> for DisallowedMethods { }, _ => return, }; - if let Some(&(path, disallowed_path)) = self.disallowed.get(&id) { + let mut active_profiles = SmallVec::<[Symbol; 2]>::new(); + let mut unknown_profiles = SmallVec::<[ProfileEntry; 2]>::new(); + if let Some(selection) = self.profile_cache.active_profiles(cx, expr.hir_id) { + for entry in selection.iter() { + let is_active = self.profiles.contains_key(&entry.name); + if is_active { + active_profiles.push(entry.name); + } else if !self.known_profiles.contains(&entry.name) { + unknown_profiles.push(entry.clone()); + } + } + } + + for entry in unknown_profiles { + self.warn_unknown_profile(cx, &entry); + } + + if let Some((profile, &(path, disallowed_path))) = active_profiles.iter().find_map(|symbol| { + self.profiles + .get(symbol) + .and_then(|map| map.get(&id).map(|info| (*symbol, info))) + }) { + span_lint_and_then( + cx, + DISALLOWED_METHODS, + span, + format!("use of a disallowed method `{path}` (profile: {profile})"), + disallowed_path.diag_amendment(span), + ); + } else if let Some(&(path, disallowed_path)) = self.default.get(&id) { span_lint_and_then( cx, DISALLOWED_METHODS, diff --git a/clippy_lints/src/disallowed_types.rs b/clippy_lints/src/disallowed_types.rs index 9a82327a0d7b..4c49b8b9980f 100644 --- a/clippy_lints/src/disallowed_types.rs +++ b/clippy_lints/src/disallowed_types.rs @@ -1,15 +1,18 @@ use clippy_config::Conf; use clippy_config::types::{DisallowedPath, create_disallowed_map}; use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::disallowed_profiles::{ProfileEntry, ProfileResolver}; use clippy_utils::paths::PathNS; -use rustc_data_structures::fx::FxHashMap; +use clippy_utils::sym; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_hir::def::{DefKind, Res}; use rustc_hir::def_id::DefIdMap; use rustc_hir::{AmbigArg, Item, ItemKind, PolyTraitRef, PrimTy, Ty, TyKind, UseKind}; use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::ty::TyCtxt; use rustc_session::impl_lint_pass; -use rustc_span::Span; +use rustc_span::{Span, Symbol}; +use smallvec::SmallVec; declare_clippy_lint! { /// ### What it does @@ -51,43 +54,147 @@ declare_clippy_lint! { /// // A similar type that is allowed by the config /// use std::collections::HashMap; /// ``` + /// + /// Profiles can scope lists to specific modules: + /// ```toml + /// [disallowed-types-profiles.forward_pass] + /// paths = [ + /// { path = "crate::buffers::HostBuffer", reason = "Prefer device buffers in forward computations" } + /// ] + /// ``` + /// + /// ```rust,ignore + /// #[clippy::disallowed_profile("forward_pass")] + /// fn forward_step(buffer: crate::buffers::DeviceBuffer) { /* ... */ } + /// ``` #[clippy::version = "1.55.0"] pub DISALLOWED_TYPES, style, "use of disallowed types" } -pub struct DisallowedTypes { +struct TypeLookup { def_ids: DefIdMap<(&'static str, &'static DisallowedPath)>, prim_tys: FxHashMap, } +impl TypeLookup { + fn from_config(tcx: TyCtxt<'_>, paths: &'static [DisallowedPath]) -> Self { + let (def_ids, prim_tys) = create_disallowed_map(tcx, paths, PathNS::Type, def_kind_predicate, "type", true); + Self { def_ids, prim_tys } + } + + fn find(&self, res: &Res) -> Option<(&'static str, &'static DisallowedPath)> { + match res { + Res::Def(_, did) => self.def_ids.get(did).copied(), + Res::PrimTy(prim) => self.prim_tys.get(prim).copied(), + _ => None, + } + } +} + +pub struct DisallowedTypes { + default: TypeLookup, + profiles: FxHashMap, + known_profiles: FxHashSet, + profile_cache: ProfileResolver, + warned_unknown_profiles: FxHashSet, +} + impl DisallowedTypes { + #[allow(rustc::potential_query_instability)] // Profiles are sorted for deterministic iteration. pub fn new(tcx: TyCtxt<'_>, conf: &'static Conf) -> Self { - let (def_ids, prim_tys) = create_disallowed_map( - tcx, - &conf.disallowed_types, - PathNS::Type, - def_kind_predicate, - "type", - true, - ); - Self { def_ids, prim_tys } + let default = TypeLookup::from_config(tcx, &conf.disallowed_types); + + let mut profiles = FxHashMap::default(); + let mut names: Vec<_> = conf.disallowed_types_profiles.keys().collect(); + names.sort(); + for name in names { + let symbol = Symbol::intern(name.as_str()); + let paths = conf + .disallowed_types_profiles + .get(name) + .expect("profile entry must exist"); + profiles.insert(symbol, TypeLookup::from_config(tcx, paths)); + } + + let mut known_profiles = FxHashSet::default(); + for name in conf + .disallowed_types_profiles + .keys() + .chain(conf.disallowed_methods_profiles.keys()) + { + known_profiles.insert(Symbol::intern(name.as_str())); + } + + Self { + default, + profiles, + known_profiles, + profile_cache: ProfileResolver::default(), + warned_unknown_profiles: FxHashSet::default(), + } } - fn check_res_emit(&self, cx: &LateContext<'_>, res: &Res, span: Span) { - let (path, disallowed_path) = match res { - Res::Def(_, did) if let Some(&x) = self.def_ids.get(did) => x, - Res::PrimTy(prim) if let Some(&x) = self.prim_tys.get(prim) => x, - _ => return, - }; - span_lint_and_then( - cx, - DISALLOWED_TYPES, - span, - format!("use of a disallowed type `{path}`"), - disallowed_path.diag_amendment(span), - ); + fn warn_unknown_profile(&mut self, cx: &LateContext<'_>, entry: &ProfileEntry) { + if self.warned_unknown_profiles.insert(entry.span) { + let attr_name = if entry.attr_name == sym::disallowed_profiles { + "clippy::disallowed_profiles" + } else { + "clippy::disallowed_profile" + }; + cx.tcx + .sess + .dcx() + .struct_span_warn( + entry.span, + format!( + "`{attr_name}` references unknown profile `{}` for `clippy::disallowed_types`", + entry.name + ), + ) + .emit(); + } + } + + fn check_res_emit(&mut self, cx: &LateContext<'_>, hir_id: rustc_hir::HirId, res: &Res, span: Span) { + let mut active_profiles = SmallVec::<[Symbol; 2]>::new(); + let mut unknown_profiles = SmallVec::<[ProfileEntry; 2]>::new(); + if let Some(selection) = self.profile_cache.active_profiles(cx, hir_id) { + for entry in selection.iter() { + if self.profiles.contains_key(&entry.name) { + active_profiles.push(entry.name); + } else if !self.known_profiles.contains(&entry.name) { + unknown_profiles.push(entry.clone()); + } + } + } + + for entry in unknown_profiles { + self.warn_unknown_profile(cx, &entry); + } + + if let Some((profile, (path, disallowed_path))) = active_profiles.iter().find_map(|symbol| { + self.profiles + .get(symbol) + .and_then(|lookup| lookup.find(res).map(|info| (*symbol, info))) + }) { + span_lint_and_then( + cx, + DISALLOWED_TYPES, + span, + format!("use of a disallowed type `{path}` (profile: {profile})"), + disallowed_path.diag_amendment(span), + ); + } else if let Some((path, disallowed_path)) = self.default.find(res) { + span_lint_and_then( + cx, + DISALLOWED_TYPES, + span, + format!("use of a disallowed type `{path}`"), + disallowed_path.diag_amendment(span), + ); + } } } @@ -111,17 +218,22 @@ impl<'tcx> LateLintPass<'tcx> for DisallowedTypes { if let ItemKind::Use(path, UseKind::Single(_)) = &item.kind && let Some(res) = path.res.type_ns { - self.check_res_emit(cx, &res, item.span); + self.check_res_emit(cx, item.hir_id(), &res, item.span); } } fn check_ty(&mut self, cx: &LateContext<'tcx>, ty: &'tcx Ty<'tcx, AmbigArg>) { if let TyKind::Path(path) = &ty.kind { - self.check_res_emit(cx, &cx.qpath_res(path, ty.hir_id), ty.span); + self.check_res_emit(cx, ty.hir_id, &cx.qpath_res(path, ty.hir_id), ty.span); } } fn check_poly_trait_ref(&mut self, cx: &LateContext<'tcx>, poly: &'tcx PolyTraitRef<'tcx>) { - self.check_res_emit(cx, &poly.trait_ref.path.res, poly.trait_ref.path.span); + self.check_res_emit( + cx, + poly.trait_ref.hir_ref_id, + &poly.trait_ref.path.res, + poly.trait_ref.path.span, + ); } } diff --git a/clippy_utils/src/attrs.rs b/clippy_utils/src/attrs.rs index 2d42e76dcbc9..c5ffb37b1052 100644 --- a/clippy_utils/src/attrs.rs +++ b/clippy_utils/src/attrs.rs @@ -28,6 +28,8 @@ pub const BUILTIN_ATTRIBUTES: &[(Symbol, DeprecationStatus)] = &[ (sym::cognitive_complexity, DeprecationStatus::None), (sym::cyclomatic_complexity, DeprecationStatus::Replaced("cognitive_complexity")), (sym::dump, DeprecationStatus::None), + (sym::disallowed_profile, DeprecationStatus::None), + (sym::disallowed_profiles, DeprecationStatus::None), (sym::msrv, DeprecationStatus::None), // The following attributes are for the 3rd party crate authors. // See book/src/attribs.md diff --git a/clippy_utils/src/disallowed_profiles.rs b/clippy_utils/src/disallowed_profiles.rs new file mode 100644 index 000000000000..7958aab1423f --- /dev/null +++ b/clippy_utils/src/disallowed_profiles.rs @@ -0,0 +1,167 @@ +use crate::sym; +use rustc_ast::ast::{LitKind, MetaItemInner}; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::{Attribute, HirId}; +use rustc_lint::LateContext; +use rustc_span::{Span, Symbol}; +use smallvec::SmallVec; + +#[derive(Clone)] +pub struct ProfileEntry { + pub attr_name: Symbol, + pub name: Symbol, + pub span: Span, +} + +#[derive(Clone)] +pub struct ProfileSelection { + entries: SmallVec<[ProfileEntry; 2]>, +} + +impl ProfileSelection { + pub fn new(entries: SmallVec<[ProfileEntry; 2]>) -> Self { + Self { entries } + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn iter(&self) -> impl Iterator { + self.entries.iter() + } +} + +#[derive(Default)] +pub struct ProfileResolver { + cache: FxHashMap>, +} + +impl ProfileResolver { + pub fn active_profiles(&mut self, cx: &LateContext<'_>, hir_id: HirId) -> Option<&ProfileSelection> { + if self.cache.contains_key(&hir_id) { + return self.cache.get(&hir_id).and_then(|selection| selection.as_ref()); + } + + let (resolved, visited) = self.resolve(cx, hir_id); + + for id in visited { + self.cache.entry(id).or_insert_with(|| resolved.clone()); + } + self.cache.insert(hir_id, resolved); + + self.cache.get(&hir_id).and_then(|selection| selection.as_ref()) + } + + fn resolve(&self, cx: &LateContext<'_>, start: HirId) -> (Option, SmallVec<[HirId; 8]>) { + let mut visited = SmallVec::<[HirId; 8]>::new(); + let mut current = Some(start); + + while let Some(id) = current { + if let Some(cached) = self.cache.get(&id) { + return (cached.clone(), visited); + } + + visited.push(id); + + if let Some(selection) = profiles_from_attrs(cx, cx.tcx.hir_attrs(id)) { + return (Some(selection), visited); + } + + if id == rustc_hir::CRATE_HIR_ID { + current = None; + } else { + current = Some(cx.tcx.parent_hir_id(id)); + } + } + + (None, visited) + } +} + +fn profiles_from_attrs(cx: &LateContext<'_>, attrs: &[Attribute]) -> Option { + let mut entries = SmallVec::<[ProfileEntry; 2]>::new(); + + for attr in attrs { + let Some(path) = attr.ident_path() else { continue }; + if path.len() != 2 || path[0].name != sym::clippy { + continue; + } + + let name = path[1].name; + if name != sym::disallowed_profile && name != sym::disallowed_profiles { + continue; + } + + let attr_label = if name == sym::disallowed_profiles { + "`clippy::disallowed_profiles`" + } else { + "`clippy::disallowed_profile`" + }; + + let Some(items) = attr.meta_item_list() else { + cx.tcx + .sess + .dcx() + .struct_span_err(attr.span(), format!("{attr_label} expects string arguments")) + .emit(); + continue; + }; + + if items.is_empty() { + cx.tcx + .sess + .dcx() + .struct_span_err(attr.span(), format!("{attr_label} expects at least one profile name")) + .emit(); + continue; + } + + if name == sym::disallowed_profile && items.len() != 1 { + cx.tcx + .sess + .dcx() + .struct_span_err(attr.span(), "use `clippy::disallowed_profiles` for multiple profiles") + .emit(); + } + + for item in items { + match literal_symbol(&item) { + Some((symbol, span)) => entries.push(ProfileEntry { + attr_name: name, + name: symbol, + span, + }), + None => emit_string_error(cx, &item), + } + } + } + + if entries.is_empty() { + None + } else { + Some(ProfileSelection::new(entries)) + } +} + +fn literal_symbol(item: &MetaItemInner) -> Option<(Symbol, Span)> { + match item { + MetaItemInner::Lit(lit) => { + let LitKind::Str(symbol, _) = lit.kind else { return None }; + Some((symbol, lit.span)) + }, + MetaItemInner::MetaItem(_) => None, + } +} + +fn emit_string_error(cx: &LateContext<'_>, item: &MetaItemInner) { + let span = match item { + MetaItemInner::Lit(lit) => lit.span, + MetaItemInner::MetaItem(meta) => meta.span, + }; + cx.tcx + .sess + .dcx() + .struct_span_err(span, "expected string literal profile name") + .emit(); +} diff --git a/clippy_utils/src/lib.rs b/clippy_utils/src/lib.rs index 708491df7707..938f25c92011 100644 --- a/clippy_utils/src/lib.rs +++ b/clippy_utils/src/lib.rs @@ -55,6 +55,7 @@ mod check_proc_macro; pub mod comparisons; pub mod consts; pub mod diagnostics; +pub mod disallowed_profiles; pub mod eager_or_lazy; pub mod higher; mod hir_utils; diff --git a/clippy_utils/src/sym.rs b/clippy_utils/src/sym.rs index 4ba0e52572dd..d21ae5637c2d 100644 --- a/clippy_utils/src/sym.rs +++ b/clippy_utils/src/sym.rs @@ -129,6 +129,8 @@ generate! { de, deprecated_in_future, diagnostics, + disallowed_profile, + disallowed_profiles, disallowed_types, drain, dump, diff --git a/tests/ui-toml/disallowed_profiles_methods/clippy.toml b/tests/ui-toml/disallowed_profiles_methods/clippy.toml new file mode 100644 index 000000000000..52b36099e0ad --- /dev/null +++ b/tests/ui-toml/disallowed_profiles_methods/clippy.toml @@ -0,0 +1,13 @@ +disallowed-methods = [ + { path = "std::mem::drop" } +] + +[disallowed-methods-profiles.forward_pass] +paths = [ + { path = "alloc::vec::Vec::push", reason = "push is forbidden in forward profile" } +] + +[disallowed-methods-profiles.export] +paths = [ + { path = "core::option::Option::unwrap" } +] diff --git a/tests/ui-toml/disallowed_profiles_methods/main.rs b/tests/ui-toml/disallowed_profiles_methods/main.rs new file mode 100644 index 000000000000..b1c04a20ad59 --- /dev/null +++ b/tests/ui-toml/disallowed_profiles_methods/main.rs @@ -0,0 +1,51 @@ +#![warn(clippy::disallowed_methods)] +#![allow( + unused, + clippy::no_effect, + clippy::needless_borrow, + clippy::vec_init_then_push, + clippy::unnecessary_literal_unwrap +)] + +fn default_violation() { + let value = String::from("test"); + std::mem::drop(value); //~ ERROR: use of a disallowed method `std::mem::drop` +} + +#[clippy::disallowed_profile("forward_pass")] +fn forward_profile() { + let mut values = Vec::new(); + values.push(1); //~ ERROR: use of a disallowed method `alloc::vec::Vec::push` (profile: forward_pass) +} + +#[clippy::disallowed_profile("export")] +fn export_profile() { + let value = Some(1); + value.unwrap(); //~ ERROR: use of a disallowed method `core::option::Option::unwrap` (profile: export) +} + +#[clippy::disallowed_profile("unknown_profile")] +//~^ WARN: unknown profile `unknown_profile` for +//~| WARN: unknown profile `unknown_profile` for +fn unknown_profile() { + let mut values = Vec::new(); + values.push(1); + // unknown profile falls back to the default list + std::mem::drop(values); //~ ERROR: use of a disallowed method `std::mem::drop` +} + +#[clippy::disallowed_profiles("forward_pass", "export")] +fn merged_profiles() { + let mut values = Vec::new(); + values.push(1); //~ ERROR: use of a disallowed method `alloc::vec::Vec::push` (profile: forward_pass) + let value = Some(1); + value.unwrap(); //~ ERROR: use of a disallowed method `core::option::Option::unwrap` (profile: export) +} + +fn main() { + default_violation(); + forward_profile(); + export_profile(); + unknown_profile(); + merged_profiles(); +} diff --git a/tests/ui-toml/disallowed_profiles_methods/main.stderr b/tests/ui-toml/disallowed_profiles_methods/main.stderr new file mode 100644 index 000000000000..ee4f4af32069 --- /dev/null +++ b/tests/ui-toml/disallowed_profiles_methods/main.stderr @@ -0,0 +1,57 @@ +error: use of a disallowed method `std::mem::drop` + --> tests/ui-toml/disallowed_profiles_methods/main.rs:12:5 + | +LL | std::mem::drop(value); + | ^^^^^^^^^^^^^^ + | + = note: `-D clippy::disallowed-methods` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::disallowed_methods)]` + +error: use of a disallowed method `alloc::vec::Vec::push` (profile: forward_pass) + --> tests/ui-toml/disallowed_profiles_methods/main.rs:18:12 + | +LL | values.push(1); + | ^^^^ + | + = note: push is forbidden in forward profile + +error: use of a disallowed method `core::option::Option::unwrap` (profile: export) + --> tests/ui-toml/disallowed_profiles_methods/main.rs:24:11 + | +LL | value.unwrap(); + | ^^^^^^ + +warning: `clippy::disallowed_profile` references unknown profile `unknown_profile` for `clippy::disallowed_methods` + --> tests/ui-toml/disallowed_profiles_methods/main.rs:27:30 + | +LL | #[clippy::disallowed_profile("unknown_profile")] + | ^^^^^^^^^^^^^^^^^ + +warning: `clippy::disallowed_profile` references unknown profile `unknown_profile` for `clippy::disallowed_types` + --> tests/ui-toml/disallowed_profiles_methods/main.rs:27:30 + | +LL | #[clippy::disallowed_profile("unknown_profile")] + | ^^^^^^^^^^^^^^^^^ + +error: use of a disallowed method `std::mem::drop` + --> tests/ui-toml/disallowed_profiles_methods/main.rs:34:5 + | +LL | std::mem::drop(values); + | ^^^^^^^^^^^^^^ + +error: use of a disallowed method `alloc::vec::Vec::push` (profile: forward_pass) + --> tests/ui-toml/disallowed_profiles_methods/main.rs:40:12 + | +LL | values.push(1); + | ^^^^ + | + = note: push is forbidden in forward profile + +error: use of a disallowed method `core::option::Option::unwrap` (profile: export) + --> tests/ui-toml/disallowed_profiles_methods/main.rs:42:11 + | +LL | value.unwrap(); + | ^^^^^^ + +error: aborting due to 6 previous errors; 2 warnings emitted + diff --git a/tests/ui-toml/disallowed_profiles_types/clippy.toml b/tests/ui-toml/disallowed_profiles_types/clippy.toml new file mode 100644 index 000000000000..f43b04bd6b33 --- /dev/null +++ b/tests/ui-toml/disallowed_profiles_types/clippy.toml @@ -0,0 +1,13 @@ +disallowed-types = [ + { path = "std::rc::Rc" } +] + +[disallowed-types-profiles.forward_pass] +paths = [ + { path = "std::cell::RefCell", reason = "Prefer shared references" } +] + +[disallowed-types-profiles.export] +paths = [ + { path = "std::sync::Mutex" } +] diff --git a/tests/ui-toml/disallowed_profiles_types/main.rs b/tests/ui-toml/disallowed_profiles_types/main.rs new file mode 100644 index 000000000000..9598fe9f8029 --- /dev/null +++ b/tests/ui-toml/disallowed_profiles_types/main.rs @@ -0,0 +1,43 @@ +#![warn(clippy::disallowed_types)] +#![allow(dead_code)] + +use std::rc::Rc; //~ ERROR: use of a disallowed type `std::rc::Rc` +use std::sync::Mutex; + +struct Wrapper; + +fn default_type() { + let _value: Rc = todo!(); //~ ERROR: use of a disallowed type `std::rc::Rc` +} + +#[clippy::disallowed_profile("forward_pass")] +fn forward_profile() { + let _value: std::cell::RefCell = todo!(); //~ ERROR: use of a disallowed type `std::cell::RefCell` (profile: forward_pass) +} + +#[clippy::disallowed_profile("export")] +fn export_profile() { + let _value: Mutex = todo!(); //~ ERROR: use of a disallowed type `std::sync::Mutex` (profile: export) +} + +#[clippy::disallowed_profile("unknown_type_profile")] +//~^ WARN: unknown profile `unknown_type_profile` for +//~| WARN: unknown profile `unknown_type_profile` for +fn unknown_profile() { + let _other = 1u32; + let _fallback: Rc = todo!(); //~ ERROR: use of a disallowed type `std::rc::Rc` +} + +#[clippy::disallowed_profiles("forward_pass", "export")] +fn merged_profiles() { + let _value: std::cell::RefCell = todo!(); //~ ERROR: use of a disallowed type `std::cell::RefCell` (profile: forward_pass) + let _other: Mutex = todo!(); //~ ERROR: use of a disallowed type `std::sync::Mutex` (profile: export) +} + +fn main() { + default_type(); + forward_profile(); + export_profile(); + unknown_profile(); + merged_profiles(); +} diff --git a/tests/ui-toml/disallowed_profiles_types/main.stderr b/tests/ui-toml/disallowed_profiles_types/main.stderr new file mode 100644 index 000000000000..cee93ae8c260 --- /dev/null +++ b/tests/ui-toml/disallowed_profiles_types/main.stderr @@ -0,0 +1,63 @@ +error: use of a disallowed type `std::rc::Rc` + --> tests/ui-toml/disallowed_profiles_types/main.rs:4:1 + | +LL | use std::rc::Rc; + | ^^^^^^^^^^^^^^^^ + | + = note: `-D clippy::disallowed-types` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::disallowed_types)]` + +error: use of a disallowed type `std::rc::Rc` + --> tests/ui-toml/disallowed_profiles_types/main.rs:10:17 + | +LL | let _value: Rc = todo!(); + | ^^^^^^^ + +error: use of a disallowed type `std::cell::RefCell` (profile: forward_pass) + --> tests/ui-toml/disallowed_profiles_types/main.rs:15:17 + | +LL | let _value: std::cell::RefCell = todo!(); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: Prefer shared references + +error: use of a disallowed type `std::sync::Mutex` (profile: export) + --> tests/ui-toml/disallowed_profiles_types/main.rs:20:17 + | +LL | let _value: Mutex = todo!(); + | ^^^^^^^^^^ + +warning: `clippy::disallowed_profile` references unknown profile `unknown_type_profile` for `clippy::disallowed_methods` + --> tests/ui-toml/disallowed_profiles_types/main.rs:23:30 + | +LL | #[clippy::disallowed_profile("unknown_type_profile")] + | ^^^^^^^^^^^^^^^^^^^^^^ + +warning: `clippy::disallowed_profile` references unknown profile `unknown_type_profile` for `clippy::disallowed_types` + --> tests/ui-toml/disallowed_profiles_types/main.rs:23:30 + | +LL | #[clippy::disallowed_profile("unknown_type_profile")] + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: use of a disallowed type `std::rc::Rc` + --> tests/ui-toml/disallowed_profiles_types/main.rs:28:20 + | +LL | let _fallback: Rc = todo!(); + | ^^^^^^^ + +error: use of a disallowed type `std::cell::RefCell` (profile: forward_pass) + --> tests/ui-toml/disallowed_profiles_types/main.rs:33:17 + | +LL | let _value: std::cell::RefCell = todo!(); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: Prefer shared references + +error: use of a disallowed type `std::sync::Mutex` (profile: export) + --> tests/ui-toml/disallowed_profiles_types/main.rs:34:17 + | +LL | let _other: Mutex = todo!(); + | ^^^^^^^^^^ + +error: aborting due to 7 previous errors; 2 warnings emitted + 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 20aeb4bb8498..fa42187cb95f 100644 --- a/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr +++ b/tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr @@ -38,8 +38,10 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect const-literal-digits-threshold disallowed-macros disallowed-methods + disallowed-methods-profiles disallowed-names disallowed-types + disallowed-types-profiles doc-valid-idents enable-raw-pointer-heuristic-for-send enforce-iter-loop-reborrow @@ -133,8 +135,10 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect const-literal-digits-threshold disallowed-macros disallowed-methods + disallowed-methods-profiles disallowed-names disallowed-types + disallowed-types-profiles doc-valid-idents enable-raw-pointer-heuristic-for-send enforce-iter-loop-reborrow @@ -228,8 +232,10 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni const-literal-digits-threshold disallowed-macros disallowed-methods + disallowed-methods-profiles disallowed-names disallowed-types + disallowed-types-profiles doc-valid-idents enable-raw-pointer-heuristic-for-send enforce-iter-loop-reborrow