|
1 |
| -use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; |
| 1 | +use clippy_utils::diagnostics::span_lint_and_then; |
2 | 2 | use clippy_utils::msrvs::{self, Msrv};
|
3 |
| -use clippy_utils::source::snippet; |
4 |
| -use clippy_utils::ty::is_type_diagnostic_item; |
5 |
| -use clippy_utils::usage::mutated_variables; |
| 3 | +use clippy_utils::source::snippet_with_applicability; |
| 4 | +use clippy_utils::ty::{is_copy, is_type_diagnostic_item}; |
| 5 | +use rustc_data_structures::fx::FxHashSet; |
6 | 6 | use rustc_errors::Applicability;
|
7 |
| -use rustc_hir as hir; |
| 7 | +use rustc_hir::def::Res; |
| 8 | +use rustc_hir::intravisit::{Visitor, walk_path}; |
| 9 | +use rustc_hir::{ExprKind, HirId, Node, PatKind, Path, QPath}; |
8 | 10 | use rustc_lint::LateContext;
|
9 |
| -use rustc_span::symbol::sym; |
| 11 | +use rustc_middle::hir::nested_filter; |
| 12 | +use rustc_span::{Span, sym}; |
| 13 | +use std::ops::ControlFlow; |
10 | 14 |
|
11 | 15 | use super::MAP_UNWRAP_OR;
|
12 | 16 |
|
13 |
| -/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s |
14 |
| -/// |
15 |
| -/// Returns true if the lint was emitted |
| 17 | +/// lint use of `map().unwrap_or()` for `Option`s and `Result`s |
| 18 | +#[expect(clippy::too_many_arguments)] |
16 | 19 | pub(super) fn check<'tcx>(
|
17 | 20 | cx: &LateContext<'tcx>,
|
18 |
| - expr: &'tcx hir::Expr<'_>, |
19 |
| - recv: &'tcx hir::Expr<'_>, |
20 |
| - map_arg: &'tcx hir::Expr<'_>, |
21 |
| - unwrap_arg: &'tcx hir::Expr<'_>, |
| 21 | + expr: &rustc_hir::Expr<'_>, |
| 22 | + recv: &rustc_hir::Expr<'_>, |
| 23 | + map_arg: &'tcx rustc_hir::Expr<'_>, |
| 24 | + unwrap_recv: &rustc_hir::Expr<'_>, |
| 25 | + unwrap_arg: &'tcx rustc_hir::Expr<'_>, |
| 26 | + map_span: Span, |
22 | 27 | msrv: Msrv,
|
23 |
| -) -> bool { |
24 |
| - // lint if the caller of `map()` is an `Option` or a `Result`. |
| 28 | +) { |
25 | 29 | let is_option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option);
|
26 | 30 | let is_result = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result);
|
27 | 31 |
|
28 |
| - if is_result && !msrv.meets(cx, msrvs::RESULT_MAP_OR_ELSE) { |
29 |
| - return false; |
| 32 | + if is_result && !msrv.meets(cx, msrvs::RESULT_MAP_OR) { |
| 33 | + return; |
30 | 34 | }
|
31 | 35 |
|
| 36 | + // lint if the caller of `map()` is an `Option` |
32 | 37 | if is_option || is_result {
|
33 |
| - // Don't make a suggestion that may fail to compile due to mutably borrowing |
34 |
| - // the same variable twice. |
35 |
| - let map_mutated_vars = mutated_variables(recv, cx); |
36 |
| - let unwrap_mutated_vars = mutated_variables(unwrap_arg, cx); |
37 |
| - if let (Some(map_mutated_vars), Some(unwrap_mutated_vars)) = (map_mutated_vars, unwrap_mutated_vars) { |
38 |
| - if map_mutated_vars.intersection(&unwrap_mutated_vars).next().is_some() { |
39 |
| - return false; |
| 38 | + if !is_copy(cx, cx.typeck_results().expr_ty(unwrap_arg)) { |
| 39 | + // Replacing `.map(<f>).unwrap_or(<a>)` with `.map_or(<a>, <f>)` can sometimes lead to |
| 40 | + // borrowck errors, see #10579 for one such instance. |
| 41 | + // In particular, if `a` causes a move and `f` references that moved binding, then we cannot lint: |
| 42 | + // ``` |
| 43 | + // let x = vec![1, 2]; |
| 44 | + // x.get(0..1).map(|s| s.to_vec()).unwrap_or(x); |
| 45 | + // ``` |
| 46 | + // This compiles, but changing it to `map_or` will produce a compile error: |
| 47 | + // ``` |
| 48 | + // let x = vec![1, 2]; |
| 49 | + // x.get(0..1).map_or(x, |s| s.to_vec()) |
| 50 | + // ^ moving `x` here |
| 51 | + // ^^^^^^^^^^^ while it is borrowed here (and later used in the closure) |
| 52 | + // ``` |
| 53 | + // So, we have to check that `a` is not referenced anywhere (even outside of the `.map` closure!) |
| 54 | + // before the call to `unwrap_or`. |
| 55 | + |
| 56 | + let mut unwrap_visitor = UnwrapVisitor { |
| 57 | + cx, |
| 58 | + identifiers: FxHashSet::default(), |
| 59 | + }; |
| 60 | + unwrap_visitor.visit_expr(unwrap_arg); |
| 61 | + |
| 62 | + let mut reference_visitor = ReferenceVisitor { |
| 63 | + cx, |
| 64 | + identifiers: unwrap_visitor.identifiers, |
| 65 | + unwrap_or_span: unwrap_arg.span, |
| 66 | + }; |
| 67 | + |
| 68 | + let body = cx.tcx.hir_body_owned_by(cx.tcx.hir_enclosing_body_owner(expr.hir_id)); |
| 69 | + |
| 70 | + // Visit the body, and return if we've found a reference |
| 71 | + if reference_visitor.visit_body(body).is_break() { |
| 72 | + return; |
40 | 73 | }
|
41 |
| - } else { |
42 |
| - return false; |
43 | 74 | }
|
44 | 75 |
|
| 76 | + if !unwrap_arg.span.eq_ctxt(map_span) { |
| 77 | + return; |
| 78 | + } |
| 79 | + |
| 80 | + // is_some_and is stabilised && `unwrap_or` argument is false; suggest `is_some_and` instead |
| 81 | + let suggest_is_some_and = matches!(&unwrap_arg.kind, ExprKind::Lit(lit) |
| 82 | + if matches!(lit.node, rustc_ast::LitKind::Bool(false))) |
| 83 | + && msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND); |
| 84 | + |
| 85 | + let mut applicability = Applicability::MachineApplicable; |
| 86 | + // get snippet for unwrap_or() |
| 87 | + let unwrap_snippet = snippet_with_applicability(cx, unwrap_arg.span, "..", &mut applicability); |
45 | 88 | // lint message
|
46 |
| - let msg = if is_option { |
47 |
| - "called `map(<f>).unwrap_or_else(<g>)` on an `Option` value" |
| 89 | + // comparing the snippet from source to raw text ("None") below is safe |
| 90 | + // because we already have checked the type. |
| 91 | + let unwrap_snippet_none = is_option && unwrap_snippet == "None"; |
| 92 | + let arg = if unwrap_snippet_none { |
| 93 | + "None" |
| 94 | + } else if suggest_is_some_and { |
| 95 | + "false" |
48 | 96 | } else {
|
49 |
| - "called `map(<f>).unwrap_or_else(<g>)` on a `Result` value" |
| 97 | + "<a>" |
50 | 98 | };
|
51 |
| - // get snippets for args to map() and unwrap_or_else() |
52 |
| - let map_snippet = snippet(cx, map_arg.span, ".."); |
53 |
| - let unwrap_snippet = snippet(cx, unwrap_arg.span, ".."); |
54 |
| - // lint, with note if neither arg is > 1 line and both map() and |
55 |
| - // unwrap_or_else() have the same span |
56 |
| - let multiline = map_snippet.lines().count() > 1 || unwrap_snippet.lines().count() > 1; |
57 |
| - let same_span = map_arg.span.eq_ctxt(unwrap_arg.span); |
58 |
| - if same_span && !multiline { |
59 |
| - let var_snippet = snippet(cx, recv.span, ".."); |
60 |
| - span_lint_and_sugg( |
61 |
| - cx, |
62 |
| - MAP_UNWRAP_OR, |
63 |
| - expr.span, |
64 |
| - msg, |
65 |
| - "try", |
66 |
| - format!("{var_snippet}.map_or_else({unwrap_snippet}, {map_snippet})"), |
67 |
| - Applicability::MachineApplicable, |
68 |
| - ); |
69 |
| - return true; |
70 |
| - } else if same_span && multiline { |
71 |
| - span_lint(cx, MAP_UNWRAP_OR, expr.span, msg); |
72 |
| - return true; |
| 99 | + let suggest = if unwrap_snippet_none { |
| 100 | + "and_then(<f>)" |
| 101 | + } else if suggest_is_some_and { |
| 102 | + if is_result { |
| 103 | + "is_ok_and(<f>)" |
| 104 | + } else { |
| 105 | + "is_some_and(<f>)" |
| 106 | + } |
| 107 | + } else { |
| 108 | + "map_or(<a>, <f>)" |
| 109 | + }; |
| 110 | + let msg = format!( |
| 111 | + "called `map(<f>).unwrap_or({arg})` on an `{}` value", |
| 112 | + if is_option { "Option" } else { "Result" } |
| 113 | + ); |
| 114 | + |
| 115 | + span_lint_and_then(cx, MAP_UNWRAP_OR, expr.span, msg, |diag| { |
| 116 | + let map_arg_span = map_arg.span; |
| 117 | + |
| 118 | + let mut suggestion = vec![ |
| 119 | + ( |
| 120 | + map_span, |
| 121 | + String::from(if unwrap_snippet_none { |
| 122 | + "and_then" |
| 123 | + } else if suggest_is_some_and { |
| 124 | + if is_result { "is_ok_and" } else { "is_some_and" } |
| 125 | + } else { |
| 126 | + "map_or" |
| 127 | + }), |
| 128 | + ), |
| 129 | + (expr.span.with_lo(unwrap_recv.span.hi()), String::new()), |
| 130 | + ]; |
| 131 | + |
| 132 | + if !unwrap_snippet_none && !suggest_is_some_and { |
| 133 | + suggestion.push((map_arg_span.with_hi(map_arg_span.lo()), format!("{unwrap_snippet}, "))); |
| 134 | + } |
| 135 | + |
| 136 | + diag.multipart_suggestion(format!("use `{suggest}` instead"), suggestion, applicability); |
| 137 | + }); |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +struct UnwrapVisitor<'a, 'tcx> { |
| 142 | + cx: &'a LateContext<'tcx>, |
| 143 | + identifiers: FxHashSet<HirId>, |
| 144 | +} |
| 145 | + |
| 146 | +impl<'tcx> Visitor<'tcx> for UnwrapVisitor<'_, 'tcx> { |
| 147 | + type NestedFilter = nested_filter::All; |
| 148 | + |
| 149 | + fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) { |
| 150 | + if let Res::Local(local_id) = path.res |
| 151 | + && let Node::Pat(pat) = self.cx.tcx.hir_node(local_id) |
| 152 | + && let PatKind::Binding(_, local_id, ..) = pat.kind |
| 153 | + { |
| 154 | + self.identifiers.insert(local_id); |
73 | 155 | }
|
| 156 | + walk_path(self, path); |
74 | 157 | }
|
75 | 158 |
|
76 |
| - false |
| 159 | + fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { |
| 160 | + self.cx.tcx |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +struct ReferenceVisitor<'a, 'tcx> { |
| 165 | + cx: &'a LateContext<'tcx>, |
| 166 | + identifiers: FxHashSet<HirId>, |
| 167 | + unwrap_or_span: Span, |
| 168 | +} |
| 169 | + |
| 170 | +impl<'tcx> Visitor<'tcx> for ReferenceVisitor<'_, 'tcx> { |
| 171 | + type NestedFilter = nested_filter::All; |
| 172 | + type Result = ControlFlow<()>; |
| 173 | + fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'_>) -> ControlFlow<()> { |
| 174 | + // If we haven't found a reference yet, check if this references |
| 175 | + // one of the locals that was moved in the `unwrap_or` argument. |
| 176 | + // We are only interested in exprs that appear before the `unwrap_or` call. |
| 177 | + if expr.span < self.unwrap_or_span |
| 178 | + && let ExprKind::Path(ref path) = expr.kind |
| 179 | + && let QPath::Resolved(_, path) = path |
| 180 | + && let Res::Local(local_id) = path.res |
| 181 | + && let Node::Pat(pat) = self.cx.tcx.hir_node(local_id) |
| 182 | + && let PatKind::Binding(_, local_id, ..) = pat.kind |
| 183 | + && self.identifiers.contains(&local_id) |
| 184 | + { |
| 185 | + return ControlFlow::Break(()); |
| 186 | + } |
| 187 | + rustc_hir::intravisit::walk_expr(self, expr) |
| 188 | + } |
| 189 | + |
| 190 | + fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { |
| 191 | + self.cx.tcx |
| 192 | + } |
77 | 193 | }
|
0 commit comments