Skip to content

Commit bbadce9

Browse files
Fixed ICE introduced in rust-lang#12004
Issue: in rust-lang#12004, we emit a lint for `filter(Option::is_some)`. If the parent expression is a `.map` we don't emit that lint as there exists a more specialized lint for that. The ICE introduced in rust-lang#12004 is a consequence of the assumption that a parent expression after a filter would be a method call with the filter call being the receiver. However, it is entirely possible to have a closure of the form ``` || { vec![Some(1), None].into_iter().filter(Option::is_some) } ``` The previous implementation looked at the parent expression; namely the closure, and tried to check the parameters by indexing [0] on an empty list. This commit is an overhaul of the lint with significantly more FP tests and checks. Impl details: 1. We verify that the filter method we are in is a proper trait method to avoid FPs. 2. We check that the parent expression is not a map by checking whether it exists; if is a trait method; and then a method call. 3. We check that we don't have comments in the span. 4. We verify that we are in an Iterator of Option and Result. 5. We check the contents of the filter. 1. For closures we peel it. If it is not a single expression, we don't lint. 2. For paths, we do a typecheck to avoid FPs for types that impl functions with the same names. 3. For calls, we verify the type, via the path, and that the param of the closure is the single argument to the call. 4. For method calls we verify that the receiver is the parameter of the closure. Since we handle single, non-block exprs, the parameter can't be shadowed, so no FP. This commit also adds additional FP tests.
1 parent 9eb2b22 commit bbadce9

8 files changed

+1092
-101
lines changed

