Skip to content

Commit cfb57d8

Browse files
committed
Fix manual_is_variant_and condition generation
When comparing `x.map(func) == Some(bool_lit)`, the value of `bool_lit` was ignored, despite the fact that its value should determine the value of the proposed expression. `func` can be either a closure or a path. For the latter, η-expansion will be used if needed to invert the result of the function call.
1 parent b631cef commit cfb57d8

File tree

4 files changed

+439
-28
lines changed

4 files changed

+439
-28
lines changed

clippy_lints/src/methods/manual_is_variant_and.rs

Lines changed: 120 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
2-
use clippy_utils::get_parent_expr;
32
use clippy_utils::msrvs::{self, Msrv};
4-
use clippy_utils::source::{snippet, snippet_opt};
3+
use clippy_utils::source::{snippet, snippet_with_applicability};
4+
use clippy_utils::sugg::Sugg;
55
use clippy_utils::ty::is_type_diagnostic_item;
6+
use clippy_utils::{get_parent_expr, sym};
7+
use rustc_ast::LitKind;
68
use rustc_errors::Applicability;
79
use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
8-
use rustc_hir::{BinOpKind, Expr, ExprKind, QPath};
10+
use rustc_hir::{BinOpKind, Closure, Expr, ExprKind, QPath};
911
use rustc_lint::LateContext;
1012
use rustc_middle::ty;
11-
use rustc_span::{BytePos, Span, sym};
13+
use rustc_span::Span;
1214

1315
use super::MANUAL_IS_VARIANT_AND;
1416

@@ -62,54 +64,147 @@ pub(super) fn check(
6264
);
6365
}
6466

65-
fn emit_lint(cx: &LateContext<'_>, op: BinOpKind, parent: &Expr<'_>, method_span: Span, is_option: bool) {
66-
if let Some(before_map_snippet) = snippet_opt(cx, parent.span.with_hi(method_span.lo()))
67-
&& let Some(after_map_snippet) = snippet_opt(cx, method_span.with_lo(method_span.lo() + BytePos(3)))
67+
#[derive(Clone, Copy, PartialEq)]
68+
enum Family {
69+
Option,
70+
Result,
71+
}
72+
73+
#[derive(Clone, Copy, PartialEq)]
74+
enum Op {
75+
Eq,
76+
Ne,
77+
}
78+
79+
impl TryFrom<BinOpKind> for Op {
80+
type Error = ();
81+
fn try_from(op: BinOpKind) -> Result<Self, Self::Error> {
82+
match op {
83+
BinOpKind::Eq => Ok(Self::Eq),
84+
BinOpKind::Ne => Ok(Self::Ne),
85+
_ => Err(()),
86+
}
87+
}
88+
}
89+
90+
/// Represents the argument of the `.map()` function, as a closure or as a path
91+
/// in case η-reduction is used.
92+
enum MapFunc<'hir> {
93+
Closure(&'hir Closure<'hir>),
94+
Path(&'hir Expr<'hir>),
95+
}
96+
97+
impl<'hir> TryFrom<&'hir Expr<'hir>> for MapFunc<'hir> {
98+
type Error = ();
99+
100+
fn try_from(expr: &'hir Expr<'hir>) -> Result<Self, Self::Error> {
101+
match expr.kind {
102+
ExprKind::Closure(closure) => Ok(Self::Closure(closure)),
103+
ExprKind::Path(_) => Ok(Self::Path(expr)),
104+
_ => Err(()),
105+
}
106+
}
107+
}
108+
109+
impl<'hir> MapFunc<'hir> {
110+
/// Build a suggestion suitable for use in a `.map()`-like function. η-expansion will be applied
111+
/// as needed.
112+
fn sugg(self, cx: &LateContext<'hir>, invert: bool, app: &mut Applicability) -> String {
113+
match self {
114+
Self::Closure(closure) => {
115+
let body = Sugg::hir_with_applicability(cx, cx.tcx.hir_body(closure.body).value, "..", app);
116+
format!(
117+
"{} {}",
118+
snippet_with_applicability(cx, closure.fn_decl_span, "|..|", app),
119+
if invert { !body } else { body }
120+
)
121+
},
122+
Self::Path(expr) => {
123+
let path = snippet_with_applicability(cx, expr.span, "_", app);
124+
if invert {
125+
format!("|x| !{path}(x)")
126+
} else {
127+
path.to_string()
128+
}
129+
},
130+
}
131+
}
132+
}
133+
134+
fn emit_lint<'tcx>(
135+
cx: &LateContext<'tcx>,
136+
span: Span,
137+
op: Op,
138+
family: Family,
139+
in_some_or_ok: bool,
140+
map_func: MapFunc<'tcx>,
141+
recv: &Expr<'_>,
142+
) {
143+
let mut app = Applicability::MachineApplicable;
144+
let recv = snippet_with_applicability(cx, recv.span, "_", &mut app);
145+
146+
let (invert_expr, method, invert_body) = match (family, op, in_some_or_ok) {
147+
(Family::Option, Op::Eq, bool_cst) => (false, "is_some_and", !bool_cst),
148+
(Family::Option, Op::Ne, bool_cst) => (false, "is_none_or", bool_cst),
149+
(Family::Result, Op::Eq, bool_cst) => (false, "is_ok_and", !bool_cst),
150+
(Family::Result, Op::Ne, bool_cst) => (true, "is_ok_and", !bool_cst),
151+
};
68152
{
69153
span_lint_and_sugg(
70154
cx,
71155
MANUAL_IS_VARIANT_AND,
72-
parent.span,
156+
span,
73157
format!(
74158
"called `.map() {}= {}()`",
75-
if op == BinOpKind::Eq { '=' } else { '!' },
76-
if is_option { "Some" } else { "Ok" },
159+
if op == Op::Eq { '=' } else { '!' },
160+
if family == Family::Option { "Some" } else { "Ok" },
77161
),
78162
"use",
79-
if is_option && op == BinOpKind::Ne {
80-
format!("{before_map_snippet}is_none_or{after_map_snippet}",)
81-
} else {
82-
format!(
83-
"{}{before_map_snippet}{}{after_map_snippet}",
84-
if op == BinOpKind::Eq { "" } else { "!" },
85-
if is_option { "is_some_and" } else { "is_ok_and" },
86-
)
87-
},
88-
Applicability::MachineApplicable,
163+
format!(
164+
"{inversion}{recv}.{method}({body})",
165+
inversion = if invert_expr { "!" } else { "" },
166+
body = map_func.sugg(cx, invert_body, &mut app),
167+
),
168+
app,
89169
);
90-
}
170+
};
91171
}
92172

