Skip to content

Commit d955bfc

Browse files
committed
Add new lint: direct-recursion Lint
changelog: new lint: [`direct_recursion`] Closes #428
1 parent f2922e7 commit d955bfc

File tree

6 files changed

+155
-0
lines changed

6 files changed

+155
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5721,6 +5721,7 @@ Released 2018-09-13
57215721
[`derive_ord_xor_partial_ord`]: https://rust-lang.github.io/rust-clippy/master/index.html#derive_ord_xor_partial_ord
57225722
[`derive_partial_eq_without_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#derive_partial_eq_without_eq
57235723
[`derived_hash_with_manual_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#derived_hash_with_manual_eq
5724+
[`direct_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#direct_recursion
57245725
[`disallowed_macros`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros
57255726
[`disallowed_method`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_method
57265727
[`disallowed_methods`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
107107
crate::derive::DERIVE_PARTIAL_EQ_WITHOUT_EQ_INFO,
108108
crate::derive::EXPL_IMPL_CLONE_ON_COPY_INFO,
109109
crate::derive::UNSAFE_DERIVE_DESERIALIZE_INFO,
110+
crate::direct_recursion::DIRECT_RECURSION_INFO,
110111
crate::disallowed_macros::DISALLOWED_MACROS_INFO,
111112
crate::disallowed_methods::DISALLOWED_METHODS_INFO,
112113
crate::disallowed_names::DISALLOWED_NAMES_INFO,

clippy_lints/src/direct_recursion.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use rustc_hir::def::{DefKind, Res};
3+
use rustc_hir::{Expr, ExprKind, QPath};
4+
use rustc_lint::{LateContext, LateLintPass};
5+
use rustc_session::declare_lint_pass;
6+
7+
declare_clippy_lint! {
8+
/// ### What it does
9+
/// Checks for functions that call themselves from their body.
10+
/// ### Why restrict this?
11+
/// In Safety Critical contexts, recursive calls can lead to catastrophic crashes if they happen to overflow
12+
/// the stack. Recursive calls must therefore be tightly vetted.
13+
/// ### Notes
14+
///
15+
/// #### Control Flow
16+
/// This lint only checks for the existence of recursive calls; it doesn't discriminate based on conditional
17+
/// control flow. Recursive calls that are considered safe should instead be vetted and documented accordingly.
18+
///
19+
/// #### How to vet recursive calls
20+
/// It is recommended that this lint be used in `deny` mode, together with #![deny(clippy::allow_attributes_without_reason)](https://rust-lang.github.io/rust-clippy/master/index.html#allow_attributes_without_reason).
21+
///
22+
/// Once that is done, recursive calls can be vetted accordingly:
23+
///
24+
/// ```no_run
25+
/// fn i_call_myself_in_a_bounded_way(bound: u8) {
26+
/// if bound > 0 {
27+
/// #[expect(
28+
/// clippy::direct_recursion,
29+
/// reason = "Author has audited this function and determined that its recursive call is fine."
30+
/// )]
31+
/// i_call_myself_in_a_bounded_way(bound - 1);
32+
/// }
33+
/// }
34+
/// ```
35+
///
36+
/// Note the use of an `expect` attribute and a `reason` to go along with it.
37+
///
38+
/// * The `expect` attribute (instead of `allow`) ensures that the lint being allowed is enabled. This serves as a
39+
/// double-check of this lint being used where the author believes it is active.
40+
/// * The `reason` field is required by the `clippy::allow_attributes_without_reason` lint. This is very useful for ensuring
41+
/// that vetting work is documented.
42+
///
43+
/// Recursive calls that are vetted to be correct should always be annotated in such a way.
44+
#[clippy::version = "1.89.0"]
45+
pub DIRECT_RECURSION,
46+
restriction,
47+
"default lint description"
48+
}
49+
declare_lint_pass!(DirectRecursion => [DIRECT_RECURSION]);
50+
51+
impl<'tcx> LateLintPass<'tcx> for DirectRecursion {
52+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
53+
if let ExprKind::Call(call_expr, _) = &expr.kind
54+
&& let body_def_id = cx.tcx.hir_enclosing_body_owner(call_expr.hir_id)
55+
&& let ExprKind::Path(c_expr_path) = call_expr.kind
56+
&& let QPath::Resolved(_lhs, path) = c_expr_path
57+
&& let Res::Def(DefKind::Fn, fn_path_id) = path.res
58+
&& fn_path_id == body_def_id.into()
59+
{
60+
span_lint(
61+
cx,
62+
DIRECT_RECURSION,
63+
expr.span,
64+
"this function contains a call to itself",
65+
);
66+
}
67+
}
68+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ mod default_union_representation;
114114
mod dereference;
115115
mod derivable_impls;
116116
mod derive;
117+
mod direct_recursion;
117118
mod disallowed_macros;
118119
mod disallowed_methods;
119120
mod disallowed_names;
@@ -951,5 +952,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
951952
store.register_late_pass(|_| Box::new(cloned_ref_to_slice_refs::ClonedRefToSliceRefs::new(conf)));
952953
store.register_late_pass(|_| Box::new(infallible_try_from::InfallibleTryFrom));
953954
store.register_late_pass(|_| Box::new(coerce_container_to_any::CoerceContainerToAny));
955+
store.register_late_pass(|_| Box::new(direct_recursion::DirectRecursion));
954956
// add lints here, do not remove this comment, it's used in `new_lint`
955957
}

