Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion clippy_lints/src/derivable_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ fn check_struct<'tcx>(
if let TyKind::Path(QPath::Resolved(_, p)) = self_ty.kind
&& let Some(PathSegment { args, .. }) = p.segments.last()
{
let args = args.map(|a| a.args).unwrap_or(&[]);
let args = args.map(|a| a.args).unwrap_or_default();

// ty_args contains the generic parameters of the type declaration, while args contains the
// arguments used at instantiation time. If both len are not equal, it means that some
Expand Down
238 changes: 180 additions & 58 deletions clippy_lints/src/methods/map_unwrap_or.rs
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);
Comment on lines +80 to +83
Copy link
Contributor

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 suggests is_some_and but not is_none_or

Copy link
Contributor Author

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 for unwrap_or(false)

Copy link
Contributor

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, ..) into map_or_else(bool, ..) first, and then manual_is_variant_and would transform that further

Copy link
Contributor Author

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?

Copy link
Contributor

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


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
}
}
74 changes: 74 additions & 0 deletions clippy_lints/src/methods/map_unwrap_or_else.rs
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
}
6 changes: 3 additions & 3 deletions clippy_lints/src/methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ mod map_err_ignore;
mod map_flatten;
mod map_identity;
mod map_unwrap_or;
mod map_unwrap_or_else;
mod map_with_unused_argument_over_ranges;
mod mut_mutex_lock;
mod needless_as_bytes;
Expand All @@ -86,7 +87,6 @@ mod open_options;
mod option_as_ref_cloned;
mod option_as_ref_deref;
mod option_map_or_none;
mod option_map_unwrap_or;
mod or_fun_call;
mod or_then_unwrap;
mod path_buf_push_overwrite;
Expand Down Expand Up @@ -5551,7 +5551,7 @@ impl Methods {
);
},
Some((sym::map, m_recv, [m_arg], span, _)) => {
option_map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, self.msrv);
map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, self.msrv);
},
Some((then_method @ (sym::then | sym::then_some), t_recv, [t_arg], _, _)) => {
obfuscated_if_else::check(cx, expr, t_recv, t_arg, Some(u_arg), then_method, name);
Expand Down Expand Up @@ -5583,7 +5583,7 @@ impl Methods {
(sym::unwrap_or_else, [u_arg]) => {
match method_call(recv) {
Some((sym::map, recv, [map_arg], _, _))
if map_unwrap_or::check(cx, expr, recv, map_arg, u_arg, self.msrv) => {},
if map_unwrap_or_else::check(cx, expr, recv, map_arg, u_arg, self.msrv) => {},
Some((then_method @ (sym::then | sym::then_some), t_recv, [t_arg], _, _)) => {
obfuscated_if_else::check(
cx,
Expand Down
Loading