An Unfortunate Coercion
When we started Rust-clippy’s
eta-reduction lint,
we did not have in mind that calling a closure involves
deref coercions. This
leads to false positives wherever a closure that takes a reference to X
is
called with another type that Deref
s to X
(as can be seen
here
– the
over
function is called via a closure to coerce the std::ptr::P
smart pointers to
&[PathSegment]
slice refs.
Note that this aren’t exactly false positives – the closure is only calling
the function with its arguments – but due to the deref coercions, our suggestion
of removing the closure will actually lead to errors: Should we change
over(left, right, |l, r| is_exp_equal(l, r))
to
over(left, right, is_exp_equal)
, we get
src/eq_op.rs:71:5: 71:9 error: type mismatch: the type `fn(&syntax::ast::Expr,
&syntax::ast::Expr) -> bool {eq_op::is_exp_equal}` implements the trait `for<'r,
'r> core::ops::FnMut<(&'r syntax::ast::Expr, &'r syntax::ast::Expr)>`, but the
trait `for<'r, 'r> core::ops::FnMut<(&'r syntax::ptr::P<syntax::ast::Expr>, &'r
syntax::ptr::P<syntax::ast::Expr>)>` is required (expected struct
`syntax::ptr::P`, found struct `syntax::ast::Expr`) [E0281]
src/eq_op.rs:71 over(left, right, is_exp_equal)
^~~~
src/eq_op.rs:71:5: 71:9 note: required by `eq_op::over`
src/eq_op.rs:71 over(left, right, is_exp_equal)
^~~~
So, naturally I first tried to make the over(..)
function more generic over
Deref
, but I couldn’t get it to work, because the function is sometimes
called with a P<Vec<X>>
(P
is rustc’s owned smart pointer), and other times
with a Vec<P<X>>
, which both are silently coerced to &[X]
for over
. There
is so much magic going on here that even if there was a generic version of
over(..)
, I would not want to inflict our eta_reduction
lint in its current
state on anyone who fails to write that version.
(By the way, should you come up with a generic version that works, tell me to win internet points and bragging rights).
If the clippy code looks to complex, here is a simplified snippet with just a pointer-to-pointer to showcase the issue:
fn all<X, F>(x: &[X], y: &X, f: F) -> bool
where F: Fn(&X, &X) -> bool {
x.iter().all(|e| f(e, y))
}
fn below(x: &u8, y: &u8) -> bool { x < y }
fn main() {
let arr = [1u8, 2, 3];
let arr2 = [&1u8];
assert!(all(&arr, &5, |x, y| below(x, y)));
assert!(all(&arr, &5, below)); // look ma, no closure!
assert!(all(&arr2, &&2, |x, y| below(x, y))); //is coerced
//assert!(all(&arr2, &&2, below)); // leads to ERROR
}
Now due to how the eta_reduction
lint works, we have a problem: The lint
matches on the closure (as it should), but to weed out the false positives,
we need all expressions calling it. If any of them calls the closure with a
different type than the function takes, we should balk.
The problem is exacerbated by the fact that the closure can be bound within a
let-binding, loaned out to other functions, and taken, shaken, turned all
around, and I think you get the picture. Even worse, closures could be
pub
lic, in which case all bets are off (However, I suspect this to be a less
common use case).
However, closures can be used as function parameters – a very common case.
These functions usually take any of the Fn*
types. So a possible
approximation that would rid us of the false positives is to search for all
arguments of all function/method calls where the expression type is the type
of our closure and to omit the lint for those.
But even this crude approximation is a non-trivial undertaking. Also it would
effectively disable the lint (because barring our very synthetic test cases, a
lot of closures are actually used as an argument to some function, e.g.
Iterator::map(…)
, Option::and_then
etc. If we don’t find a better solution,
we might as well get rid of the lint (or at least allow
it by default and
change the warning to something more cautious).
Alas, today I’m empty-handed – I’ve given you the gordian knot, but no sword to slice it. Perhaps one day there shall be a solution.
In the meantime, feel free to discuss this at /r/rust or users.rust-lang.
Bonus: Not a solution to the coercion conundrum, but at least I managed to make the lint somewhat work, albeit in a reduced fashion: It now only lints closures that are given as arguments in a method call – within this construct, we can check if type adjustments have been made to any argument of the closure within the call, and in that case omit the lint. The code to check for type adjustments is:
fn is_adjusted(cx: &Context, e: &Expr) -> bool {
cx.tcx.tables.borrow().adjustments.get(&e.id).is_some()
}
... if args.iter().any(|arg| is_adjusted(cx, arg)) { return; } // omit lint