Skip to content

Commit 8507e4c

Browse files
committed
Auto merge of #12090 - GuillaumeGomez:default-unconditional-recursion, r=llogiq
Extend `unconditional_recursion` lint to check for `Default` trait implementation In case the `Default` trait is implemented manually and is calling a static method (let's call it `a`) and then `a` is using `Self::default()`, it makes an infinite call recursion difficult to see without debugging. This extension checks that there is no such recursion possible. r? `@llogiq` changelog: Extend `unconditional_recursion` lint to check for `Default` trait implementation
2 parents 52ae262 + bf5c0d8 commit 8507e4c

File tree

4 files changed

+280
-44
lines changed

4 files changed

+280
-44
lines changed

clippy_lints/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
10831083
store.register_late_pass(|_| Box::new(repeat_vec_with_capacity::RepeatVecWithCapacity));
10841084
store.register_late_pass(|_| Box::new(uninhabited_references::UninhabitedReferences));
10851085
store.register_late_pass(|_| Box::new(ineffective_open_options::IneffectiveOpenOptions));
1086-
store.register_late_pass(|_| Box::new(unconditional_recursion::UnconditionalRecursion));
1086+
store.register_late_pass(|_| Box::<unconditional_recursion::UnconditionalRecursion>::default());
10871087
store.register_late_pass(move |_| {
10881088
Box::new(pub_underscore_fields::PubUnderscoreFields {
10891089
behavior: pub_underscore_fields_behavior,

clippy_lints/src/unconditional_recursion.rs

+230-41
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::{expr_or_init, get_trait_def_id};
2+
use clippy_utils::{expr_or_init, get_trait_def_id, path_def_id};
33
use rustc_ast::BinOpKind;
4+
use rustc_data_structures::fx::FxHashMap;
5+
use rustc_hir as hir;
6+
use rustc_hir::def::{DefKind, Res};
47
use rustc_hir::def_id::{DefId, LocalDefId};
5-
use rustc_hir::intravisit::{walk_body, FnKind};
6-
use rustc_hir::{Body, Expr, ExprKind, FnDecl, Item, ItemKind, Node};
8+
use rustc_hir::intravisit::{walk_body, walk_expr, FnKind, Visitor};
9+
use rustc_hir::{Body, Expr, ExprKind, FnDecl, HirId, Item, ItemKind, Node, QPath, TyKind};
10+
use rustc_hir_analysis::hir_ty_to_ty;
711
use rustc_lint::{LateContext, LateLintPass};
8-
use rustc_middle::ty::{self, Ty};
9-
use rustc_session::declare_lint_pass;
10-
use rustc_span::symbol::Ident;
12+
use rustc_middle::hir::map::Map;
13+
use rustc_middle::hir::nested_filter;
14+
use rustc_middle::ty::{self, AssocKind, Ty, TyCtxt};
15+
use rustc_session::impl_lint_pass;
16+
use rustc_span::symbol::{kw, Ident};
1117
use rustc_span::{sym, Span};
1218
use rustc_trait_selection::traits::error_reporting::suggestions::ReturnsVisitor;
1319

@@ -42,7 +48,26 @@ declare_clippy_lint! {
4248
"detect unconditional recursion in some traits implementation"
4349
}
4450

45-
declare_lint_pass!(UnconditionalRecursion => [UNCONDITIONAL_RECURSION]);
51+
#[derive(Default)]
52+
pub struct UnconditionalRecursion {
53+
/// The key is the `DefId` of the type implementing the `Default` trait and the value is the
54+
/// `DefId` of the return call.
55+
default_impl_for_type: FxHashMap<DefId, DefId>,
56+
}
57+
58+
impl_lint_pass!(UnconditionalRecursion => [UNCONDITIONAL_RECURSION]);
59+
60+
fn span_error(cx: &LateContext<'_>, method_span: Span, expr: &Expr<'_>) {
61+
span_lint_and_then(
62+
cx,
63+
UNCONDITIONAL_RECURSION,
64+
method_span,
65+
"function cannot return without recursing",
66+
|diag| {
67+
diag.span_note(expr.span, "recursive call site");
68+
},
69+
);
70+
}
4671

4772
fn get_ty_def_id(ty: Ty<'_>) -> Option<DefId> {
4873
match ty.peel_refs().kind() {
@@ -52,17 +77,60 @@ fn get_ty_def_id(ty: Ty<'_>) -> Option<DefId> {
5277
}
5378
}
5479

55-
fn has_conditional_return(body: &Body<'_>, expr: &Expr<'_>) -> bool {
80+
fn get_hir_ty_def_id(tcx: TyCtxt<'_>, hir_ty: rustc_hir::Ty<'_>) -> Option<DefId> {
81+
let TyKind::Path(qpath) = hir_ty.kind else { return None };
82+
match qpath {
83+
QPath::Resolved(_, path) => path.res.opt_def_id(),
84+
QPath::TypeRelative(_, _) => {
85+
let ty = hir_ty_to_ty(tcx, &hir_ty);
86+
87+
match ty.kind() {
88+
ty::Alias(ty::Projection, proj) => {
89+
Res::<HirId>::Def(DefKind::Trait, proj.trait_ref(tcx).def_id).opt_def_id()
90+
},
91+
_ => None,
92+
}
93+
},
94+
QPath::LangItem(..) => None,
95+
}
96+
}
97+
98+
fn get_return_calls_in_body<'tcx>(body: &'tcx Body<'tcx>) -> Vec<&'tcx Expr<'tcx>> {
5699
let mut visitor = ReturnsVisitor::default();
57100

58-
walk_body(&mut visitor, body);
59-
match visitor.returns.as_slice() {
101+
visitor.visit_body(body);
102+
visitor.returns
103+
}
104+
105+
fn has_conditional_return(body: &Body<'_>, expr: &Expr<'_>) -> bool {
106+
match get_return_calls_in_body(body).as_slice() {
60107
[] => false,
61108
[return_expr] => return_expr.hir_id != expr.hir_id,
62109
_ => true,
63110
}
64111
}
65112

113+
fn get_impl_trait_def_id(cx: &LateContext<'_>, method_def_id: LocalDefId) -> Option<DefId> {
114+
let hir_id = cx.tcx.local_def_id_to_hir_id(method_def_id);
115+
if let Some((
116+
_,
117+
Node::Item(Item {
118+
kind: ItemKind::Impl(impl_),
119+
owner_id,
120+
..
121+
}),
122+
)) = cx.tcx.hir().parent_iter(hir_id).next()
123+
// We exclude `impl` blocks generated from rustc's proc macros.
124+
&& !cx.tcx.has_attr(*owner_id, sym::automatically_derived)
125+
// It is a implementation of a trait.
126+
&& let Some(trait_) = impl_.of_trait
127+
{
128+
trait_.trait_def_id()
129+
} else {
130+
None
131+
}
132+
}
133+
66134
#[allow(clippy::unnecessary_def_path)]
67135
fn check_partial_eq(cx: &LateContext<'_>, method_span: Span, method_def_id: LocalDefId, name: Ident, expr: &Expr<'_>) {
68136
let args = cx
@@ -75,20 +143,7 @@ fn check_partial_eq(cx: &LateContext<'_>, method_span: Span, method_def_id: Loca
75143
&& let Some(other_arg) = get_ty_def_id(*other_arg)
76144
// The two arguments are of the same type.
77145
&& self_arg == other_arg
78-
&& let hir_id = cx.tcx.local_def_id_to_hir_id(method_def_id)
79-
&& let Some((
80-
_,
81-
Node::Item(Item {
82-
kind: ItemKind::Impl(impl_),
83-
owner_id,
84-
..
85-
}),
86-
)) = cx.tcx.hir().parent_iter(hir_id).next()
87-
// We exclude `impl` blocks generated from rustc's proc macros.
88-
&& !cx.tcx.has_attr(*owner_id, sym::automatically_derived)
89-
// It is a implementation of a trait.
90-
&& let Some(trait_) = impl_.of_trait
91-
&& let Some(trait_def_id) = trait_.trait_def_id()
146+
&& let Some(trait_def_id) = get_impl_trait_def_id(cx, method_def_id)
92147
// The trait is `PartialEq`.
93148
&& Some(trait_def_id) == get_trait_def_id(cx, &["core", "cmp", "PartialEq"])
94149
{
@@ -125,15 +180,7 @@ fn check_partial_eq(cx: &LateContext<'_>, method_span: Span, method_def_id: Loca
125180
_ => false,
126181
};
127182
if is_bad {
128-
span_lint_and_then(
129-
cx,
130-
UNCONDITIONAL_RECURSION,
131-
method_span,
132-
"function cannot return without recursing",
133-
|diag| {
134-
diag.span_note(expr.span, "recursive call site");
135-
},
136-
);
183+
span_error(cx, method_span, expr);
137184
}
138185
}
139186
}
@@ -177,15 +224,156 @@ fn check_to_string(cx: &LateContext<'_>, method_span: Span, method_def_id: Local
177224
_ => false,
178225
};
179226
if is_bad {
180-
span_lint_and_then(
227+
span_error(cx, method_span, expr);
228+
}
229+
}
230+
}
231+
232+
fn is_default_method_on_current_ty(tcx: TyCtxt<'_>, qpath: QPath<'_>, implemented_ty_id: DefId) -> bool {
233+
match qpath {
234+
QPath::Resolved(_, path) => match path.segments {
235+
[first, .., last] => last.ident.name == kw::Default && first.res.opt_def_id() == Some(implemented_ty_id),
236+
_ => false,
237+
},
238+
QPath::TypeRelative(ty, segment) => {
239+
if segment.ident.name != kw::Default {
240+
return false;
241+
}
242+
if matches!(
243+
ty.kind,
244+
TyKind::Path(QPath::Resolved(
245+
_,
246+
hir::Path {
247+
res: Res::SelfTyAlias { .. },
248+
..
249+
},
250+
))
251+
) {
252+
return true;
253+
}
254+
get_hir_ty_def_id(tcx, *ty) == Some(implemented_ty_id)
255+
},
256+
QPath::LangItem(..) => false,
257+
}
258+
}
259+
260+
struct CheckCalls<'a, 'tcx> {
261+
cx: &'a LateContext<'tcx>,
262+
map: Map<'tcx>,
263+
implemented_ty_id: DefId,
264+
found_default_call: bool,
265+
method_span: Span,
266+
}
267+
268+
impl<'a, 'tcx> Visitor<'tcx> for CheckCalls<'a, 'tcx>
269+
where
270+
'tcx: 'a,
271+
{
272+
type NestedFilter = nested_filter::OnlyBodies;
273+
274+
fn nested_visit_map(&mut self) -> Self::Map {
275+
self.map
276+
}
277+
278+
#[allow(clippy::unnecessary_def_path)]
279+
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
280+
if self.found_default_call {
281+
return;
282+
}
283+
walk_expr(self, expr);
284+
285+
if let ExprKind::Call(f, _) = expr.kind
286+
&& let ExprKind::Path(qpath) = f.kind
287+
&& is_default_method_on_current_ty(self.cx.tcx, qpath, self.implemented_ty_id)
288+
&& let Some(method_def_id) = path_def_id(self.cx, f)
289+
&& let Some(trait_def_id) = self.cx.tcx.trait_of_item(method_def_id)
290+
&& Some(trait_def_id) == get_trait_def_id(self.cx, &["core", "default", "Default"])
291+
{
292+
self.found_default_call = true;
293+
span_error(self.cx, self.method_span, expr);
294+
}
295+
}
296+
}
297+
298+
impl UnconditionalRecursion {
299+
#[allow(clippy::unnecessary_def_path)]
300+
fn init_default_impl_for_type_if_needed(&mut self, cx: &LateContext<'_>) {
301+
if self.default_impl_for_type.is_empty()
302+
&& let Some(default_trait_id) = get_trait_def_id(cx, &["core", "default", "Default"])
303+
{
304+
let impls = cx.tcx.trait_impls_of(default_trait_id);
305+
for (ty, impl_def_ids) in impls.non_blanket_impls() {
306+
let Some(self_def_id) = ty.def() else { continue };
307+
for impl_def_id in impl_def_ids {
308+
if !cx.tcx.has_attr(*impl_def_id, sym::automatically_derived) &&
309+
let Some(assoc_item) = cx
310+
.tcx
311+
.associated_items(impl_def_id)
312+
.in_definition_order()
313+
// We're not interested in foreign implementations of the `Default` trait.
314+
.find(|item| {
315+
item.kind == AssocKind::Fn && item.def_id.is_local() && item.name == kw::Default
316+
})
317+
&& let Some(body_node) = cx.tcx.hir().get_if_local(assoc_item.def_id)
318+
&& let Some(body_id) = body_node.body_id()
319+
&& let body = cx.tcx.hir().body(body_id)
320+
// We don't want to keep it if it has conditional return.
321+
&& let [return_expr] = get_return_calls_in_body(body).as_slice()
322+
&& let ExprKind::Call(call_expr, _) = return_expr.kind
323+
// We need to use typeck here to infer the actual function being called.
324+
&& let body_def_id = cx.tcx.hir().enclosing_body_owner(call_expr.hir_id)
325+
&& let Some(body_owner) = cx.tcx.hir().maybe_body_owned_by(body_def_id)
326+
&& let typeck = cx.tcx.typeck_body(body_owner)
327+
&& let Some(call_def_id) = typeck.type_dependent_def_id(call_expr.hir_id)
328+
{
329+
self.default_impl_for_type.insert(self_def_id, call_def_id);
330+
}
331+
}
332+
}
333+
}
334+
}
335+
336+
fn check_default_new<'tcx>(
337+
&mut self,
338+
cx: &LateContext<'tcx>,
339+
decl: &FnDecl<'tcx>,
340+
body: &'tcx Body<'tcx>,
341+
method_span: Span,
342+
method_def_id: LocalDefId,
343+
) {
344+
// We're only interested into static methods.
345+
if decl.implicit_self.has_implicit_self() {
346+
return;
347+
}
348+
// We don't check trait implementations.
349+
if get_impl_trait_def_id(cx, method_def_id).is_some() {
350+
return;
351+
}
352+
353+
let hir_id = cx.tcx.local_def_id_to_hir_id(method_def_id);
354+
if let Some((
355+
_,
356+
Node::Item(Item {
357+
kind: ItemKind::Impl(impl_),
358+
..
359+
}),
360+
)) = cx.tcx.hir().parent_iter(hir_id).next()
361+
&& let Some(implemented_ty_id) = get_hir_ty_def_id(cx.tcx, *impl_.self_ty)
362+
&& {
363+
self.init_default_impl_for_type_if_needed(cx);
364+
true
365+
}
366+
&& let Some(return_def_id) = self.default_impl_for_type.get(&implemented_ty_id)
367+
&& method_def_id.to_def_id() == *return_def_id
368+
{
369+
let mut c = CheckCalls {
181370
cx,
182-
UNCONDITIONAL_RECURSION,
371+
map: cx.tcx.hir(),
372+
implemented_ty_id,
373+
found_default_call: false,
183374
method_span,
184-
"function cannot return without recursing",
185-
|diag| {
186-
diag.span_note(expr.span, "recursive call site");
187-
},
188-
);
375+
};
376+
walk_body(&mut c, body);
189377
}
190378
}
191379
}
@@ -195,7 +383,7 @@ impl<'tcx> LateLintPass<'tcx> for UnconditionalRecursion {
195383
&mut self,
196384
cx: &LateContext<'tcx>,
197385
kind: FnKind<'tcx>,
198-
_decl: &'tcx FnDecl<'tcx>,
386+
decl: &'tcx FnDecl<'tcx>,
199387
body: &'tcx Body<'tcx>,
200388
method_span: Span,
201389
method_def_id: LocalDefId,
@@ -211,6 +399,7 @@ impl<'tcx> LateLintPass<'tcx> for UnconditionalRecursion {
211399
} else if name.name == sym::to_string {
212400
check_to_string(cx, method_span, method_def_id, name, expr);
213401
}
402+
self.check_default_new(cx, decl, body, method_span, method_def_id);
214403
}
215404
}
216405
}

tests/ui/unconditional_recursion.rs

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//@no-rustfix
22

33
#![warn(clippy::unconditional_recursion)]
4-
#![allow(clippy::partialeq_ne_impl)]
4+
#![allow(clippy::partialeq_ne_impl, clippy::default_constructed_unit_structs)]
55

66
enum Foo {
77
A,
@@ -232,6 +232,38 @@ impl std::string::ToString for S11 {
232232
}
233233
}
234234

235+
struct S12;
236+
237+
impl std::default::Default for S12 {
238+
fn default() -> Self {
239+
Self::new()
240+
}
241+
}
242+
243+
impl S12 {
244+
fn new() -> Self {
245+
//~^ ERROR: function cannot return without recursing
246+
Self::default()
247+
}
248+
249+
fn bar() -> Self {
250+
// Should not warn!
251+
Self::default()
252+
}
253+
}
254+
255+
#[derive(Default)]
256+
struct S13 {
257+
f: u32,
258+
}
259+
260+
impl S13 {
261+
fn new() -> Self {
262+
// Shoud not warn!
263+
Self::default()
264+
}
265+
}
266+
235267
fn main() {
236268
// test code goes here
237269
}

0 commit comments

Comments
 (0)