-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Fix map_unwrap_or
fail to cover Result::unwrap_or
#15718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
profetia
wants to merge
3
commits into
rust-lang:master
Choose a base branch
from
profetia:issue15714
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,77 +1,199 @@ | ||
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; | ||
use clippy_utils::diagnostics::span_lint_and_then; | ||
use clippy_utils::msrvs::{self, Msrv}; | ||
use clippy_utils::source::snippet; | ||
use clippy_utils::ty::is_type_diagnostic_item; | ||
use clippy_utils::usage::mutated_variables; | ||
use clippy_utils::source::snippet_with_applicability; | ||
use clippy_utils::ty::{get_type_diagnostic_name, is_copy}; | ||
use clippy_utils::{is_res_lang_ctor, path_res}; | ||
use rustc_data_structures::fx::FxHashSet; | ||
use rustc_errors::Applicability; | ||
use rustc_hir as hir; | ||
use rustc_hir::def::Res; | ||
use rustc_hir::intravisit::{Visitor, walk_path}; | ||
use rustc_hir::{ExprKind, HirId, LangItem, Node, PatKind, Path, QPath}; | ||
use rustc_lint::LateContext; | ||
use rustc_span::symbol::sym; | ||
use rustc_middle::hir::nested_filter; | ||
use rustc_span::{Span, sym}; | ||
use std::ops::ControlFlow; | ||
|
||
use super::MAP_UNWRAP_OR; | ||
|
||
/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s | ||
/// | ||
/// Returns true if the lint was emitted | ||
/// lint use of `map().unwrap_or()` for `Option`s and `Result`s | ||
#[expect(clippy::too_many_arguments)] | ||
pub(super) fn check<'tcx>( | ||
cx: &LateContext<'tcx>, | ||
expr: &'tcx hir::Expr<'_>, | ||
recv: &'tcx hir::Expr<'_>, | ||
map_arg: &'tcx hir::Expr<'_>, | ||
unwrap_arg: &'tcx hir::Expr<'_>, | ||
expr: &rustc_hir::Expr<'_>, | ||
recv: &rustc_hir::Expr<'_>, | ||
map_arg: &'tcx rustc_hir::Expr<'_>, | ||
unwrap_recv: &rustc_hir::Expr<'_>, | ||
unwrap_arg: &'tcx rustc_hir::Expr<'_>, | ||
map_span: Span, | ||
msrv: Msrv, | ||
) -> bool { | ||
// lint if the caller of `map()` is an `Option` or a `Result`. | ||
let is_option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option); | ||
let is_result = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result); | ||
) { | ||
let recv_ty = cx.typeck_results().expr_ty(recv).peel_refs(); | ||
let is_option = match get_type_diagnostic_name(cx, recv_ty) { | ||
Some(sym::Option) => true, | ||
Some(sym::Result) if msrv.meets(cx, msrvs::RESULT_MAP_OR) => false, | ||
_ => return, | ||
}; | ||
|
||
if is_result && !msrv.meets(cx, msrvs::RESULT_MAP_OR_ELSE) { | ||
return false; | ||
let unwrap_arg_ty = cx.typeck_results().expr_ty(unwrap_arg); | ||
if !is_copy(cx, unwrap_arg_ty) { | ||
// Replacing `.map(<f>).unwrap_or(<a>)` with `.map_or(<a>, <f>)` can sometimes lead to | ||
// borrowck errors, see #10579 for one such instance. | ||
// In particular, if `a` causes a move and `f` references that moved binding, then we cannot lint: | ||
// ``` | ||
// let x = vec![1, 2]; | ||
// x.get(0..1).map(|s| s.to_vec()).unwrap_or(x); | ||
// ``` | ||
// This compiles, but changing it to `map_or` will produce a compile error: | ||
// ``` | ||
// let x = vec![1, 2]; | ||
// x.get(0..1).map_or(x, |s| s.to_vec()) | ||
// ^ moving `x` here | ||
// ^^^^^^^^^^^ while it is borrowed here (and later used in the closure) | ||
// ``` | ||
// So, we have to check that `a` is not referenced anywhere (even outside of the `.map` closure!) | ||
// before the call to `unwrap_or`. | ||
|
||
let mut unwrap_visitor = UnwrapVisitor { | ||
cx, | ||
identifiers: FxHashSet::default(), | ||
}; | ||
unwrap_visitor.visit_expr(unwrap_arg); | ||
|
||
let mut reference_visitor = ReferenceVisitor { | ||
cx, | ||
identifiers: unwrap_visitor.identifiers, | ||
unwrap_or_span: unwrap_arg.span, | ||
}; | ||
|
||
let body = cx.tcx.hir_body_owned_by(cx.tcx.hir_enclosing_body_owner(expr.hir_id)); | ||
|
||
// Visit the body, and return if we've found a reference | ||
if reference_visitor.visit_body(body).is_break() { | ||
return; | ||
} | ||
} | ||
|
||
if is_option || is_result { | ||
// Don't make a suggestion that may fail to compile due to mutably borrowing | ||
// the same variable twice. | ||
let map_mutated_vars = mutated_variables(recv, cx); | ||
let unwrap_mutated_vars = mutated_variables(unwrap_arg, cx); | ||
if let (Some(map_mutated_vars), Some(unwrap_mutated_vars)) = (map_mutated_vars, unwrap_mutated_vars) { | ||
if map_mutated_vars.intersection(&unwrap_mutated_vars).next().is_some() { | ||
return false; | ||
} | ||
if !unwrap_arg.span.eq_ctxt(map_span) { | ||
return; | ||
} | ||
|
||
// is_some_and is stabilised && `unwrap_or` argument is false; suggest `is_some_and` instead | ||
let suggest_is_some_and = matches!(&unwrap_arg.kind, ExprKind::Lit(lit) | ||
if matches!(lit.node, rustc_ast::LitKind::Bool(false))) | ||
&& msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND); | ||
|
||
let mut applicability = Applicability::MachineApplicable; | ||
// get snippet for unwrap_or() | ||
let unwrap_snippet = snippet_with_applicability(cx, unwrap_arg.span, "..", &mut applicability); | ||
// lint message | ||
// comparing the snippet from source to raw text ("None") below is safe | ||
// because we already have checked the type. | ||
let unwrap_snippet_none = is_option && is_res_lang_ctor(cx, path_res(cx, unwrap_arg), LangItem::OptionNone); | ||
let arg = if unwrap_snippet_none { | ||
"None" | ||
} else if suggest_is_some_and { | ||
"false" | ||
} else { | ||
"<a>" | ||
}; | ||
let suggest = if unwrap_snippet_none { | ||
"and_then(<f>)" | ||
} else if suggest_is_some_and { | ||
if is_option { | ||
"is_some_and(<f>)" | ||
} else { | ||
return false; | ||
"is_ok_and(<f>)" | ||
} | ||
} else { | ||
"map_or(<a>, <f>)" | ||
}; | ||
let msg = format!( | ||
"called `map(<f>).unwrap_or({arg})` on an `{}` value", | ||
if is_option { "Option" } else { "Result" } | ||
); | ||
|
||
// lint message | ||
let msg = if is_option { | ||
"called `map(<f>).unwrap_or_else(<g>)` on an `Option` value" | ||
} else { | ||
"called `map(<f>).unwrap_or_else(<g>)` on a `Result` value" | ||
}; | ||
// get snippets for args to map() and unwrap_or_else() | ||
let map_snippet = snippet(cx, map_arg.span, ".."); | ||
let unwrap_snippet = snippet(cx, unwrap_arg.span, ".."); | ||
// lint, with note if neither arg is > 1 line and both map() and | ||
// unwrap_or_else() have the same span | ||
let multiline = map_snippet.lines().count() > 1 || unwrap_snippet.lines().count() > 1; | ||
let same_span = map_arg.span.eq_ctxt(unwrap_arg.span); | ||
if same_span && !multiline { | ||
let var_snippet = snippet(cx, recv.span, ".."); | ||
span_lint_and_sugg( | ||
cx, | ||
MAP_UNWRAP_OR, | ||
expr.span, | ||
msg, | ||
"try", | ||
format!("{var_snippet}.map_or_else({unwrap_snippet}, {map_snippet})"), | ||
Applicability::MachineApplicable, | ||
); | ||
return true; | ||
} else if same_span && multiline { | ||
span_lint(cx, MAP_UNWRAP_OR, expr.span, msg); | ||
return true; | ||
span_lint_and_then(cx, MAP_UNWRAP_OR, expr.span, msg, |diag| { | ||
let map_arg_span = map_arg.span; | ||
|
||
let mut suggestion = vec![ | ||
( | ||
map_span, | ||
String::from(if unwrap_snippet_none { | ||
"and_then" | ||
} else if suggest_is_some_and { | ||
if is_option { "is_some_and" } else { "is_ok_and" } | ||
} else { | ||
let unwrap_arg_ty = unwrap_arg_ty.peel_refs(); | ||
if unwrap_arg_ty.is_array() | ||
&& let unwrap_arg_ty_adj = cx.typeck_results().expr_ty_adjusted(unwrap_arg).peel_refs() | ||
&& unwrap_arg_ty_adj.is_slice() | ||
{ | ||
return; | ||
} | ||
"map_or" | ||
}), | ||
), | ||
(expr.span.with_lo(unwrap_recv.span.hi()), String::new()), | ||
]; | ||
|
||
if !unwrap_snippet_none && !suggest_is_some_and { | ||
suggestion.push((map_arg_span.with_hi(map_arg_span.lo()), format!("{unwrap_snippet}, "))); | ||
} | ||
|
||
diag.multipart_suggestion(format!("use `{suggest}` instead"), suggestion, applicability); | ||
}); | ||
} | ||
|
||
struct UnwrapVisitor<'a, 'tcx> { | ||
cx: &'a LateContext<'tcx>, | ||
identifiers: FxHashSet<HirId>, | ||
} | ||
|
||
impl<'tcx> Visitor<'tcx> for UnwrapVisitor<'_, 'tcx> { | ||
type NestedFilter = nested_filter::All; | ||
|
||
fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) { | ||
if let Res::Local(local_id) = path.res | ||
&& let Node::Pat(pat) = self.cx.tcx.hir_node(local_id) | ||
&& let PatKind::Binding(_, local_id, ..) = pat.kind | ||
{ | ||
self.identifiers.insert(local_id); | ||
} | ||
walk_path(self, path); | ||
} | ||
|
||
false | ||
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { | ||
self.cx.tcx | ||
} | ||
} | ||
|
||
struct ReferenceVisitor<'a, 'tcx> { | ||
cx: &'a LateContext<'tcx>, | ||
identifiers: FxHashSet<HirId>, | ||
unwrap_or_span: Span, | ||
} | ||
|
||
impl<'tcx> Visitor<'tcx> for ReferenceVisitor<'_, 'tcx> { | ||
type NestedFilter = nested_filter::All; | ||
type Result = ControlFlow<()>; | ||
fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'_>) -> ControlFlow<()> { | ||
// If we haven't found a reference yet, check if this references | ||
// one of the locals that was moved in the `unwrap_or` argument. | ||
// We are only interested in exprs that appear before the `unwrap_or` call. | ||
if expr.span < self.unwrap_or_span | ||
&& let ExprKind::Path(ref path) = expr.kind | ||
&& let QPath::Resolved(_, path) = path | ||
&& let Res::Local(local_id) = path.res | ||
&& let Node::Pat(pat) = self.cx.tcx.hir_node(local_id) | ||
&& let PatKind::Binding(_, local_id, ..) = pat.kind | ||
&& self.identifiers.contains(&local_id) | ||
{ | ||
return ControlFlow::Break(()); | ||
} | ||
rustc_hir::intravisit::walk_expr(self, expr) | ||
} | ||
|
||
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { | ||
self.cx.tcx | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg}; | ||
use clippy_utils::msrvs::{self, Msrv}; | ||
use clippy_utils::source::snippet; | ||
use clippy_utils::ty::get_type_diagnostic_name; | ||
use clippy_utils::usage::mutated_variables; | ||
use rustc_errors::Applicability; | ||
use rustc_hir as hir; | ||
use rustc_lint::LateContext; | ||
use rustc_span::symbol::sym; | ||
|
||
use super::MAP_UNWRAP_OR; | ||
|
||
/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s | ||
/// | ||
/// Returns true if the lint was emitted | ||
pub(super) fn check<'tcx>( | ||
cx: &LateContext<'tcx>, | ||
expr: &'tcx hir::Expr<'_>, | ||
recv: &'tcx hir::Expr<'_>, | ||
map_arg: &'tcx hir::Expr<'_>, | ||
unwrap_arg: &'tcx hir::Expr<'_>, | ||
msrv: Msrv, | ||
) -> bool { | ||
let recv_ty = cx.typeck_results().expr_ty(recv).peel_refs(); | ||
let is_option = match get_type_diagnostic_name(cx, recv_ty) { | ||
Some(sym::Option) => true, | ||
Some(sym::Result) if msrv.meets(cx, msrvs::RESULT_MAP_OR_ELSE) => false, | ||
_ => return false, | ||
}; | ||
|
||
// Don't make a suggestion that may fail to compile due to mutably borrowing | ||
// the same variable twice. | ||
let map_mutated_vars = mutated_variables(recv, cx); | ||
let unwrap_mutated_vars = mutated_variables(unwrap_arg, cx); | ||
if let (Some(map_mutated_vars), Some(unwrap_mutated_vars)) = (map_mutated_vars, unwrap_mutated_vars) { | ||
if map_mutated_vars.intersection(&unwrap_mutated_vars).next().is_some() { | ||
return false; | ||
} | ||
} else { | ||
return false; | ||
} | ||
|
||
// lint message | ||
let msg = if is_option { | ||
"called `map(<f>).unwrap_or_else(<g>)` on an `Option` value" | ||
} else { | ||
"called `map(<f>).unwrap_or_else(<g>)` on a `Result` value" | ||
}; | ||
// get snippets for args to map() and unwrap_or_else() | ||
let map_snippet = snippet(cx, map_arg.span, ".."); | ||
let unwrap_snippet = snippet(cx, unwrap_arg.span, ".."); | ||
// lint, with note if neither arg is > 1 line and both map() and | ||
// unwrap_or_else() have the same span | ||
let multiline = map_snippet.lines().count() > 1 || unwrap_snippet.lines().count() > 1; | ||
let same_span = map_arg.span.eq_ctxt(unwrap_arg.span); | ||
if same_span && !multiline { | ||
let var_snippet = snippet(cx, recv.span, ".."); | ||
span_lint_and_sugg( | ||
cx, | ||
MAP_UNWRAP_OR, | ||
expr.span, | ||
msg, | ||
"try", | ||
format!("{var_snippet}.map_or_else({unwrap_snippet}, {map_snippet})"), | ||
Applicability::MachineApplicable, | ||
); | ||
return true; | ||
} else if same_span && multiline { | ||
span_lint(cx, MAP_UNWRAP_OR, expr.span, msg); | ||
return true; | ||
} | ||
|
||
false | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be left to
manual_is_variant_and
? For example, this suggestsis_some_and
but notis_none_or
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think
manual_is_variant_and
checks forunwrap_or(false)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I mean let this lint turn
map().unwrap_or_else(bool, ..)
intomap_or_else(bool, ..)
first, and thenmanual_is_variant_and
would transform that furtherThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok with this behavior. Do you want it to be handled in one transform?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for not explaining myself clearly^^ Yes, I also prefer this to be handled in two transforms, so what I'm proposing is to remove the
suggest_is_some_and
logic from this lint