93173
pub(super) fn check_map(cx: &LateContext<'_>, expr: &Expr<'_>) {
94174
if let Some(parent_expr) = get_parent_expr(cx, expr)
95175
&& let ExprKind::Binary(op, left, right) = parent_expr.kind
96-
&& matches!(op.node, BinOpKind::Eq | BinOpKind::Ne)
97176
&& op.span.eq_ctxt(expr.span)
177+
&& let Ok(op) = Op::try_from(op.node)
98178
{
99179
// Check `left` and `right` expression in any order, and for `Option` and `Result`
100180
for (expr1, expr2) in [(left, right), (right, left)] {
101181
for item in [sym::Option, sym::Result] {
102-
if let ExprKind::Call(call, ..) = expr1.kind
182+
if let ExprKind::Call(call, [arg]) = expr1.kind
183+
&& let ExprKind::Lit(lit) = arg.kind
184+
&& let LitKind::Bool(bool_cst) = lit.node
103185
&& let ExprKind::Path(QPath::Resolved(_, path)) = call.kind
104186
&& let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Fn), _) = path.res
105187
&& let ty = cx.typeck_results().expr_ty(expr1)
106188
&& let ty::Adt(adt, args) = ty.kind()
107189
&& cx.tcx.is_diagnostic_item(item, adt.did())
108190
&& args.type_at(0).is_bool()
109-
&& let ExprKind::MethodCall(_, recv, _, span) = expr2.kind
191+
&& let ExprKind::MethodCall(_, recv, [map_expr], _) = expr2.kind
110192
&& is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), item)
193+
&& let Ok(map_func) = MapFunc::try_from(map_expr)
111194
{
112-
return emit_lint(cx, op.node, parent_expr, span, item == sym::Option);
195+
return emit_lint(
196+
cx,
197+
parent_expr.span,
198+
op,
199+
if item == sym::Option {
200+
Family::Option
201+
} else {
202+
Family::Result
203+
},
204+
bool_cst,
205+
map_func,
206+
recv,
207+
);
113208
}
114209
}
115210
}