clippy_lints/src/methods/filter_map.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ fn is_method(cx: &LateContext<'_>, expr: &hir::Expr<'_>, method_name: Symbol) ->
2626
hir::ExprKind::Closure(&hir::Closure { body, .. }) => {
2727
let body = cx.tcx.hir().body(body);
2828
let closure_expr = peel_blocks(body.value);
29-
let arg_id = body.params[0].pat.hir_id;
3029
match closure_expr.kind {
3130
hir::ExprKind::MethodCall(hir::PathSegment { ident, .. }, receiver, ..) => {
3231
if ident.name == method_name
3332
&& let hir::ExprKind::Path(path) = &receiver.kind
3433
&& let Res::Local(ref local) = cx.qpath_res(path, receiver.hir_id)
34+
&& !body.params.is_empty()
3535
{
36+
let arg_id = body.params[0].pat.hir_id;
3637
return arg_id == *local;
3738
}
3839
false
Lines changed: 158 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,197 @@
1+
use clippy_utils::ty::get_iterator_item_ty;
2+
use hir::ExprKind;
13
use rustc_lint::{LateContext, LintContext};
24

35
use super::{ITER_FILTER_IS_OK, ITER_FILTER_IS_SOME};
46

57
use clippy_utils::diagnostics::span_lint_and_sugg;
68
use clippy_utils::source::{indent_of, reindent_multiline};
7-
use clippy_utils::{is_trait_method, peel_blocks, span_contains_comment};
9+
use clippy_utils::{get_parent_expr, is_trait_method, peel_blocks, span_contains_comment};
810
use rustc_errors::Applicability;
911
use rustc_hir as hir;
10-
use rustc_hir::def::Res;
1112
use rustc_hir::QPath;
12-
use rustc_span::symbol::{sym, Symbol};
13+
use rustc_span::symbol::{sym, Ident, Symbol};
1314
use rustc_span::Span;
1415
use std::borrow::Cow;
1516

16-
fn is_method(cx: &LateContext<'_>, expr: &hir::Expr<'_>, method_name: Symbol) -> bool {
17-
match &expr.kind {
18-
hir::ExprKind::Path(QPath::TypeRelative(_, mname)) => mname.ident.name == method_name,
19-
hir::ExprKind::Path(QPath::Resolved(_, segments)) => {
20-
segments.segments.last().unwrap().ident.name == method_name
17+
///
18+
/// Returns true if the expression is a method call to `method_name`
19+
/// e.g. `a.method_name()` or `Option::method_name`.
20+
///
21+
/// The type-checker verifies for us that the method accepts the right kind of items
22+
/// (e.g. `Option::is_some` accepts `Option<_>`), so we don't need to check that.
23+
///
24+
/// How to capture each case:
25+
///
26+
/// `.filter(|a| { std::option::Option::is_some(a) })`
27+
/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ <- this is a closure, getting unwrapped and
28+
/// recursively checked.
29+
/// `std::option::Option::is_some(a)`
30+
/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ <- this is a call. It unwraps to a path with
31+
/// `QPath::TypeRelative`. Since this is a type relative path, we need to check the method name, the
32+
/// type, and that the parameter of the closure is passed in the call. This part is the dual of
33+
/// `receiver.method_name()` below.
34+
///
35+
/// `filter(std::option::Option::is_some);`
36+
/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ <- this is a type relative path, like above, we check the
37+
/// type and the method name.
38+
///
39+
/// `filter(|a| a.is_some());`
40+
/// ^^^^^^^^^^^^^^^ <- this is a method call inside a closure,
41+
/// we check that the parameter of the closure is the receiver of the method call and don't allow
42+
/// any other parameters.
43+
fn is_method(
44+
cx: &LateContext<'_>,
45+
expr: &hir::Expr<'_>,
46+
type_symbol: Symbol,
47+
method_name: Symbol,
48+
params: &[&hir::Pat<'_>],
49+
) -> bool {
50+
fn pat_is_recv(ident: Ident, param: &hir::Pat<'_>) -> bool {
51+
match param.kind {
52+
hir::PatKind::Binding(_, _, other, _) => ident == other,
53+
hir::PatKind::Ref(pat, _) => pat_is_recv(ident, pat),
54+
_ => false,
55+
}
56+
}
57+
match expr.kind {
58+
hir::ExprKind::MethodCall(hir::PathSegment { ident, .. }, recv, ..) => {
59+
// compare the identifier of the receiver to the parameter
60+
// we are in a filter => closure has a single parameter and a single, non-block
61+
// expression, this means that the parameter shadows all outside variables with
62+
// the same name => avoid FPs. If the parameter is not the receiver, then this hits
63+
// outside variables => avoid FP
64+
if ident.name == method_name
65+
&& let ExprKind::Path(QPath::Resolved(None, path)) = recv.kind
66+
&& let &[seg] = path.segments
67+
&& params.iter().any(|p| pat_is_recv(seg.ident, p))
68+
{
69+
return true;
70+
}
71+
false
72+
},
73+
// This is used to check for complete paths via `|a| std::option::Option::is_some(a)`
74+
// this then unwraps to a path with `QPath::TypeRelative`
75+
// we pass the params as they've been passed to the current call through the closure
76+
hir::ExprKind::Call(expr, [param]) => {
77+
// this will hit the `QPath::TypeRelative` case and check that the method name is correct
78+
if is_method(cx, expr, type_symbol, method_name, params)
79+
// we then check that this is indeed passing the parameter of the closure
80+
&& let ExprKind::Path(QPath::Resolved(None, path)) = param.kind
81+
&& let &[seg] = path.segments
82+
&& params.iter().any(|p| pat_is_recv(seg.ident, p))
83+
{
84+
return true;
85+
}
86+
false
87+
},
88+
hir::ExprKind::Path(QPath::TypeRelative(ty, mname)) => {
89+
let ty = cx.typeck_results().node_type(ty.hir_id);
90+
if let Some(did) = cx.tcx.get_diagnostic_item(type_symbol)
91+
&& ty.ty_adt_def() == cx.tcx.type_of(did).skip_binder().ty_adt_def()
92+
{
93+
return mname.ident.name == method_name;
94+
}
95+
false
2196
},
22-
hir::ExprKind::MethodCall(segment, _, _, _) => segment.ident.name == method_name,
2397
hir::ExprKind::Closure(&hir::Closure { body, .. }) => {
2498
let body = cx.tcx.hir().body(body);
2599
let closure_expr = peel_blocks(body.value);
26-
let arg_id = body.params[0].pat.hir_id;
27-
match closure_expr.kind {
28-
hir::ExprKind::MethodCall(hir::PathSegment { ident, .. }, receiver, ..) => {
29-
if ident.name == method_name
30-
&& let hir::ExprKind::Path(path) = &receiver.kind
31-
&& let Res::Local(ref local) = cx.qpath_res(path, receiver.hir_id)
32-
{
33-
return arg_id == *local;
34-
}
35-
false
36-
},
37-
_ => false,
38-
}
100+
let params = body.params.iter().map(|param| param.pat).collect::<Vec<_>>();
101+
is_method(cx, closure_expr, type_symbol, method_name, params.as_slice())
39102
},
40103
_ => false,
41104
}
42105
}
43106

44107
fn parent_is_map(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool {
45-
if let hir::Node::Expr(parent_expr) = cx.tcx.hir().get_parent(expr.hir_id) {
46-
is_method(cx, parent_expr, rustc_span::sym::map)
47-
} else {
48-
false
108+
if let Some(expr) = get_parent_expr(cx, expr)
109+
&& is_trait_method(cx, expr, sym::Iterator)
110+
&& let hir::ExprKind::MethodCall(path, _, _, _) = expr.kind
111+
&& path.ident.name == rustc_span::sym::map
112+
{
113+
return true;
49114
}
115+
false
50116
}
51117

52-
#[allow(clippy::too_many_arguments)]
53-
pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, filter_arg: &hir::Expr<'_>, filter_span: Span) {
54-
let is_iterator = is_trait_method(cx, expr, sym::Iterator);
55-
let parent_is_not_map = !parent_is_map(cx, expr);
118+
enum FilterType {
119+
IsSome,
120+
IsOk,
121+
}
56122

57-
if is_iterator
58-
&& parent_is_not_map
59-
&& is_method(cx, filter_arg, sym!(is_some))
60-
&& !span_contains_comment(cx.sess().source_map(), filter_span.with_hi(expr.span.hi()))
123+
/// Returns the `FilterType` of the expression if it is a filter over an Iter<Option> or
124+
/// Iter<Result> with the parent expression not being a map, and not having a comment in the span of
125+
/// the filter. If it is not a filter over an Iter<Option> or Iter<Result> then it returns None
126+
///
127+
/// How this is done:
128+
/// 1. we know that this is invoked in a method call with `filter` as the method name via `mod.rs`
129+
/// 2. we check that we are in a trait method. Therefore we are in an
130+
/// `(x as Iterator).filter({filter_arg})` method call.
131+
/// 3. we check that the parent expression is not a map. This is because we don't want to lint
132+
/// twice, and we already have a specialized lint for that.
133+
/// 4. we check that the span of the filter does not contain a comment.
134+
/// 5. we get the type of the `Item` in the `Iterator`, and compare against the type of Option and
135+
/// Result.
136+
/// 6. we finally check the contents of the filter argument to see if it is a call to `is_some` or
137+
/// `is_ok`.
138+
/// 7. if all of the above are true, then we return the `FilterType`
139+
fn expression_type(
140+
cx: &LateContext<'_>,
141+
expr: &hir::Expr<'_>,
142+
filter_arg: &hir::Expr<'_>,
143+
filter_span: Span,
144+
) -> Option<FilterType> {
145+
if !is_trait_method(cx, expr, sym::Iterator)
146+
|| parent_is_map(cx, expr)
147+
|| span_contains_comment(cx.sess().source_map(), filter_span.with_hi(expr.span.hi()))
61148
{
62-
span_lint_and_sugg(
149+
return None;
150+
}
151+
if let hir::ExprKind::MethodCall(_, receiver, _, _) = expr.kind
152+
&& let receiver_ty = cx.typeck_results().expr_ty(receiver)
153+
&& let Some(iter_item_ty) = get_iterator_item_ty(cx, receiver_ty)
154+
{
155+
if let Some(opt_defid) = cx.tcx.get_diagnostic_item(sym::Option)
156+
&& let opt_ty = cx.tcx.type_of(opt_defid).skip_binder()
157+
&& iter_item_ty.ty_adt_def() == opt_ty.ty_adt_def()
158+
&& is_method(cx, filter_arg, sym::Option, sym!(is_some), &[])
159+
{
160+
return Some(FilterType::IsSome);
161+
}
162+
163+
if let Some(opt_defid) = cx.tcx.get_diagnostic_item(sym::Result)
164+
&& let opt_ty = cx.tcx.type_of(opt_defid).skip_binder()
165+
&& iter_item_ty.ty_adt_def() == opt_ty.ty_adt_def()
166+
&& is_method(cx, filter_arg, sym::Result, sym!(is_ok), &[])
167+
{
168+
return Some(FilterType::IsOk);
169+
}
170+
}
171+
None
172+
}
173+
174+
pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, filter_arg: &hir::Expr<'_>, filter_span: Span) {
175+
// we are in a filter inside an iterator
176+
match expression_type(cx, expr, filter_arg, filter_span) {
177+
None => (),
178+
Some(FilterType::IsOk) => span_lint_and_sugg(
63179
cx,
64-
ITER_FILTER_IS_SOME,
180+
ITER_FILTER_IS_OK,
65181
filter_span.with_hi(expr.span.hi()),
66-
"`filter` for `is_some` on iterator over `Option`",
182+
"`filter` for `is_ok` on iterator over `Result`s",
67183
"consider using `flatten` instead",
68184
reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, filter_span)).into_owned(),
69185
Applicability::HasPlaceholders,
70-
);
71-
}
72-
if is_iterator
73-
&& parent_is_not_map
74-
&& is_method(cx, filter_arg, sym!(is_ok))
75-
&& !span_contains_comment(cx.sess().source_map(), filter_span.with_hi(expr.span.hi()))
76-
{
77-
span_lint_and_sugg(
186+
),
187+
Some(FilterType::IsSome) => span_lint_and_sugg(
78188
cx,
79-
ITER_FILTER_IS_OK,
189+
ITER_FILTER_IS_SOME,
80190
filter_span.with_hi(expr.span.hi()),
81-
"`filter` for `is_ok` on iterator over `Result`s",
191+
"`filter` for `is_some` on iterator over `Option`",
82192
"consider using `flatten` instead",
83193
reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, filter_span)).into_owned(),
84194
Applicability::HasPlaceholders,
85-
);
195+
),
86196
}
87197
}

0 commit comments

Comments
 (0)