Skip to content

Commit 5b60388

Browse files
committed
Auto merge of #10930 - y21:issue9956, r=blyxyas,xFrednet
[`redundant_closure_call`]: handle nested closures Fixes #9956. This ended up being a much larger change than I'd thought, and I ended up having to pretty much rewrite it as a late lint pass, because it needs access to certain things that I don't think are available in early lint passes (e.g. getting the parent expr). I think this'll be required to fi-x #10922 anyway, so this is probably fine. (edit: had to write "fi-x" because "fix" makes github think that this PR fixes it, which it doesn't 😅 ) Previously, it would suggest changing `(|| || 42)()()` to `|| 42()`, which is a type error (it needs parens: `(|| 42)()`). In my opinion, though, the suggested fix should have really been `42`, so that's what this PR changes. changelog: [`redundant_closure_call`]: handle nested closures and rewrite as a late lint pass
2 parents f396004 + 3fe2478 commit 5b60388

File tree

5 files changed

+269
-51
lines changed

5 files changed

+269
-51
lines changed

clippy_lints/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
804804
store.register_early_pass(|| Box::new(int_plus_one::IntPlusOne));
805805
store.register_early_pass(|| Box::new(formatting::Formatting));
806806
store.register_early_pass(|| Box::new(misc_early::MiscEarlyLints));
807-
store.register_early_pass(|| Box::new(redundant_closure_call::RedundantClosureCall));
808807
store.register_late_pass(|_| Box::new(redundant_closure_call::RedundantClosureCall));
809808
store.register_early_pass(|| Box::new(unused_unit::UnusedUnit));
810809
store.register_late_pass(|_| Box::new(returns::Return));

clippy_lints/src/redundant_closure_call.rs

Lines changed: 120 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
use crate::rustc_lint::LintContext;
12
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
3+
use clippy_utils::get_parent_expr;
24
use clippy_utils::sugg::Sugg;
35
use if_chain::if_chain;
4-
use rustc_ast::ast;
5-
use rustc_ast::visit as ast_visit;
6-
use rustc_ast::visit::Visitor as AstVisitor;
76
use rustc_errors::Applicability;
87
use rustc_hir as hir;
98
use rustc_hir::intravisit as hir_visit;
109
use rustc_hir::intravisit::Visitor as HirVisitor;
11-
use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};
10+
use rustc_hir::intravisit::Visitor;
11+
use rustc_lint::{LateContext, LateLintPass};
1212
use rustc_middle::hir::nested_filter;
1313
use rustc_middle::lint::in_external_macro;
1414
use rustc_session::{declare_lint_pass, declare_tool_lint};
@@ -51,59 +51,136 @@ impl ReturnVisitor {
5151
}
5252
}
5353

