Skip to content

Commit a453e8c

Browse files
committed
Add inline_trait_bounds lint
1 parent f763854 commit a453e8c

7 files changed

Lines changed: 491 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6805,6 +6805,7 @@ Released 2018-09-13
68056805
[`inline_asm_x86_intel_syntax`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_asm_x86_intel_syntax
68066806
[`inline_fn_without_body`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_fn_without_body
68076807
[`inline_modules`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_modules
6808+
[`inline_trait_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#inline_trait_bounds
68086809
[`inspect_for_each`]: https://rust-lang.github.io/rust-clippy/master/index.html#inspect_for_each
68096810
[`int_plus_one`]: https://rust-lang.github.io/rust-clippy/master/index.html#int_plus_one
68106811
[`integer_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#integer_arithmetic

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
232232
crate::inherent_to_string::INHERENT_TO_STRING_SHADOW_DISPLAY_INFO,
233233
crate::init_numbered_fields::INIT_NUMBERED_FIELDS_INFO,
234234
crate::inline_fn_without_body::INLINE_FN_WITHOUT_BODY_INFO,
235+
crate::inline_trait_bounds::INLINE_TRAIT_BOUNDS_INFO,
235236
crate::int_plus_one::INT_PLUS_ONE_INFO,
236237
crate::item_name_repetitions::ENUM_VARIANT_NAMES_INFO,
237238
crate::item_name_repetitions::MODULE_INCEPTION_INFO,
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::source::{HasSession, snippet};
3+
use rustc_ast::NodeId;
4+
use rustc_ast::ast::{Fn, FnRetTy, GenericParam, GenericParamKind};
5+
use rustc_ast::visit::{FnCtxt, FnKind};
6+
use rustc_errors::Applicability;
7+
use rustc_lint::{EarlyContext, EarlyLintPass};
8+
use rustc_session::declare_lint_pass;
9+
use rustc_span::Span;
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
/// Enforce that `where` bounds are used for all trait bounds.
14+
///
15+
/// ### Why restrict this?
16+
/// Enforce a single style throughout a codebase.
17+
/// Avoid uncertainty about whether a bound should be inline
18+
/// or out-of-line (i.e. a where bound).
19+
/// Avoid complex inline bounds, which could make a function declaration more difficult to read.
20+
///
21+
/// ### Known limitations
22+
/// Only lints functions and method declararions. Bounds on structs, enums,
23+
/// and impl blocks are not yet covered.
24+
///
25+
/// ### Example
26+
/// ```no_run
27+
/// fn foo<T: Clone>() {}
28+
/// ```
29+
///
30+
/// Use instead:
31+
/// ```no_run
32+
/// fn foo<T>() where T: Clone {}
33+
/// ```
34+
#[clippy::version = "1.97.0"]
35+
pub INLINE_TRAIT_BOUNDS,
36+
restriction,
37+
"enforce that `where` bounds are used for all trait bounds"
38+
}
39+
40+
declare_lint_pass!(InlineTraitBounds => [INLINE_TRAIT_BOUNDS]);
41+
42+
impl EarlyLintPass for InlineTraitBounds {
43+
fn check_fn(&mut self, cx: &EarlyContext<'_>, kind: FnKind<'_>, _: Span, _: NodeId) {
44+
let FnKind::Fn(ctxt, _vis, f) = kind else {
45+
return;
46+
};
47+
48+
// Skip foreign functions (extern "C" etc.)
49+
if !matches!(ctxt, FnCtxt::Free | FnCtxt::Assoc(..)) {
50+
return;
51+
}
52+
53+
if f.sig.span.in_external_macro(cx.sess().source_map()) {
54+
return;
55+
}
56+
57+
lint_fn(cx, f);
58+
}
59+
}
60+
61+
fn lint_fn(cx: &EarlyContext<'_>, f: &Fn) {
62+
let generics = &f.generics;
63+
let offenders: Vec<&GenericParam> = generics
64+
.params
65+
.iter()
66+
.filter(|param| {
67+
!param.bounds.is_empty() && matches!(param.kind, GenericParamKind::Lifetime | GenericParamKind::Type { .. })
68+
})
69+
.collect();
70+
if offenders.is_empty() {
71+
return;
72+
}
73+
74+
let predicates = offenders
75+
.iter()
76+
.map(|param| build_predicate_text(cx, param))
77+
.collect::<Vec<_>>();
78+
79+
let mut edits = Vec::new();
80+
81+
for param in offenders {
82+
if let Some(colon) = param.colon_span {
83+
let remove_span = colon.to(param.bounds.last().unwrap().span());
84+
85+
edits.push((remove_span, String::new()));
86+
}
87+
}
88+
89+
let predicate_text = predicates.join(", ");
90+
91+
let where_clause = &generics.where_clause;
92+
if where_clause.has_where_token {
93+
let (insert_at, suffix) = if let Some(last_pred) = where_clause.predicates.last() {
94+
// existing `where` with predicates: append after last predicate
95+
(last_pred.span.shrink_to_hi(), format!(", {predicate_text}"))
96+
} else {
97+
// `where` token present but empty predicate list
98+
(where_clause.span.shrink_to_hi(), format!(" {predicate_text}"))
99+
};
100+
101+
edits.push((insert_at, suffix));
102+
} else {
103+
let insert_at = match &f.sig.decl.output {
104+
FnRetTy::Default(span) => span.shrink_to_lo(),
105+
FnRetTy::Ty(ty) => ty.span.shrink_to_hi(),
106+
};
107+
edits.push((insert_at, format!(" where {predicate_text}")));
108+
}
109+
110+
span_lint_and_then(
111+
cx,
112+
INLINE_TRAIT_BOUNDS,
113+
generics.span,
114+
"inline trait bounds used",
115+
|diag| {
116+
diag.multipart_suggestion(
117+
"move bounds to a `where` clause",
118+
edits,
119+
Applicability::MachineApplicable,
120+
);
121+
},
122+
);
123+
}
124+
125+
fn build_predicate_text(cx: &EarlyContext<'_>, param: &GenericParam) -> String {
126+
// bounds is guaranteed non-empty by the filter in `lint_fn`
127+
let first = param.bounds.first().unwrap();
128+
let last = param.bounds.last().unwrap();
129+
130+
let bounds_span = first.span().to(last.span());
131+
132+
let lhs = snippet(cx, param.ident.span, "..");
133+
134+
let rhs = snippet(cx, bounds_span, "..");
135+
136+
format!("{lhs}: {rhs}")
137+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ mod inherent_impl;
168168
mod inherent_to_string;
169169
mod init_numbered_fields;
170170
mod inline_fn_without_body;
171+
mod inline_trait_bounds;
171172
mod int_plus_one;
172173
mod item_name_repetitions;
173174
mod items_after_statements;
@@ -518,6 +519,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
518519
Box::new(|| Box::new(field_scoped_visibility_modifiers::FieldScopedVisibilityModifiers)),
519520
Box::new(|| Box::new(cfg_not_test::CfgNotTest)),
520521
Box::new(|| Box::new(empty_line_after::EmptyLineAfter::new())),
522+
Box::new(|| Box::new(inline_trait_bounds::InlineTraitBounds)),
521523
// add early passes here, used by `cargo dev new_lint`
522524
];
523525
store.early_passes.extend(early_lints);

tests/ui/inline_trait_bounds.fixed

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#![warn(clippy::inline_trait_bounds)]
2+
3+
// Free functions
4+
5+
fn inline_simple<T>() where T: Clone {}
6+
//~^ inline_trait_bounds
7+
8+
fn inline_multiple<T, U>() where T: Clone + Copy, U: core::fmt::Debug {}
9+
//~^ inline_trait_bounds
10+
11+
fn inline_lifetime<'a, 'b>(x: &'a str, y: &'b str) -> &'b str where 'a: 'b {
12+
//~^ inline_trait_bounds
13+
y
14+
}
15+
16+
#[allow(clippy::multiple_bound_locations)]
17+
fn inline_with_where<T>()
18+
//~^ inline_trait_bounds
19+
where
20+
T: core::fmt::Debug, T: Clone,
21+
{
22+
}
23+
24+
fn inline_with_const<T, const N: usize>() where T: Clone {}
25+
//~^ inline_trait_bounds
26+
27+
fn inline_with_return<T>(val: T) -> T where T: Clone {
28+
//~^ inline_trait_bounds
29+
val
30+
}
31+
32+
// Trait methods
33+
34+
trait MyTrait {
35+
fn trait_method_inline<T>(&self) where T: Clone;
36+
//~^ inline_trait_bounds
37+
38+
fn trait_method_default<T>(&self) where T: Clone + Copy {}
39+
//~^ inline_trait_bounds
40+
41+
fn trait_method_where<T>(&self)
42+
where
43+
T: Clone;
44+
}
45+
46+
// Impl methods
47+
48+
struct MyStruct;
49+
50+
impl MyStruct {
51+
fn impl_method_inline<T>(&self) where T: Clone {}
52+
//~^ inline_trait_bounds
53+
54+
fn impl_method_multiple<T, U>(&self) where T: Clone, U: core::fmt::Debug {}
55+
//~^ inline_trait_bounds
56+
}
57+
58+
impl MyTrait for MyStruct {
59+
fn trait_method_inline<T>(&self) where T: Clone {}
60+
//~^ inline_trait_bounds
61+
62+
fn trait_method_default<T>(&self) where T: Clone + Copy {}
63+
//~^ inline_trait_bounds
64+
65+
fn trait_method_where<T>(&self)
66+
where
67+
T: Clone,
68+
{
69+
}
70+
}
71+
72+
// Should NOT lint
73+
74+
fn where_only<T>()
75+
where
76+
T: Clone,
77+
{
78+
}
79+
80+
fn no_bounds<T, U>() {}
81+
82+
fn no_generics() {}
83+
84+
struct InlineStruct<T: Clone>(T);
85+
86+
enum InlineEnum<T: Clone> {
87+
A(T),
88+
}
89+
90+
#[allow(invalid_type_param_default)]
91+
//~v inline_trait_bounds
92+
fn with_default_value<T = u32>(x: T) -> T where T: Clone {
93+
x
94+
}

tests/ui/inline_trait_bounds.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#![warn(clippy::inline_trait_bounds)]
2+
3+
// Free functions
4+
5+
fn inline_simple<T: Clone>() {}
6+
//~^ inline_trait_bounds
7+
8+
fn inline_multiple<T: Clone + Copy, U: core::fmt::Debug>() {}
9+
//~^ inline_trait_bounds
10+
11+
fn inline_lifetime<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'b str {
12+
//~^ inline_trait_bounds
13+
y
14+
}
15+
16+
#[allow(clippy::multiple_bound_locations)]
17+
fn inline_with_where<T: Clone>()
18+
//~^ inline_trait_bounds
19+
where
20+
T: core::fmt::Debug,
21+
{
22+
}
23+
24+
fn inline_with_const<T: Clone, const N: usize>() {}
25+
//~^ inline_trait_bounds
26+
27+
fn inline_with_return<T: Clone>(val: T) -> T {
28+
//~^ inline_trait_bounds
29+
val
30+
}
31+
32+
// Trait methods
33+
34+
trait MyTrait {
35+
fn trait_method_inline<T: Clone>(&self);
36+
//~^ inline_trait_bounds
37+
38+
fn trait_method_default<T: Clone + Copy>(&self) {}
39+
//~^ inline_trait_bounds
40+
41+
fn trait_method_where<T>(&self)
42+
where
43+
T: Clone;
44+
}
45+
46+
// Impl methods
47+
48+
struct MyStruct;
49+
50+
impl MyStruct {
51+
fn impl_method_inline<T: Clone>(&self) {}
52+
//~^ inline_trait_bounds
53+
54+
fn impl_method_multiple<T: Clone, U: core::fmt::Debug>(&self) {}
55+
//~^ inline_trait_bounds
56+
}
57+
58+
impl MyTrait for MyStruct {
59+
fn trait_method_inline<T: Clone>(&self) {}
60+
//~^ inline_trait_bounds
61+
62+
fn trait_method_default<T: Clone + Copy>(&self) {}
63+
//~^ inline_trait_bounds
64+
65+
fn trait_method_where<T>(&self)
66+
where
67+
T: Clone,
68+
{
69+
}
70+
}
71+
72+
// Should NOT lint
73+
74+
fn where_only<T>()
75+
where
76+
T: Clone,
77+
{
78+
}
79+
80+
fn no_bounds<T, U>() {}
81+
82+
fn no_generics() {}
83+
84+
struct InlineStruct<T: Clone>(T);
85+
86+
enum InlineEnum<T: Clone> {
87+
A(T),
88+
}
89+
90+
#[allow(invalid_type_param_default)]
91+
//~v inline_trait_bounds
92+
fn with_default_value<T: Clone = u32>(x: T) -> T {
93+
x
94+
}

0 commit comments

Comments
 (0)