tests/ui/direct_recursion.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#![deny(clippy::direct_recursion)]
2+
#![deny(clippy::allow_attributes_without_reason)]
3+
4+
// Basic Cases //
5+
6+
#[allow(unconditional_recursion, reason = "We're not testing for that lint")]
7+
fn i_call_myself_always() {
8+
i_call_myself_always();
9+
//~^ direct_recursion
10+
}
11+
12+
fn i_call_myself_conditionally(do_i: bool) {
13+
if do_i {
14+
i_call_myself_conditionally(false);
15+
//~^ direct_recursion
16+
}
17+
}
18+
19+
// Basic Counterexamples //
20+
21+
fn i_get_called_by_others() {}
22+
23+
fn i_call_something_else() {
24+
i_get_called_by_others();
25+
}
26+
27+
// Elaborate Cases //
28+
29+
// Here we check that we're allowed to bless specific recursive calls.
30+
// A fine-grained control of where to allow recursion is desirable.
31+
// This is a test of such a feature.
32+
fn i_call_myself_in_a_bounded_way(bound: u8) {
33+
if bound > 0 {
34+
#[expect(
35+
clippy::direct_recursion,
36+
reason = "Author has audited this function and determined that its recursive call is fine."
37+
)]
38+
i_call_myself_in_a_bounded_way(bound - 1);
39+
}
40+
}
41+
42+
// Here we check that blessing a specific recursive call doesn't
43+
// let other recursive calls go through.
44+
fn i_have_one_blessing_but_two_calls(bound: u8) {
45+
if bound > 25 {
46+
// "Author has audited this function and determined that its recursive call is fine."
47+
#[expect(
48+
clippy::direct_recursion,
49+
reason = "Author has audited this function and determined that its recursive call is fine."
50+
)]
51+
i_have_one_blessing_but_two_calls(bound - 1);
52+
} else if bound > 0 {
53+
// "WIP: we still need to audit this part of the function"
54+
i_have_one_blessing_but_two_calls(bound - 2)
55+
//~^ direct_recursion
56+
}
57+
}

tests/ui/direct_recursion.stderr

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
error: this function contains a call to itself
2+
--> tests/ui/direct_recursion.rs:8:5
3+
|
4+
LL | i_call_myself_always();
5+
| ^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
note: the lint level is defined here
8+
--> tests/ui/direct_recursion.rs:1:9
9+
|
10+
LL | #![deny(clippy::direct_recursion)]
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^
12+
13+
error: this function contains a call to itself
14+
--> tests/ui/direct_recursion.rs:14:9
15+
|
16+
LL | i_call_myself_conditionally(false);
17+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
18+
19+
error: this function contains a call to itself
20+
--> tests/ui/direct_recursion.rs:54:9
21+
|
22+
LL | i_have_one_blessing_but_two_calls(bound - 2)
23+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
24+
25+
error: aborting due to 3 previous errors
26+

0 commit comments

Comments
 (0)