54-
impl<'ast> ast_visit::Visitor<'ast> for ReturnVisitor {
55-
fn visit_expr(&mut self, ex: &'ast ast::Expr) {
56-
if let ast::ExprKind::Ret(_) | ast::ExprKind::Try(_) = ex.kind {
54+
impl<'tcx> Visitor<'tcx> for ReturnVisitor {
55+
fn visit_expr(&mut self, ex: &'tcx hir::Expr<'tcx>) {
56+
if let hir::ExprKind::Ret(_) | hir::ExprKind::Match(.., hir::MatchSource::TryDesugar) = ex.kind {
5757
self.found_return = true;
58+
} else {
59+
hir_visit::walk_expr(self, ex);
5860
}
61+
}
62+
}
5963

60-
ast_visit::walk_expr(self, ex);
64+
/// Checks if the body is owned by an async closure
65+
fn is_async_closure(body: &hir::Body<'_>) -> bool {
66+
if let hir::ExprKind::Closure(closure) = body.value.kind
67+
&& let [resume_ty] = closure.fn_decl.inputs
68+
&& let hir::TyKind::Path(hir::QPath::LangItem(hir::LangItem::ResumeTy, ..)) = resume_ty.kind
69+
{
70+
true
71+
} else {
72+
false
6173
}
6274
}
6375

64-
impl EarlyLintPass for RedundantClosureCall {
65-
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
76+
/// Tries to find the innermost closure:
77+
/// ```rust,ignore
78+
/// (|| || || || 42)()()()()
79+
/// ^^^^^^^^^^^^^^ given this nested closure expression
80+
/// ^^^^^ we want to return this closure
81+
/// ```
82+
/// It also has a parameter for how many steps to go in at most, so as to
83+
/// not take more closures than there are calls.
84+
fn find_innermost_closure<'tcx>(
85+
cx: &LateContext<'tcx>,
86+
mut expr: &'tcx hir::Expr<'tcx>,
87+
mut steps: usize,
88+
) -> Option<(&'tcx hir::Expr<'tcx>, &'tcx hir::FnDecl<'tcx>, hir::IsAsync)> {
89+
let mut data = None;
90+
91+
while let hir::ExprKind::Closure(closure) = expr.kind
92+
&& let body = cx.tcx.hir().body(closure.body)
93+
&& {
94+
let mut visitor = ReturnVisitor::new();
95+
visitor.visit_expr(body.value);
96+
!visitor.found_return
97+
}
98+
&& steps > 0
99+
{
100+
expr = body.value;
101+
data = Some((body.value, closure.fn_decl, if is_async_closure(body) {
102+
hir::IsAsync::Async
103+
} else {
104+
hir::IsAsync::NotAsync
105+
}));
106+
steps -= 1;
107+
}
108+
109+
data
110+
}
111+
112+
/// "Walks up" the chain of calls to find the outermost call expression, and returns the depth:
113+
/// ```rust,ignore
114+
/// (|| || || 3)()()()
115+
/// ^^ this is the call expression we were given
116+
/// ^^ this is what we want to return (and the depth is 3)
117+
/// ```
118+
fn get_parent_call_exprs<'tcx>(
119+
cx: &LateContext<'tcx>,
120+
mut expr: &'tcx hir::Expr<'tcx>,
121+
) -> (&'tcx hir::Expr<'tcx>, usize) {
122+
let mut depth = 1;
123+
while let Some(parent) = get_parent_expr(cx, expr)
124+
&& let hir::ExprKind::Call(recv, _) = parent.kind
125+
&& expr.span == recv.span
126+
{
127+
expr = parent;
128+
depth += 1;
129+
}
130+
(expr, depth)
131+
}
132+
133+
impl<'tcx> LateLintPass<'tcx> for RedundantClosureCall {
134+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
66135
if in_external_macro(cx.sess(), expr.span) {
67136
return;
68137
}
69-
if_chain! {
70-
if let ast::ExprKind::Call(ref paren, _) = expr.kind;
71-
if let ast::ExprKind::Paren(ref closure) = paren.kind;
72-
if let ast::ExprKind::Closure(box ast::Closure { ref asyncness, ref fn_decl, ref body, .. }) = closure.kind;
73-
then {
74-
let mut visitor = ReturnVisitor::new();
75-
visitor.visit_expr(body);
76-
if !visitor.found_return {
77-
span_lint_and_then(
78-
cx,
79-
REDUNDANT_CLOSURE_CALL,
80-
expr.span,
81-
"try not to call a closure in the expression where it is declared",
82-
|diag| {
83-
if fn_decl.inputs.is_empty() {
84-
let mut app = Applicability::MachineApplicable;
85-
let mut hint = Sugg::ast(cx, body, "..", closure.span.ctxt(), &mut app);
86-
87-
if asyncness.is_async() {
88-
// `async x` is a syntax error, so it becomes `async { x }`
89-
if !matches!(body.kind, ast::ExprKind::Block(_, _)) {
90-
hint = hint.blockify();
91-
}
92-
93-
hint = hint.asyncify();
94-
}
95-
96-
diag.span_suggestion(expr.span, "try doing something like", hint.to_string(), app);
138+
139+
if let hir::ExprKind::Call(recv, _) = expr.kind
140+
// don't lint if the receiver is a call, too.
141+
// we do this in order to prevent linting multiple times; consider:
142+
// `(|| || 1)()()`
143+
// ^^ we only want to lint for this call (but we walk up the calls to consider both calls).
144+
// without this check, we'd end up linting twice.
145+
&& !matches!(recv.kind, hir::ExprKind::Call(..))
146+
&& let (full_expr, call_depth) = get_parent_call_exprs(cx, expr)
147+
&& let Some((body, fn_decl, generator_kind)) = find_innermost_closure(cx, recv, call_depth)
148+
{
149+
span_lint_and_then(
150+
cx,
151+
REDUNDANT_CLOSURE_CALL,
152+
full_expr.span,
153+
"try not to call a closure in the expression where it is declared",
154+
|diag| {
155+
if fn_decl.inputs.is_empty() {
156+
let mut applicability = Applicability::MachineApplicable;
157+
let mut hint = Sugg::hir_with_context(cx, body, full_expr.span.ctxt(), "..", &mut applicability);
158+
159+
if generator_kind.is_async()
160+
&& let hir::ExprKind::Closure(closure) = body.kind
161+
{
162+
let async_closure_body = cx.tcx.hir().body(closure.body);
163+
164+
// `async x` is a syntax error, so it becomes `async { x }`
165+
if !matches!(async_closure_body.value.kind, hir::ExprKind::Block(_, _)) {
166+
hint = hint.blockify();
97167
}
98-
},
99-
);
168+
169+
hint = hint.asyncify();
170+
}
171+
172+
diag.span_suggestion(
173+
full_expr.span,
174+
"try doing something like",
175+
hint.maybe_par(),
176+
applicability
177+
);
178+
}
100179
}
101-
}
180+
);
102181
}
103182
}
104-
}
105183

106-
impl<'tcx> LateLintPass<'tcx> for RedundantClosureCall {
107184
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'_>) {
108185
fn count_closure_usage<'tcx>(
109186
cx: &LateContext<'tcx>,

tests/ui/redundant_closure_call_fixable.fixed

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#![feature(async_closure)]
44
#![warn(clippy::redundant_closure_call)]
55
#![allow(clippy::redundant_async_block)]
6+
#![allow(clippy::type_complexity)]
67
#![allow(unused)]
78

89
async fn something() -> u32 {
@@ -38,4 +39,50 @@ fn main() {
3839
};
3940
}
4041
m2!();
42+
issue9956();
43+
}
44+
45+
fn issue9956() {
46+
assert_eq!(43, 42);
47+
48+
// ... and some more interesting cases I've found while implementing the fix
49+
50+
// not actually immediately calling the closure:
51+
let a = (|| 42);
52+
dbg!(a());
53+
54+
// immediately calling it inside of a macro
55+
dbg!(42);
56+
57+
// immediately calling only one closure, so we can't remove the other ones
58+
let a = (|| || 123);
59+
dbg!(a()());
60+
61+
// nested async closures
62+
let a = async { 1 };
63+
let h = async { a.await };
64+
65+
// macro expansion tests
66+
macro_rules! echo {
67+
($e:expr) => {
68+
$e
69+
};
70+
}
71+
let a = 1;
72+
assert_eq!(a, 1);
73+
let a = 123;
74+
assert_eq!(a, 123);
75+
76+
// chaining calls, but not closures
77+
fn x() -> fn() -> fn() -> fn() -> i32 {
78+
|| || || 42
79+
}
80+
let _ = x()()()();
81+
82+
fn bar() -> fn(i32, i32) {
83+
foo
84+
}
85+
fn foo(_: i32, _: i32) {}
86+
bar()(42, 5);
87+
foo(42, 5);
4188
}

tests/ui/redundant_closure_call_fixable.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#![feature(async_closure)]
44
#![warn(clippy::redundant_closure_call)]
55
#![allow(clippy::redundant_async_block)]
6+
#![allow(clippy::type_complexity)]
67
#![allow(unused)]
78

89
async fn something() -> u32 {
@@ -38,4 +39,50 @@ fn main() {
3839
};
3940
}
4041
m2!();
42+
issue9956();
43+
}
44+
45+
fn issue9956() {
46+
assert_eq!((|| || 43)()(), 42);
47+
48+
// ... and some more interesting cases I've found while implementing the fix
49+
50+
// not actually immediately calling the closure:
51+
let a = (|| 42);
52+
dbg!(a());
53+
54+
// immediately calling it inside of a macro
55+
dbg!((|| 42)());
56+
57+
// immediately calling only one closure, so we can't remove the other ones
58+
let a = (|| || || 123)();
59+
dbg!(a()());
60+
61+
// nested async closures
62+
let a = (|| || || || async || 1)()()()()();
63+
let h = async { a.await };
64+
65+
// macro expansion tests
66+
macro_rules! echo {
67+
($e:expr) => {
68+
$e
69+
};
70+
}
71+
let a = (|| echo!(|| echo!(|| 1)))()()();
72+
assert_eq!(a, 1);
73+
let a = (|| echo!((|| 123)))()();
74+
assert_eq!(a, 123);
75+
76+
// chaining calls, but not closures
77+
fn x() -> fn() -> fn() -> fn() -> i32 {
78+
|| || || 42
79+
}
80+
let _ = x()()()();
81+
82+
fn bar() -> fn(i32, i32) {
83+
foo
84+
}
85+
fn foo(_: i32, _: i32) {}
86+
bar()((|| || 42)()(), 5);
87+
foo((|| || 42)()(), 5);
4188
}

0 commit comments

Comments
 (0)