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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7734,6 +7734,7 @@ Released 2018-09-13
[`module-items-ordered-within-groupings`]: https://doc.rust-lang.org/clippy/lint_configuration.html#module-items-ordered-within-groupings
[`msrv`]: https://doc.rust-lang.org/clippy/lint_configuration.html#msrv
[`pass-by-value-size-limit`]: https://doc.rust-lang.org/clippy/lint_configuration.html#pass-by-value-size-limit
[`profiles`]: https://doc.rust-lang.org/clippy/lint_configuration.html#profiles
[`pub-underscore-fields-behavior`]: https://doc.rust-lang.org/clippy/lint_configuration.html#pub-underscore-fields-behavior
[`recursive-self-in-type-definitions`]: https://doc.rust-lang.org/clippy/lint_configuration.html#recursive-self-in-type-definitions
[`semicolon-inside-block-ignore-singleline`]: https://doc.rust-lang.org/clippy/lint_configuration.html#semicolon-inside-block-ignore-singleline
Expand Down
22 changes: 22 additions & 0 deletions book/src/lint_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,28 @@ The minimum size (in bytes) to consider a type for passing by reference instead
* [`large_types_passed_by_value`](https://rust-lang.github.io/rust-clippy/master/index.html#large_types_passed_by_value)


## `profiles`
Named profiles of disallowed items (unrelated to Cargo build profiles).

#### Example

```toml
[profiles.persistent]
disallowed-methods = [{ path = "std::env::temp_dir" }]
disallowed-types = [{ path = "std::time::Instant", reason = "use our custom time API" }]

[profiles.single_threaded]
disallowed-methods = [{ path = "std::thread::spawn" }]
```

**Default Value:** `{}`

---
**Affected lints:**
* [`disallowed_methods`](https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods)
* [`disallowed_types`](https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types)


## `pub-underscore-fields-behavior`
Lint "public" fields in a struct that are prefixed with an underscore based on their
exported visibility, or whether they are marked as "pub".
Expand Down
24 changes: 20 additions & 4 deletions clippy_config/src/conf.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use crate::ConfMetadata;
use crate::de::{DeserializeOrDefault, DiagCtxt, FromDefault, create_value_list_msg, find_closest_match};
use crate::types::{
DisallowedPath, DisallowedPathWithoutReplacement, InherentImplLintScope, MacroMatcher, MatchLintBehaviour,
PubUnderscoreFieldsBehaviour, Rename, SourceItemOrdering, SourceItemOrderingModuleItemGroupings,
SourceItemOrderingTraitAssocItemKinds, SourceItemOrderingWithinModuleItemGroupings, TraitImplItemOrder,
DisallowedPath, DisallowedPathWithoutReplacement, DisallowedProfile, InherentImplLintScope, MacroMatcher,
MatchLintBehaviour, PubUnderscoreFieldsBehaviour, Rename, SourceItemOrdering,
SourceItemOrderingModuleItemGroupings, SourceItemOrderingTraitAssocItemKinds,
SourceItemOrderingWithinModuleItemGroupings, TraitImplItemOrder,
};
use rustc_attr_parsing::parse_version;
use rustc_data_structures::fx::FxHashSet;
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_errors::Applicability;
use rustc_hir::attrs::RustcVersion;
use rustc_session::Session;
Expand Down Expand Up @@ -729,6 +730,21 @@ define_Conf! {
/// The minimum size (in bytes) to consider a type for passing by reference instead of by value.
#[lints(large_types_passed_by_value)]
pass_by_value_size_limit("pass-by-value-size-limit"): u64 = 256,
/// Named profiles of disallowed items (unrelated to Cargo build profiles).
///
/// #### Example
///
/// ```toml
/// [profiles.persistent]
/// disallowed-methods = [{ path = "std::env::temp_dir" }]
/// disallowed-types = [{ path = "std::time::Instant", reason = "use our custom time API" }]
///
/// [profiles.single_threaded]
/// disallowed-methods = [{ path = "std::thread::spawn" }]
/// ```
#[default_text = "{}"]
#[lints(disallowed_methods, disallowed_types)]
profiles("profiles"): FxHashMap<String, DisallowedProfile>,
/// Lint "public" fields in a struct that are prefixed with an underscore based on their
/// exported visibility, or whether they are marked as "pub".
#[lints(pub_underscore_fields)]
Expand Down
9 changes: 9 additions & 0 deletions clippy_config/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,15 @@ impl<T, S: Default> FromDefault<()> for HashSet<T, S> {
}
}

impl<K, V, S: Default> FromDefault<()> for HashMap<K, V, S> {
fn from_default((): ()) -> Self {
HashMap::default()
}
fn display_default((): ()) -> impl Display {
"{}"
}
}

struct DisplaySlice<T: 'static, U>(&'static [T], PhantomData<U>);
impl<T, U> Display for DisplaySlice<T, U>
where
Expand Down
43 changes: 43 additions & 0 deletions clippy_config/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use rustc_middle::ty::TyCtxt;
use rustc_session::Session;
use rustc_span::{Span, Spanned, Symbol};
use std::collections::HashMap;
use std::hash::BuildHasher;