tests/ui/manual_is_variant_and.fixed

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fn option_methods() {
6161

6262
let _ = Some(2).is_some_and(|x| x % 2 == 0);
6363
//~^ manual_is_variant_and
64-
let _ = Some(2).is_none_or(|x| x % 2 == 0);
64+
let _ = Some(2).is_none_or(|x| x % 2 != 0);
6565
//~^ manual_is_variant_and
6666
let _ = Some(2).is_some_and(|x| x % 2 == 0);
6767
//~^ manual_is_variant_and
@@ -116,3 +116,113 @@ fn main() {
116116
option_methods();
117117
result_methods();
118118
}
119+
120+
fn issue15202() {
121+
let xs = [None, Some(b'_'), Some(b'1')];
122+
for x in xs {
123+
let a1 = x.is_none_or(|b| !b.is_ascii_digit());
124+
//~^ manual_is_variant_and
125+
let a2 = x.is_none_or(|b| !b.is_ascii_digit());
126+
assert_eq!(a1, a2);
127+
}
128+
129+
for x in xs {
130+
let a1 = x.is_none_or(|b| b.is_ascii_digit());
131+
//~^ manual_is_variant_and
132+
let a2 = x.is_none_or(|b| b.is_ascii_digit());
133+
assert_eq!(a1, a2);
134+
}
135+
136+
for x in xs {
137+
let a1 = x.is_some_and(|b| b.is_ascii_digit());
138+
//~^ manual_is_variant_and
139+
let a2 = x.is_some_and(|b| b.is_ascii_digit());
140+
assert_eq!(a1, a2);
141+
}
142+
143+
for x in xs {
144+
let a1 = x.is_some_and(|b| !b.is_ascii_digit());
145+
//~^ manual_is_variant_and
146+
let a2 = x.is_some_and(|b| !b.is_ascii_digit());
147+
assert_eq!(a1, a2);
148+
}
149+
150+
let xs = [Err("foo"), Ok(b'_'), Ok(b'1')];
151+
for x in xs {
152+
let a1 = !x.is_ok_and(|b| b.is_ascii_digit());
153+
//~^ manual_is_variant_and
154+
let a2 = !x.is_ok_and(|b| b.is_ascii_digit());
155+
assert_eq!(a1, a2);
156+
}
157+
158+
for x in xs {
159+
let a1 = !x.is_ok_and(|b| !b.is_ascii_digit());
160+
//~^ manual_is_variant_and
161+
let a2 = !x.is_ok_and(|b| !b.is_ascii_digit());
162+
assert_eq!(a1, a2);
163+
}
164+
165+
for x in xs {
166+
let a1 = x.is_ok_and(|b| b.is_ascii_digit());
167+
//~^ manual_is_variant_and
168+
let a2 = x.is_ok_and(|b| b.is_ascii_digit());
169+
assert_eq!(a1, a2);
170+
}
171+
172+
for x in xs {
173+
let a1 = x.is_ok_and(|b| !b.is_ascii_digit());
174+
//~^ manual_is_variant_and
175+
let a2 = x.is_ok_and(|b| !b.is_ascii_digit());
176+
assert_eq!(a1, a2);
177+
}
178+
}
179+
180+
mod with_func {
181+
fn iad(b: u8) -> bool {
182+
b.is_ascii_digit()
183+
}
184+
185+
fn check_option(b: Option<u8>) {
186+
let a1 = b.is_some_and(iad);
187+
//~^ manual_is_variant_and
188+
let a2 = b.is_some_and(iad);
189+
assert_eq!(a1, a2);
190+
191+
let a1 = b.is_some_and(|x| !iad(x));
192+
//~^ manual_is_variant_and
193+
let a2 = b.is_some_and(|x| !iad(x));
194+
assert_eq!(a1, a2);
195+
196+
let a1 = b.is_none_or(|x| !iad(x));
197+
//~^ manual_is_variant_and
198+
let a2 = b.is_none_or(|x| !iad(x));
199+
assert_eq!(a1, a2);
200+
201+
let a1 = b.is_none_or(iad);
202+
//~^ manual_is_variant_and
203+
let a2 = b.is_none_or(iad);
204+
assert_eq!(a1, a2);
205+
}
206+
207+
fn check_result(b: Result<u8, ()>) {
208+
let a1 = b.is_ok_and(iad);
209+
//~^ manual_is_variant_and
210+
let a2 = b.is_ok_and(iad);
211+
assert_eq!(a1, a2);
212+
213+
let a1 = b.is_ok_and(|x| !iad(x));
214+
//~^ manual_is_variant_and
215+
let a2 = b.is_ok_and(|x| !iad(x));
216+
assert_eq!(a1, a2);
217+
218+
let a1 = !b.is_ok_and(iad);
219+
//~^ manual_is_variant_and
220+
let a2 = !b.is_ok_and(iad);
221+
assert_eq!(a1, a2);
222+
223+
let a1 = !b.is_ok_and(|x| !iad(x));
224+
//~^ manual_is_variant_and
225+
let a2 = !b.is_ok_and(|x| !iad(x));
226+
assert_eq!(a1, a2);
227+
}
228+
}

0 commit comments

Comments
 (0)