feat(multiple_inherent_impl): Add config option to target specific scope

This commit is contained in:
Paul MIALANE
2025-10-07 11:06:21 +02:00
parent 6cc8f8dbaa
commit 9be213666e
28 changed files with 463 additions and 19 deletions
+1
View File
@@ -6874,6 +6874,7 @@ Released 2018-09-13
[`excessive-nesting-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#excessive-nesting-threshold
[`future-size-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#future-size-threshold
[`ignore-interior-mutability`]: https://doc.rust-lang.org/clippy/lint_configuration.html#ignore-interior-mutability
[`inherent-impl-lint-scope`]: https://doc.rust-lang.org/clippy/lint_configuration.html#inherent-impl-lint-scope
[`large-error-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#large-error-threshold
[`lint-commented-code`]: https://doc.rust-lang.org/clippy/lint_configuration.html#lint-commented-code
[`literal-representation-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#literal-representation-threshold
+10
View File
@@ -671,6 +671,16 @@ A list of paths to types that should be treated as if they do not contain interi
* [`mutable_key_type`](https://rust-lang.github.io/rust-clippy/master/index.html#mutable_key_type)
## `inherent-impl-lint-scope`
Sets the scope ("crate", "file", or "module") in which duplicate inherent `impl` blocks for the same type are linted.
**Default Value:** `"crate"`
---
**Affected lints:**
* [`multiple_inherent_impl`](https://rust-lang.github.io/rust-clippy/master/index.html#multiple_inherent_impl)
## `large-error-threshold`
The maximum size of the `Err`-variant in a `Result` returned from a function
+7 -4
View File
@@ -1,9 +1,9 @@
use crate::ClippyConfiguration;
use crate::types::{
DisallowedPath, DisallowedPathWithoutReplacement, MacroMatcher, MatchLintBehaviour, PubUnderscoreFieldsBehaviour,
Rename, SourceItemOrdering, SourceItemOrderingCategory, SourceItemOrderingModuleItemGroupings,
SourceItemOrderingModuleItemKind, SourceItemOrderingTraitAssocItemKind, SourceItemOrderingTraitAssocItemKinds,
SourceItemOrderingWithinModuleItemGroupings,
DisallowedPath, DisallowedPathWithoutReplacement, InherentImplLintScope, MacroMatcher, MatchLintBehaviour,
PubUnderscoreFieldsBehaviour, Rename, SourceItemOrdering, SourceItemOrderingCategory,
SourceItemOrderingModuleItemGroupings, SourceItemOrderingModuleItemKind, SourceItemOrderingTraitAssocItemKind,
SourceItemOrderingTraitAssocItemKinds, SourceItemOrderingWithinModuleItemGroupings,
};
use clippy_utils::msrvs::Msrv;
use itertools::Itertools;
@@ -663,6 +663,9 @@ fn span_from_toml_range(file: &SourceFile, span: Range<usize>) -> Span {
/// A list of paths to types that should be treated as if they do not contain interior mutability
#[lints(borrow_interior_mutable_const, declare_interior_mutable_const, ifs_same_cond, mutable_key_type)]
ignore_interior_mutability: Vec<String> = Vec::from(["bytes::Bytes".into()]),
/// Sets the scope ("crate", "file", or "module") in which duplicate inherent `impl` blocks for the same type are linted.
#[lints(multiple_inherent_impl)]
inherent_impl_lint_scope: InherentImplLintScope = InherentImplLintScope::Crate,
/// The maximum size of the `Err`-variant in a `Result` returned from a function
#[lints(result_large_err)]
large_error_threshold: u64 = 128,
+8
View File
@@ -698,3 +698,11 @@ pub enum PubUnderscoreFieldsBehaviour {
PubliclyExported,
AllPubFields,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum InherentImplLintScope {
Crate,
File,
Module,
}
+44 -11
View File
@@ -1,17 +1,23 @@
use clippy_config::Conf;
use clippy_config::types::InherentImplLintScope;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::is_lint_allowed;
use clippy_utils::fulfill_or_allowed;
use rustc_data_structures::fx::FxHashMap;
use rustc_hir::def_id::LocalDefId;
use rustc_hir::def_id::{LocalDefId, LocalModDefId};
use rustc_hir::{Item, ItemKind, Node};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::Span;
use rustc_session::impl_lint_pass;
use rustc_span::{FileName, Span};
use std::collections::hash_map::Entry;
declare_clippy_lint! {
/// ### What it does
/// Checks for multiple inherent implementations of a struct
///
/// The config option controls the scope in which multiple inherent `impl` blocks for the same
/// struct are linted, allowing values of `module` (only within the same module), `file`
/// (within the same file), or `crate` (anywhere in the crate, default).
///
/// ### Why restrict this?
/// Splitting the implementation of a type makes the code harder to navigate.
///
@@ -41,7 +47,26 @@
"Multiple inherent impl that could be grouped"
}
declare_lint_pass!(MultipleInherentImpl => [MULTIPLE_INHERENT_IMPL]);
impl_lint_pass!(MultipleInherentImpl => [MULTIPLE_INHERENT_IMPL]);
pub struct MultipleInherentImpl {
scope: InherentImplLintScope,
}
impl MultipleInherentImpl {
pub fn new(conf: &'static Conf) -> Self {
Self {
scope: conf.inherent_impl_lint_scope,
}
}
}
#[derive(Hash, Eq, PartialEq, Clone)]
enum Criterion {
Module(LocalModDefId),
File(FileName),
Crate,
}
impl<'tcx> LateLintPass<'tcx> for MultipleInherentImpl {
fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
@@ -55,18 +80,27 @@ fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
for (&id, impl_ids) in &impls.inherent_impls {
if impl_ids.len() < 2
// Check for `#[allow]` on the type definition
|| is_lint_allowed(
// Check for `#[expect]` or `#[allow]` on the type definition
|| fulfill_or_allowed(
cx,
MULTIPLE_INHERENT_IMPL,
cx.tcx.local_def_id_to_hir_id(id),
[cx.tcx.local_def_id_to_hir_id(id)],
) {
continue;
}
for impl_id in impl_ids.iter().map(|id| id.expect_local()) {
let impl_ty = cx.tcx.type_of(impl_id).instantiate_identity();
match type_map.entry(impl_ty) {
let hir_id = cx.tcx.local_def_id_to_hir_id(impl_id);
let criterion = match self.scope {
InherentImplLintScope::Module => Criterion::Module(cx.tcx.parent_module(hir_id)),
InherentImplLintScope::File => {
let span = cx.tcx.hir_span(hir_id);
Criterion::File(cx.tcx.sess.source_map().lookup_source_file(span.lo()).name.clone())
},
InherentImplLintScope::Crate => Criterion::Crate,
};
match type_map.entry((impl_ty, criterion)) {
Entry::Vacant(e) => {
// Store the id for the first impl block of this type. The span is retrieved lazily.
e.insert(IdOrSpan::Id(impl_id));
@@ -97,7 +131,6 @@ fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
// Switching to the next type definition, no need to keep the current entries around.
type_map.clear();
}
// `TyCtxt::crate_inherent_impls` doesn't have a defined order. Sort the lint output first.
lint_spans.sort_by_key(|x| x.0.lo());
for (span, first_span) in lint_spans {
@@ -125,7 +158,7 @@ fn get_impl_span(cx: &LateContext<'_>, id: LocalDefId) -> Option<Span> {
{
(!span.from_expansion()
&& impl_item.generics.params.is_empty()
&& !is_lint_allowed(cx, MULTIPLE_INHERENT_IMPL, id))
&& !fulfill_or_allowed(cx, MULTIPLE_INHERENT_IMPL, [id]))
.then_some(span)
} else {
None
+1 -1
View File
@@ -587,7 +587,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_early_pass(|| Box::new(suspicious_operation_groupings::SuspiciousOperationGroupings));
store.register_late_pass(|_| Box::new(suspicious_trait_impl::SuspiciousImpl));
store.register_late_pass(|_| Box::new(map_unit_fn::MapUnit));
store.register_late_pass(|_| Box::new(inherent_impl::MultipleInherentImpl));
store.register_late_pass(move |_| Box::new(inherent_impl::MultipleInherentImpl::new(conf)));
store.register_late_pass(|_| Box::new(neg_cmp_op_on_partial_ord::NoNegCompOpForPartialOrd));
store.register_late_pass(move |_| Box::new(unwrap::Unwrap::new(conf)));
store.register_late_pass(move |_| Box::new(indexing_slicing::IndexingSlicing::new(conf)));
@@ -0,0 +1,7 @@
error: error reading Clippy's configuration file: unknown variant `FooBar`, expected one of `crate`, `file`, `module`
--> $DIR/tests/ui-cargo/multiple_inherent_impl/config_fail/clippy.toml:1:28
|
1 | inherent-impl-lint-scope = "FooBar"
| ^^^^^^^^
error: could not compile `config_fail` (bin "config_fail") due to 1 previous error
@@ -0,0 +1,7 @@
[package]
name = "config_fail"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
@@ -0,0 +1 @@
inherent-impl-lint-scope = "FooBar"
@@ -0,0 +1,3 @@
#![allow(dead_code)]
#![deny(clippy::multiple_inherent_impl)]
fn main() {}
@@ -0,0 +1,57 @@
error: multiple implementations of this structure
--> src/main.rs:11:1
|
11 | / impl S {
12 | | //^ Must trigger
13 | | fn second() {}
14 | | }
| |_^
|
note: first implementation here
--> src/main.rs:7:1
|
7 | / impl S {
8 | | fn first() {}
9 | | }
| |_^
note: the lint level is defined here
--> src/main.rs:2:9
|
2 | #![deny(clippy::multiple_inherent_impl)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: multiple implementations of this structure
--> src/main.rs:22:5
|
22 | / impl T {
23 | | //^ Must trigger
24 | | fn second() {}
25 | | }
| |_____^
|
note: first implementation here
--> src/main.rs:16:1
|
16 | / impl T {
17 | | fn first() {}
18 | | }
| |_^
error: multiple implementations of this structure
--> src/main.rs:36:1
|
36 | / impl b::T {
37 | | //^ Must trigger
38 | | fn second() {}
39 | | }
| |_^
|
note: first implementation here
--> src/b.rs:4:1
|
4 | / impl T {
5 | | fn first() {}
6 | | }
| |_^
error: could not compile `crate_fail` (bin "crate_fail") due to 3 previous errors
@@ -0,0 +1,7 @@
[package]
name = "crate_fail"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
@@ -0,0 +1 @@
inherent-impl-lint-scope = "crate"
@@ -0,0 +1,6 @@
pub struct S;
pub struct T;
impl T {
fn first() {}
}
@@ -0,0 +1,41 @@
#![allow(dead_code)]
#![deny(clippy::multiple_inherent_impl)]
struct S;
struct T;
impl S {
fn first() {}
}
impl S {
//^ Must trigger
fn second() {}
}
impl T {
fn first() {}
}
mod a {
use super::T;
impl T {
//^ Must trigger
fn second() {}
}
}
mod b;
impl b::S {
//^ Must NOT trigger
fn first() {}
fn second() {}
}
impl b::T {
//^ Must trigger
fn second() {}
}
fn main() {}
@@ -0,0 +1,58 @@
error: multiple implementations of this structure
--> src/main.rs:13:5
|
13 | / impl S {
14 | | //^ Must trigger
15 | | fn second() {}
16 | | }
| |_____^
|
note: first implementation here
--> src/main.rs:6:1
|
6 | / impl S {
7 | | fn first() {}
8 | | }
| |_^
note: the lint level is defined here
--> src/main.rs:2:9
|
2 | #![deny(clippy::multiple_inherent_impl)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: multiple implementations of this structure
--> src/main.rs:26:5
|
26 | / impl S {
27 | | //^ Must trigger
28 | |
29 | | fn second() {}
30 | | }
| |_____^
|
note: first implementation here
--> src/main.rs:22:5
|
22 | / impl S {
23 | | fn first() {}
24 | | }
| |_____^
error: multiple implementations of this structure
--> src/c.rs:17:5
|
17 | / impl T {
18 | | //^ Must trigger
19 | | fn second() {}
20 | | }
| |_____^
|
note: first implementation here
--> src/c.rs:10:5
|
10 | / impl T {
11 | | fn first() {}
12 | | }
| |_____^
error: could not compile `file_fail` (bin "file_fail") due to 3 previous errors
@@ -0,0 +1,7 @@
[package]
name = "file_fail"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
@@ -0,0 +1 @@
inherent-impl-lint-scope = "file"
@@ -0,0 +1,21 @@
pub struct S;
struct T;
impl S {
fn first() {}
}
mod d {
use super::T;
impl T {
fn first() {}
}
}
mod e {
use super::T;
impl T {
//^ Must trigger
fn second() {}
}
}
@@ -0,0 +1,40 @@
#![allow(dead_code)]
#![deny(clippy::multiple_inherent_impl)]
struct S;
impl S {
fn first() {}
}
mod a {
use super::S;
impl S {
//^ Must trigger
fn second() {}
}
}
mod b {
struct S;
impl S {
fn first() {}
}
impl S {
//^ Must trigger
fn second() {}
}
}
mod c;
impl c::S {
//^ Must NOT trigger
fn second() {}
}
fn main() {}
@@ -0,0 +1,41 @@
error: multiple implementations of this structure
--> src/main.rs:26:5
|
26 | / impl S {
27 | | //^ Must trigger
28 | |
29 | | fn second() {}
30 | | }
| |_____^
|
note: first implementation here
--> src/main.rs:22:5
|
22 | / impl S {
23 | | fn first() {}
24 | | }
| |_____^
note: the lint level is defined here
--> src/main.rs:2:9
|
2 | #![deny(clippy::multiple_inherent_impl)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: multiple implementations of this structure
--> src/c.rs:12:1
|
12 | / impl T {
13 | | //^ Must trigger
14 | | fn second() {}
15 | | }
| |_^
|
note: first implementation here
--> src/c.rs:8:1
|
8 | / impl T {
9 | | fn first() {}
10 | | }
| |_^
error: could not compile `module_fail` (bin "module_fail") due to 2 previous errors
@@ -0,0 +1,7 @@
[package]
name = "module_fail"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
@@ -0,0 +1 @@
inherent-impl-lint-scope = "module"
@@ -0,0 +1,15 @@
pub struct S;
struct T;
impl S {
fn first() {}
}
impl T {
fn first() {}
}
impl T {
//^ Must trigger
fn second() {}
}
@@ -0,0 +1,40 @@
#![allow(dead_code)]
#![deny(clippy::multiple_inherent_impl)]
struct S;
impl S {
fn first() {}
}
mod a {
use super::S;
impl S {
//^ Must NOT trigger
fn second() {}
}
}
mod b {
struct S;
impl S {
fn first() {}
}
impl S {
//^ Must trigger
fn second() {}
}
}
mod c;
impl c::S {
//^ Must NOT trigger
fn second() {}
}
fn main() {}
@@ -49,6 +49,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
excessive-nesting-threshold
future-size-threshold
ignore-interior-mutability
inherent-impl-lint-scope
large-error-threshold
lint-commented-code
literal-representation-threshold
@@ -144,6 +145,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
excessive-nesting-threshold
future-size-threshold
ignore-interior-mutability
inherent-impl-lint-scope
large-error-threshold
lint-commented-code
literal-representation-threshold
@@ -239,6 +241,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
excessive-nesting-threshold
future-size-threshold
ignore-interior-mutability
inherent-impl-lint-scope
large-error-threshold
lint-commented-code
literal-representation-threshold
+14 -1
View File
@@ -68,7 +68,20 @@ impl AllowedImpl {}
impl OneAllowedImpl {}
#[allow(clippy::multiple_inherent_impl)]
impl OneAllowedImpl {}
impl OneAllowedImpl {} // Lint, only one of the three blocks is allowed.
impl OneAllowedImpl {}
//~^ multiple_inherent_impl
#[expect(clippy::multiple_inherent_impl)]
struct ExpectedFulfilled;
impl ExpectedFulfilled {}
impl ExpectedFulfilled {}
struct OneExpected;
impl OneExpected {}
#[expect(clippy::multiple_inherent_impl)]
impl OneExpected {}
impl OneExpected {}
//~^ multiple_inherent_impl
fn main() {}
+14 -2
View File
@@ -57,7 +57,7 @@ LL | | }
error: multiple implementations of this structure
--> tests/ui/impl.rs:71:1
|
LL | impl OneAllowedImpl {} // Lint, only one of the three blocks is allowed.
LL | impl OneAllowedImpl {}
| ^^^^^^^^^^^^^^^^^^^^^^
|
note: first implementation here
@@ -66,5 +66,17 @@ note: first implementation here
LL | impl OneAllowedImpl {}
| ^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to 4 previous errors
error: multiple implementations of this structure
--> tests/ui/impl.rs:84:1
|
LL | impl OneExpected {}
| ^^^^^^^^^^^^^^^^^^^
|
note: first implementation here
--> tests/ui/impl.rs:81:1
|
LL | impl OneExpected {}
| ^^^^^^^^^^^^^^^^^^^
error: aborting due to 5 previous errors