|
| 1 | +use clippy_utils::ty::get_iterator_item_ty; |
| 2 | +use hir::ExprKind; |
1 | 3 | use rustc_lint::{LateContext, LintContext};
|
2 | 4 |
|
3 | 5 | use super::{ITER_FILTER_IS_OK, ITER_FILTER_IS_SOME};
|
4 | 6 |
|
5 | 7 | use clippy_utils::diagnostics::span_lint_and_sugg;
|
6 | 8 | 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}; |
8 | 10 | use rustc_errors::Applicability;
|
9 | 11 | use rustc_hir as hir;
|
10 |
| -use rustc_hir::def::Res; |
11 | 12 | use rustc_hir::QPath;
|
12 |
| -use rustc_span::symbol::{sym, Symbol}; |
| 13 | +use rustc_span::symbol::{sym, Ident, Symbol}; |
13 | 14 | use rustc_span::Span;
|
14 | 15 | use std::borrow::Cow;
|
15 | 16 |
|
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 |
21 | 96 | },
|
22 |
| - hir::ExprKind::MethodCall(segment, _, _, _) => segment.ident.name == method_name, |
23 | 97 | hir::ExprKind::Closure(&hir::Closure { body, .. }) => {
|
24 | 98 | let body = cx.tcx.hir().body(body);
|
25 | 99 | 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()) |
39 | 102 | },
|
40 | 103 | _ => false,
|
41 | 104 | }
|
42 | 105 | }
|
43 | 106 |
|
44 | 107 | 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; |
49 | 114 | }
|
| 115 | + false |
50 | 116 | }
|
51 | 117 |
|
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 | +} |
56 | 122 |
|
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())) |
61 | 148 | {
|
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( |
63 | 179 | cx,
|
64 |
| - ITER_FILTER_IS_SOME, |
| 180 | + ITER_FILTER_IS_OK, |
65 | 181 | 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", |
67 | 183 | "consider using `flatten` instead",
|
68 | 184 | reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, filter_span)).into_owned(),
|
69 | 185 | 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( |
78 | 188 | cx,
|
79 |
| - ITER_FILTER_IS_OK, |
| 189 | + ITER_FILTER_IS_SOME, |
80 | 190 | 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`", |
82 | 192 | "consider using `flatten` instead",
|
83 | 193 | reindent_multiline(Cow::Borrowed("flatten()"), true, indent_of(cx, filter_span)).into_owned(),
|
84 | 194 | Applicability::HasPlaceholders,
|
85 |
| - ); |
| 195 | + ), |
86 | 196 | }
|
87 | 197 | }
|
0 commit comments