Skip to content

Commit 41be5d3

Browse files
committed
Rework redundant_closure
* Better track when a early-bound region appears when a late-bound region is required * Don't lint when the closure gives explicit types.
1 parent c7e6863 commit 41be5d3

File tree

5 files changed

+303
-115
lines changed

5 files changed

+303
-115
lines changed

clippy_lints/src/eta_reduction.rs

+181-114
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
22
use clippy_utils::higher::VecArgs;
33
use clippy_utils::source::snippet_opt;
4-
use clippy_utils::ty::is_type_diagnostic_item;
4+
use clippy_utils::ty::type_diagnostic_name;
55
use clippy_utils::usage::local_used_after_expr;
66
use clippy_utils::{get_enclosing_loop_or_closure, higher, path_to_local, path_to_local_id};
7-
use if_chain::if_chain;
87
use rustc_errors::Applicability;
98
use rustc_hir::def_id::DefId;
10-
use rustc_hir::{Expr, ExprKind, Param, PatKind, Unsafety};
9+
use rustc_hir::{BindingAnnotation, Expr, ExprKind, FnRetTy, Param, PatKind, TyKind, Unsafety};
1110
use rustc_lint::{LateContext, LateLintPass};
12-
use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow};
13-
use rustc_middle::ty::binding::BindingMode;
14-
use rustc_middle::ty::subst::Subst;
15-
use rustc_middle::ty::{self, ClosureKind, Ty, TypeFoldable};
11+
use rustc_middle::ty::subst::{GenericArg, GenericArgKind};
12+
use rustc_middle::ty::{self, ClosureKind, ClosureSubsts, FnSig, Region, RegionKind, Ty, TypeFoldable, TypeckResults};
1613
use rustc_session::{declare_lint_pass, declare_tool_lint};
1714
use rustc_span::symbol::sym;
15+
use rustc_target::spec::abi::Abi;
1816