macro_rules! concat_expr {
($($e:expr)*) => {
Expand Down Expand Up @@ -228,6 +229,48 @@ impl Deserialize for DisallowedPath<true> {
}
}

/// A named group of disallowed items, selected per item with
/// `#[clippy::disallowed_profile(s)]`.
#[derive(Default)]
pub struct DisallowedProfile {
pub disallowed_methods: Vec<DisallowedPath>,
pub disallowed_types: Vec<DisallowedPath>,
}

impl Deserialize for DisallowedProfile {
fn deserialize(dcx: &DiagCtxt<'_>, value: &TomlValue<'_>) -> Option<Self> {
let Some(table) = value.as_ref().as_table() else {
dcx.span_err(value.span(), "expected a table");
return None;
};
deserialize_table!(dcx, table,
disallowed_methods("disallowed-methods"): Vec<DisallowedPath>,
disallowed_types("disallowed-types"): Vec<DisallowedPath>,
);
Some(DisallowedProfile {
disallowed_methods: disallowed_methods.unwrap_or_default(),
disallowed_types: disallowed_types.unwrap_or_default(),
})
}
}

impl<S: Default + BuildHasher> Deserialize for HashMap<String, DisallowedProfile, S> {
fn deserialize(dcx: &DiagCtxt<'_>, value: &TomlValue<'_>) -> Option<Self> {
let Some(table) = value.as_ref().as_table() else {
dcx.span_err(value.span(), "expected a table of named profiles");
return None;
};
Some(
table
.iter()
.filter_map(|(name, profile)| {
DisallowedProfile::deserialize(dcx, profile).map(|p| (name.get_ref().to_string(), p))
})
.collect(),
)
}
}

/// Creates a map of disallowed items to the reason they were disallowed.
#[expect(clippy::type_complexity)]
pub fn create_disallowed_map<const REPLACEMENT_ALLOWED: bool>(
Expand Down
129 changes: 122 additions & 7 deletions clippy_lints/src/disallowed_methods.rs
Original file line number Diff line number Diff line change
@@ -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::diagnostics::{span_lint, 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_data_structures::smallvec::SmallVec;
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};

declare_clippy_lint! {
/// ### What it does
Expand Down Expand Up @@ -55,6 +60,19 @@ 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.
/// ```
///
/// Disallowed profiles allow scoping different disallow lists:
/// ```toml
/// [profiles.forward_pass]
/// disallowed-methods = [{ 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,
Expand All @@ -64,12 +82,22 @@ declare_clippy_lint! {
impl_lint_pass!(DisallowedMethods => [DISALLOWED_METHODS]);

pub struct DisallowedMethods {
disallowed: DefIdMap<(&'static str, &'static DisallowedPath)>,
default: DefIdMap<(&'static str, &'static DisallowedPath)>,
/// Lookup per profile that declares a non-empty `disallowed_methods` list. Profiles
/// declared in `[profiles.*]` but without `disallowed_methods` entries are absent here.
profiles: FxHashMap<Symbol, DefIdMap<(&'static str, &'static DisallowedPath)>>,
/// Every profile name declared in `[profiles.*]`, regardless of whether it contributes
/// to this lint. Used to suppress the "unknown profile" warning for profiles that exist
/// in config but only define entries for other lints (e.g. `disallowed_types`).
known_profiles: FxHashSet<Symbol>,
profile_cache: ProfileResolver,
warned_unknown_profiles: FxHashSet<Span>,
}

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,
Expand All @@ -82,13 +110,70 @@ impl DisallowedMethods {
"function",
false,
);
Self { disallowed }

let mut profiles = FxHashMap::default();
let mut known_profiles = FxHashSet::default();
let mut profile_entries: Vec<_> = conf.profiles.iter().collect();
profile_entries.sort_by_key(|(a, _)| *a);
for (name, profile) in profile_entries {
let symbol = Symbol::intern(name.as_str());
known_profiles.insert(symbol);

let paths = profile.disallowed_methods.as_slice();
if paths.is_empty() {
continue;
}

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);
}

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"
};
span_lint(
cx,
DISALLOWED_METHODS,
entry.span,
format!(
"`{attr_name}` references unknown profile `{}` for `clippy::disallowed_methods`",
entry.name
),
);
}
}
}

impl<'tcx> LateLintPass<'tcx> for DisallowedMethods {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if self.disallowed.is_empty() {
// Bail when the lint is entirely unconfigured. `known_profiles` is part of the check
// because unknown-profile diagnostics are emitted below even when no method list applies.
if self.default.is_empty() && self.profiles.is_empty() && self.known_profiles.is_empty() {
return;
}
if expr.span.desugaring_kind().is_some() {
Expand All @@ -101,13 +186,43 @@ 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();
// Copy entries out of the cache before iterating: `warn_unknown_profile` takes
// `&mut self`, which conflicts with the borrow held by `active_profiles(...)`.
let entries: SmallVec<[ProfileEntry; 2]> = self
.profile_cache
.active_profiles(cx, expr.hir_id)
.map(|selection| selection.iter().copied().collect())
.unwrap_or_default();
for entry in &entries {
if self.profiles.contains_key(&entry.name) {
active_profiles.push(entry.name);
} else if !self.known_profiles.contains(&entry.name) {
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)))
}) {
let diag_amendment = disallowed_path.diag_amendment(span);
span_lint_and_then(
cx,
DISALLOWED_METHODS,
span,
format!("use of a disallowed method `{path}` (profile: {profile})"),
|diag| diag_amendment(diag),
);
} else if let Some(&(path, disallowed_path)) = self.default.get(&id) {
let diag_amendment = disallowed_path.diag_amendment(span);
span_lint_and_then(
cx,
DISALLOWED_METHODS,
span,
format!("use of a disallowed method `{path}`"),
disallowed_path.diag_amendment(span),
|diag| diag_amendment(diag),
);
}
}
Expand Down
Loading
Loading