|
1 | | -use clippy_utils::diagnostics::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::res::MaybeDef; |
4 | | -use clippy_utils::source::snippet; |
5 | | -use clippy_utils::usage::mutated_variables; |
| 3 | +use clippy_utils::res::{MaybeDef as _, MaybeResPath as _}; |
| 4 | +use clippy_utils::source::snippet_with_applicability; |
| 5 | +use clippy_utils::ty::is_copy; |
| 6 | +use rustc_data_structures::fx::FxHashSet; |
6 | 7 | use rustc_errors::Applicability; |
7 | | -use rustc_hir as hir; |
| 8 | +use rustc_hir::def::Res; |
| 9 | +use rustc_hir::intravisit::{Visitor, walk_expr, walk_path}; |
| 10 | +use rustc_hir::{ExprKind, HirId, LangItem, Node, PatKind, Path, QPath}; |
8 | 11 | use rustc_lint::LateContext; |
9 | | -use rustc_span::symbol::sym; |
| 12 | +use rustc_middle::hir::nested_filter; |
| 13 | +use rustc_span::{Span, sym}; |
| 14 | +use std::ops::ControlFlow; |
10 | 15 |
|
11 | 16 | use super::MAP_UNWRAP_OR; |
12 | 17 |
|
13 | | -/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s |
14 | | -/// |
15 | | -/// Returns true if the lint was emitted |
| 18 | +/// lint use of `map().unwrap_or()` for `Option`s and `Result`s |
| 19 | +#[expect(clippy::too_many_arguments)] |
16 | 20 | pub(super) fn check<'tcx>( |
17 | 21 | 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<'_>, |
| 22 | + expr: &rustc_hir::Expr<'_>, |
| 23 | + recv: &rustc_hir::Expr<'_>, |
| 24 | + map_arg: &'tcx rustc_hir::Expr<'_>, |
| 25 | + unwrap_recv: &rustc_hir::Expr<'_>, |
| 26 | + unwrap_arg: &'tcx rustc_hir::Expr<'_>, |
| 27 | + map_span: Span, |
22 | 28 | msrv: Msrv, |
23 | | -) -> bool { |
24 | | - // lint if the caller of `map()` is an `Option` or a `Result`. |
25 | | - let is_option = cx.typeck_results().expr_ty(recv).is_diag_item(cx, sym::Option); |
26 | | - let is_result = cx.typeck_results().expr_ty(recv).is_diag_item(cx, sym::Result); |
| 29 | +) { |
| 30 | + let recv_ty = cx.typeck_results().expr_ty(recv).peel_refs(); |
| 31 | + let recv_ty_kind = match recv_ty.opt_diag_name(cx) { |
| 32 | + Some(sym::Option) => sym::Option, |
| 33 | + Some(sym::Result) if msrv.meets(cx, msrvs::RESULT_MAP_OR) => sym::Result, |
| 34 | + _ => return, |
| 35 | + }; |
27 | 36 |
|
28 | | - if is_result && !msrv.meets(cx, msrvs::RESULT_MAP_OR_ELSE) { |
29 | | - return false; |
| 37 | + let unwrap_arg_ty = cx.typeck_results().expr_ty(unwrap_arg); |
| 38 | + if !is_copy(cx, unwrap_arg_ty) { |
| 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; |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + if !unwrap_arg.span.eq_ctxt(map_span) { |
| 77 | + return; |
30 | 78 | } |
31 | 79 |
|
32 | | - 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; |
40 | | - } |
41 | | - } else { |
42 | | - return false; |
| 80 | + let mut applicability = Applicability::MachineApplicable; |
| 81 | + // get snippet for unwrap_or() |
| 82 | + let unwrap_snippet = snippet_with_applicability(cx, unwrap_arg.span, "..", &mut applicability); |
| 83 | + // lint message |
| 84 | + |
| 85 | + let suggest_kind = if recv_ty_kind == sym::Option |
| 86 | + && unwrap_arg |
| 87 | + .basic_res() |
| 88 | + .ctor_parent(cx) |
| 89 | + .is_lang_item(cx, LangItem::OptionNone) |
| 90 | + { |
| 91 | + SuggestedKind::AndThen |
| 92 | + } |
| 93 | + // is_some_and is stabilised && `unwrap_or` argument is false; suggest `is_some_and` instead |
| 94 | + else if matches!(&unwrap_arg.kind, ExprKind::Lit(lit) |
| 95 | + if matches!(lit.node, rustc_ast::LitKind::Bool(false))) |
| 96 | + && msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) |
| 97 | + { |
| 98 | + SuggestedKind::IsVariantAnd |
| 99 | + } else { |
| 100 | + SuggestedKind::Other |
| 101 | + }; |
| 102 | + |
| 103 | + let arg = match suggest_kind { |
| 104 | + SuggestedKind::AndThen => "None", |
| 105 | + SuggestedKind::IsVariantAnd => "false", |
| 106 | + SuggestedKind::Other => "<a>", |
| 107 | + }; |
| 108 | + |
| 109 | + let suggest = match (suggest_kind, recv_ty_kind) { |
| 110 | + (SuggestedKind::AndThen, _) => "and_then(<f>)", |
| 111 | + (SuggestedKind::IsVariantAnd, sym::Result) => "is_ok_and(<f>)", |
| 112 | + (SuggestedKind::IsVariantAnd, sym::Option) => "is_some_and(<f>)", |
| 113 | + _ => "map_or(<a>, <f>)", |
| 114 | + }; |
| 115 | + |
| 116 | + let msg = format!( |
| 117 | + "called `map(<f>).unwrap_or({arg})` on {} `{recv_ty_kind}` value", |
| 118 | + if recv_ty_kind == sym::Option { "an" } else { "a" } |
| 119 | + ); |
| 120 | + |
| 121 | + span_lint_and_then(cx, MAP_UNWRAP_OR, expr.span, msg, |diag| { |
| 122 | + let map_arg_span = map_arg.span; |
| 123 | + |
| 124 | + let mut suggestion = vec![ |
| 125 | + ( |
| 126 | + map_span, |
| 127 | + String::from(match (suggest_kind, recv_ty_kind) { |
| 128 | + (SuggestedKind::AndThen, _) => "and_then", |
| 129 | + (SuggestedKind::IsVariantAnd, sym::Result) => "is_ok_and", |
| 130 | + (SuggestedKind::IsVariantAnd, sym::Option) => "is_some_and", |
| 131 | + (SuggestedKind::Other, _) |
| 132 | + if unwrap_arg_ty.peel_refs().is_array() |
| 133 | + && cx.typeck_results().expr_ty_adjusted(unwrap_arg).peel_refs().is_slice() => |
| 134 | + { |
| 135 | + return; |
| 136 | + }, |
| 137 | + _ => "map_or", |
| 138 | + }), |
| 139 | + ), |
| 140 | + (expr.span.with_lo(unwrap_recv.span.hi()), String::new()), |
| 141 | + ]; |
| 142 | + |
| 143 | + if matches!(suggest_kind, SuggestedKind::Other) { |
| 144 | + suggestion.push((map_arg_span.with_hi(map_arg_span.lo()), format!("{unwrap_snippet}, "))); |
43 | 145 | } |
44 | 146 |
|
45 | | - // lint message |
46 | | - let msg = if is_option { |
47 | | - "called `map(<f>).unwrap_or_else(<g>)` on an `Option` value" |
48 | | - } else { |
49 | | - "called `map(<f>).unwrap_or_else(<g>)` on a `Result` value" |
50 | | - }; |
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 both map() and unwrap_or_else() have the same span |
55 | | - if map_arg.span.eq_ctxt(unwrap_arg.span) { |
56 | | - let var_snippet = snippet(cx, recv.span, ".."); |
57 | | - span_lint_and_sugg( |
58 | | - cx, |
59 | | - MAP_UNWRAP_OR, |
60 | | - expr.span, |
61 | | - msg, |
62 | | - "try", |
63 | | - format!("{var_snippet}.map_or_else({unwrap_snippet}, {map_snippet})"), |
64 | | - Applicability::MachineApplicable, |
65 | | - ); |
66 | | - return true; |
| 147 | + diag.multipart_suggestion(format!("use `{suggest}` instead"), suggestion, applicability); |
| 148 | + }); |
| 149 | +} |
| 150 | + |
| 151 | +#[derive(Clone, Copy, PartialEq, Eq)] |
| 152 | +enum SuggestedKind { |
| 153 | + AndThen, |
| 154 | + IsVariantAnd, |
| 155 | + Other, |
| 156 | +} |
| 157 | + |
| 158 | +struct UnwrapVisitor<'a, 'tcx> { |
| 159 | + cx: &'a LateContext<'tcx>, |
| 160 | + identifiers: FxHashSet<HirId>, |
| 161 | +} |
| 162 | + |
| 163 | +impl<'tcx> Visitor<'tcx> for UnwrapVisitor<'_, 'tcx> { |
| 164 | + type NestedFilter = nested_filter::All; |
| 165 | + |
| 166 | + fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) { |
| 167 | + if let Res::Local(local_id) = path.res |
| 168 | + && let Node::Pat(pat) = self.cx.tcx.hir_node(local_id) |
| 169 | + && let PatKind::Binding(_, local_id, ..) = pat.kind |
| 170 | + { |
| 171 | + self.identifiers.insert(local_id); |
67 | 172 | } |
| 173 | + walk_path(self, path); |
68 | 174 | } |
69 | 175 |
|
70 | | - false |
| 176 | + fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { |
| 177 | + self.cx.tcx |
| 178 | + } |
| 179 | +} |
| 180 | + |
| 181 | +struct ReferenceVisitor<'a, 'tcx> { |
| 182 | + cx: &'a LateContext<'tcx>, |
| 183 | + identifiers: FxHashSet<HirId>, |
| 184 | + unwrap_or_span: Span, |
| 185 | +} |
| 186 | + |
| 187 | +impl<'tcx> Visitor<'tcx> for ReferenceVisitor<'_, 'tcx> { |
| 188 | + type NestedFilter = nested_filter::All; |
| 189 | + type Result = ControlFlow<()>; |
| 190 | + fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'_>) -> ControlFlow<()> { |
| 191 | + // If we haven't found a reference yet, check if this references |
| 192 | + // one of the locals that was moved in the `unwrap_or` argument. |
| 193 | + // We are only interested in exprs that appear before the `unwrap_or` call. |
| 194 | + if expr.span < self.unwrap_or_span |
| 195 | + && let ExprKind::Path(ref path) = expr.kind |
| 196 | + && let QPath::Resolved(_, path) = path |
| 197 | + && let Res::Local(local_id) = path.res |
| 198 | + && let Node::Pat(pat) = self.cx.tcx.hir_node(local_id) |
| 199 | + && let PatKind::Binding(_, local_id, ..) = pat.kind |
| 200 | + && self.identifiers.contains(&local_id) |
| 201 | + { |
| 202 | + return ControlFlow::Break(()); |
| 203 | + } |
| 204 | + walk_expr(self, expr) |
| 205 | + } |
| 206 | + |
| 207 | + fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { |
| 208 | + self.cx.tcx |
| 209 | + } |
71 | 210 | } |
0 commit comments