1917
declare_clippy_lint! {
2018
/// ### What it does
@@ -73,14 +71,18 @@ declare_clippy_lint! {
7371
declare_lint_pass!(EtaReduction => [REDUNDANT_CLOSURE, REDUNDANT_CLOSURE_FOR_METHOD_CALLS]);
7472

7573
impl<'tcx> LateLintPass<'tcx> for EtaReduction {
74+
#[allow(clippy::too_many_lines)]
7675
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
77-
if expr.span.from_expansion() {
78-
return;
79-
}
80-
let body = match expr.kind {
81-
ExprKind::Closure(_, _, id, _, _) => cx.tcx.hir().body(id),
82-
_ => return,
76+
let body = if let ExprKind::Closure(_, decl, id, _, _) = expr.kind
77+
&& decl.inputs.iter().all(|ty| matches!(ty.kind, TyKind::Infer))
78+
&& matches!(decl.output, FnRetTy::DefaultReturn(_))
79+
&& !expr.span.from_expansion()
80+
{
81+
cx.tcx.hir().body(id)
82+
} else {
83+
return
8384
};
85+
8486
if body.value.span.from_expansion() {
8587
if body.params.is_empty() {
8688
if let Some(VecArgs::Vec(&[])) = higher::VecArgs::hir(cx, &body.value) {
@@ -100,122 +102,187 @@ impl<'tcx> LateLintPass<'tcx> for EtaReduction {
100102
return;
101103
}
102104

103-
let closure_ty = cx.typeck_results().expr_ty(expr);
104-
105-
if_chain!(
106-
if let ExprKind::Call(callee, args) = body.value.kind;
107-
if let ExprKind::Path(_) = callee.kind;
108-
if check_inputs(cx, body.params, args);
109-
let callee_ty = cx.typeck_results().expr_ty_adjusted(callee);
110-
let call_ty = cx.typeck_results().type_dependent_def_id(body.value.hir_id)
111-
.map_or(callee_ty, |id| cx.tcx.type_of(id));
112-
if check_sig(cx, closure_ty, call_ty);
113-
let substs = cx.typeck_results().node_substs(callee.hir_id);
114-
// This fixes some false positives that I don't entirely understand
115-
if substs.is_empty() || !cx.typeck_results().expr_ty(expr).has_late_bound_regions();
116-
// A type param function ref like `T::f` is not 'static, however
117-
// it is if cast like `T::f as fn()`. This seems like a rustc bug.
118-
if !substs.types().any(|t| matches!(t.kind(), ty::Param(_)));
119-
let callee_ty_unadjusted = cx.typeck_results().expr_ty(callee).peel_refs();
120-
if !is_type_diagnostic_item(cx, callee_ty_unadjusted, sym::Arc);
121-
if !is_type_diagnostic_item(cx, callee_ty_unadjusted, sym::Rc);
122-
then {
123-
span_lint_and_then(cx, REDUNDANT_CLOSURE, expr.span, "redundant closure", |diag| {
124-
if let Some(mut snippet) = snippet_opt(cx, callee.span) {
125-
if_chain! {
126-
if let ty::Closure(_, substs) = callee_ty.peel_refs().kind();
127-
if substs.as_closure().kind() == ClosureKind::FnMut;
128-
if get_enclosing_loop_or_closure(cx.tcx, expr).is_some()
129-
|| path_to_local(callee).map_or(false, |l| local_used_after_expr(cx, l, callee));
105+
let typeck = cx.typeck_results();
106+
let closure = if let ty::Closure(_, closure_subs) = typeck.expr_ty(expr).kind() {
107+
closure_subs.as_closure()
108+
} else {
109+
return;
110+
};
130111

131-
then {
132-
// Mutable closure is used after current expr; we cannot consume it.
133-
snippet = format!("&mut {}", snippet);
112+
match body.value.kind {
113+
ExprKind::Call(callee, args)
114+
if matches!(callee.kind, ExprKind::Path(..))
115+
&& !matches!(
116+
type_diagnostic_name(cx, typeck.expr_ty(callee).peel_refs()),
117+
Some(sym::Arc | sym::Rc)
118+
)
119+
&& check_inputs(typeck, body.params, args) =>
120+
{
121+
let sig = match *typeck.expr_ty_adjusted(callee).kind() {
122+
ty::FnDef(def, _) => cx.tcx.fn_sig(def).skip_binder(),
123+
ty::FnPtr(sig) => sig.skip_binder(),
124+
ty::Closure(_, subs) => cx
125+
.tcx
126+
.signature_unclosure(subs.as_closure().sig(), Unsafety::Normal)
127+
.skip_binder(),
128+
_ => {
129+
if typeck.type_dependent_def_id(body.value.hir_id).is_some()
130+
&& let subs = typeck.node_substs(body.value.hir_id)
131+
&& let output = typeck.expr_ty(&body.value)
132+
&& let ty::Tuple(tys) = *subs.type_at(1).kind()
133+
{
134+
cx.tcx.mk_fn_sig(tys.into_iter(), output, false, Unsafety::Normal, Abi::Rust)
135+
} else {
136+
return;
137+
}
138+
},
139+
};
140+
if check_sig(cx, closure, sig)
141+
// Given some trait fn `fn f() -> ()` and some type `T: Trait`, `T::f` is not
142+
// `'static` unless `T: 'static`. The cast `T::f as fn()` will, however, result
143+
// in a type which is `'static`.
144+
// For now ignore all callee types which reference a type parameter.
145+
&& !typeck.node_substs(callee.hir_id).types().any(|t| matches!(t.kind(), ty::Param(_)))
146+
{
147+
span_lint_and_then(
148+
cx,
149+
REDUNDANT_CLOSURE,
150+
expr.span,
151+
"redundant closure",
152+
|diag| {
153+
if let Some(mut snippet) = snippet_opt(cx, callee.span) {
154+
if let ty::Closure(_, substs) = *typeck.expr_ty(callee).peel_refs().kind()
155+
&& substs.as_closure().kind() == ClosureKind::FnMut
156+
&& (
157+
get_enclosing_loop_or_closure(cx.tcx, expr).is_some()
158+
|| path_to_local(callee)
159+
.map_or(false, |l| local_used_after_expr(cx, l, callee))
160+
)
161+
{
162+
// Mutable closure is used after current expr; we cannot consume it.
163+
snippet = format!("&mut {}", snippet);
164+
}
165+
diag.span_suggestion(
166+
expr.span,
167+
"replace the closure with the function itself",
168+
snippet,
169+
Applicability::MachineApplicable,
170+
);
134171
}
135172
}
136-
diag.span_suggestion(
137-
expr.span,
138-
"replace the closure with the function itself",
139-
snippet,
140-
Applicability::MachineApplicable,
141-
);
142-
}
143-
});
144-
}
145-
);
146-
147-
if_chain!(
148-
if let ExprKind::MethodCall(path, args, _) = body.value.kind;
149-
if check_inputs(cx, body.params, args);
150-
let method_def_id = cx.typeck_results().type_dependent_def_id(body.value.hir_id).unwrap();
151-
let substs = cx.typeck_results().node_substs(body.value.hir_id);
152-
let call_ty = cx.tcx.type_of(method_def_id).subst(cx.tcx, substs);
153-
if check_sig(cx, closure_ty, call_ty);
154-
then {
155-
span_lint_and_then(cx, REDUNDANT_CLOSURE_FOR_METHOD_CALLS, expr.span, "redundant closure", |diag| {
156-
let name = get_ufcs_type_name(cx, method_def_id);
157-
diag.span_suggestion(
173+
);
174+
}
175+
},
176+
ExprKind::MethodCall(path, args, _) if check_inputs(typeck, body.params, args) => {
177+
if let Some(method_def_id) = typeck.type_dependent_def_id(body.value.hir_id)
178+
&& check_sig(cx, closure, cx.tcx.fn_sig(method_def_id).skip_binder())
179+
{
180+
span_lint_and_then(
181+
cx,
182+
REDUNDANT_CLOSURE_FOR_METHOD_CALLS,
158183
expr.span,
159-
"replace the closure with the method itself",
160-
format!("{}::{}", name, path.ident.name),
161-
Applicability::MachineApplicable,
184+
"redundant closure",
185+
|diag| {
186+
let name = get_ufcs_type_name(cx, method_def_id);
187+
diag.span_suggestion(
188+
expr.span,
189+
"replace the closure with the method itself",
190+
format!("{}::{}", name, path.ident.name),
191+
Applicability::MachineApplicable,
192+
);
193+
},
162194
);
163-
})
164-
}
165-
);
195+
}
196+
},
197+
_ => (),
198+
}
166199
}
167200
}
168201

169-
fn check_inputs(cx: &LateContext<'_>, params: &[Param<'_>], call_args: &[Expr<'_>]) -> bool {
170-
if params.len() != call_args.len() {
171-
return false;
202+
fn check_inputs(typeck: &TypeckResults<'_>, params: &[Param<'_>], call_args: &[Expr<'_>]) -> bool {
203+
params.len() == call_args.len()
204+
&& params.iter().zip(call_args).all(|(p, arg)| {
205+
matches!(
206+
p.pat.kind,PatKind::Binding(BindingAnnotation::Unannotated, id, _, None)
207+
if path_to_local_id(arg, id)
208+
)
209+
// Only allow adjustments which change regions (i.e. re-borrowing).
210+
&& typeck
211+
.expr_adjustments(arg)
212+
.last()
213+
.map_or(true, |a| a.target == typeck.expr_ty(arg))
214+
})
215+
}
216+
217+
fn check_sig<'tcx>(cx: &LateContext<'tcx>, closure: ClosureSubsts<'tcx>, call_sig: FnSig<'_>) -> bool {
218+
call_sig.unsafety == Unsafety::Normal
219+
&& !has_late_bound_to_non_late_bound_regions(
220+
cx.tcx
221+
.signature_unclosure(closure.sig(), Unsafety::Normal)
222+
.skip_binder(),
223+
call_sig,
224+
)
225+
}
226+
227+
/// This walks through both signatures and checks for any time a late-bound region is expected by an
228+
/// `impl Fn` type, but the target signature does not have a late-bound region in the same position.
229+
///
230+
/// This is needed because rustc is unable to late bind early-bound regions in a function signature.
231+
fn has_late_bound_to_non_late_bound_regions(from_sig: FnSig<'_>, to_sig: FnSig<'_>) -> bool {
232+
fn check_region(from_region: Region<'_>, to_region: Region<'_>) -> bool {
233+
matches!(from_region.kind(), RegionKind::ReLateBound(..))
234+
&& !matches!(to_region.kind(), RegionKind::ReLateBound(..))
172235
}
173-
let binding_modes = cx.typeck_results().pat_binding_modes();
174-
std::iter::zip(params, call_args).all(|(param, arg)| {
175-
match param.pat.kind {
176-
PatKind::Binding(_, id, ..) if path_to_local_id(arg, id) => {},
177-
_ => return false,
178-
}
179-
// checks that parameters are not bound as `ref` or `ref mut`
180-
if let Some(BindingMode::BindByReference(_)) = binding_modes.get(param.pat.hir_id) {
181-
return false;
182-
}
183236

184-
match *cx.typeck_results().expr_adjustments(arg) {
185-
[] => true,
186-
[
187-
Adjustment {
188-
kind: Adjust::Deref(None),
189-
..
237+
fn check_subs(from_subs: &[GenericArg<'_>], to_subs: &[GenericArg<'_>]) -> bool {
238+
if from_subs.len() != to_subs.len() {
239+
return true;
240+
}
241+
for (from_arg, to_arg) in to_subs.iter().zip(from_subs) {
242+
match (from_arg.unpack(), to_arg.unpack()) {
243+
(GenericArgKind::Lifetime(from_region), GenericArgKind::Lifetime(to_region)) => {
244+
if check_region(from_region, to_region) {
245+
return true;
246+
}
190247
},
191-
Adjustment {
192-
kind: Adjust::Borrow(AutoBorrow::Ref(_, mu2)),
193-
..
248+
(GenericArgKind::Type(from_ty), GenericArgKind::Type(to_ty)) => {
249+
if check_ty(from_ty, to_ty) {
250+
return true;
251+
}
194252
},
195-
] => {
196-
// re-borrow with the same mutability is allowed
197-
let ty = cx.typeck_results().expr_ty(arg);
198-
matches!(*ty.kind(), ty::Ref(.., mu1) if mu1 == mu2.into())
199-
},
200-
_ => false,
253+
(GenericArgKind::Const(_), GenericArgKind::Const(_)) => (),
254+
_ => return true,
255+
}
201256
}
202-
})
203-
}
204-
205-
fn check_sig<'tcx>(cx: &LateContext<'tcx>, closure_ty: Ty<'tcx>, call_ty: Ty<'tcx>) -> bool {
206-
let call_sig = call_ty.fn_sig(cx.tcx);
207-
if call_sig.unsafety() == Unsafety::Unsafe {
208-
return false;
257+
false
209258
}
210-
if !closure_ty.has_late_bound_regions() {
211-
return true;
259+
260+
fn check_ty(from_ty: Ty<'_>, to_ty: Ty<'_>) -> bool {
261+
match (from_ty.kind(), to_ty.kind()) {
262+
(&ty::Adt(_, from_subs), &ty::Adt(_, to_subs)) => check_subs(from_subs, to_subs),
263+
(&ty::Array(from_ty, _), &ty::Array(to_ty, _)) | (&ty::Slice(from_ty), &ty::Slice(to_ty)) => {
264+
check_ty(from_ty, to_ty)
265+
},
266+
(&ty::Ref(from_region, from_ty, _), &ty::Ref(to_region, to_ty, _)) => {
267+
check_region(from_region, to_region) || check_ty(from_ty, to_ty)
268+
},
269+
(&ty::Tuple(from_tys), &ty::Tuple(to_tys)) => {
270+
from_tys.len() != to_tys.len()
271+
|| from_tys
272+
.iter()
273+
.zip(to_tys)
274+
.any(|(from_ty, to_ty)| check_ty(from_ty, to_ty))
275+
},
276+
_ => from_ty.has_late_bound_regions(),
277+
}
212278
}
213-
let substs = match closure_ty.kind() {
214-
ty::Closure(_, substs) => substs,
215-
_ => return false,
216-
};
217-
let closure_sig = cx.tcx.signature_unclosure(substs.as_closure().sig(), Unsafety::Normal);
218-
cx.tcx.erase_late_bound_regions(closure_sig) == cx.tcx.erase_late_bound_regions(call_sig)
279+
280+
assert!(from_sig.inputs_and_output.len() == to_sig.inputs_and_output.len());
281+
from_sig
282+
.inputs_and_output
283+
.iter()
284+
.zip(to_sig.inputs_and_output)
285+
.any(|(from_ty, to_ty)| check_ty(from_ty, to_ty))
219286
}
220287

221288
fn get_ufcs_type_name(cx: &LateContext<'_>, method_def_id: DefId) -> String {

clippy_utils/src/ty.rs

+5
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@ pub fn is_type_lang_item(cx: &LateContext<'_>, ty: Ty<'_>, lang_item: hir::LangI
303303
}
304304
}
305305

306+
/// Gets the diagnostic name of the type, if it has one
307+
pub fn type_diagnostic_name(cx: &LateContext<'_>, ty: Ty<'_>) -> Option<Symbol> {
308+
ty.ty_adt_def().and_then(|adt| cx.tcx.get_diagnostic_name(adt.did()))
309+
}
310+
306311
/// Return `true` if the passed `typ` is `isize` or `usize`.
307312
pub fn is_isize_or_usize(typ: Ty<'_>) -> bool {
308313
matches!(typ.kind(), ty::Int(IntTy::Isize) | ty::Uint(UintTy::Usize))

0 commit comments

Comments
 (0)