Skip to content

Commit 3af9072

Browse files
committed
Auto merge of #9288 - lukaslueg:partialeqnone, r=Jarcho
Add partialeq_to_none lint Initial implementation of #9275, adding lint `partialeq_to_none`. This is my first time working on `clippy`, so please review carefully. I'm unsure especially about the `Sugg`, as it covers the entire `BinOp`, instead of just covering one of the sides and the operator (see the multi-line example). I was unsure if pinpointing the suggestion wouldn't be brittle... changelog: [`PARTIALEQ_TO_NONE`]: Initial commit
2 parents 8f39061 + 657b0da commit 3af9072

16 files changed

+356
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3824,6 +3824,7 @@ Released 2018-09-13
38243824
[`panic_params`]: https://rust-lang.github.io/rust-clippy/master/index.html#panic_params
38253825
[`panicking_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#panicking_unwrap
38263826
[`partialeq_ne_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl
3827+
[`partialeq_to_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_to_none
38273828
[`path_buf_push_overwrite`]: https://rust-lang.github.io/rust-clippy/master/index.html#path_buf_push_overwrite
38283829
[`pattern_type_mismatch`]: https://rust-lang.github.io/rust-clippy/master/index.html#pattern_type_mismatch
38293830
[`possible_missing_comma`]: https://rust-lang.github.io/rust-clippy/master/index.html#possible_missing_comma

clippy_lints/src/lib.register_all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
266266
LintId::of(option_env_unwrap::OPTION_ENV_UNWRAP),
267267
LintId::of(overflow_check_conditional::OVERFLOW_CHECK_CONDITIONAL),
268268
LintId::of(partialeq_ne_impl::PARTIALEQ_NE_IMPL),
269+
LintId::of(partialeq_to_none::PARTIALEQ_TO_NONE),
269270
LintId::of(precedence::PRECEDENCE),
270271
LintId::of(ptr::CMP_NULL),
271272
LintId::of(ptr::INVALID_NULL_PTR_USAGE),

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ store.register_lints(&[
454454
panic_unimplemented::UNIMPLEMENTED,
455455
panic_unimplemented::UNREACHABLE,
456456
partialeq_ne_impl::PARTIALEQ_NE_IMPL,
457+
partialeq_to_none::PARTIALEQ_TO_NONE,
457458
pass_by_ref_or_value::LARGE_TYPES_PASSED_BY_VALUE,
458459
pass_by_ref_or_value::TRIVIALLY_COPY_PASS_BY_REF,
459460
path_buf_push_overwrite::PATH_BUF_PUSH_OVERWRITE,

clippy_lints/src/lib.register_style.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ store.register_group(true, "clippy::style", Some("clippy_style"), vec![
100100
LintId::of(operators::ASSIGN_OP_PATTERN),
101101
LintId::of(operators::OP_REF),
102102
LintId::of(operators::PTR_EQ),
103+
LintId::of(partialeq_to_none::PARTIALEQ_TO_NONE),
103104
LintId::of(ptr::CMP_NULL),
104105
LintId::of(ptr::PTR_ARG),
105106
LintId::of(question_mark::QUESTION_MARK),

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ mod overflow_check_conditional;
332332
mod panic_in_result_fn;
333333
mod panic_unimplemented;
334334
mod partialeq_ne_impl;
335+
mod partialeq_to_none;
335336
mod pass_by_ref_or_value;
336337
mod path_buf_push_overwrite;
337338
mod pattern_type_mismatch;
@@ -931,6 +932,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
931932
store.register_late_pass(|| Box::new(invalid_utf8_in_unchecked::InvalidUtf8InUnchecked));
932933
store.register_late_pass(|| Box::new(std_instead_of_core::StdReexports::default()));
933934
store.register_late_pass(|| Box::new(manual_instant_elapsed::ManualInstantElapsed));
935+
store.register_late_pass(|| Box::new(partialeq_to_none::PartialeqToNone));
934936
// add lints here, do not remove this comment, it's used in `new_lint`
935937
}
936938

clippy_lints/src/partialeq_to_none.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use clippy_utils::{
2+
diagnostics::span_lint_and_sugg, is_lang_ctor, peel_hir_expr_refs, peel_ref_operators, sugg,
3+
ty::is_type_diagnostic_item,
4+
};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{BinOpKind, Expr, ExprKind, LangItem};
7+
use rustc_lint::{LateContext, LateLintPass};
8+
use rustc_session::{declare_lint_pass, declare_tool_lint};
9+
use rustc_span::sym;
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
///
14+
/// Checks for binary comparisons to a literal `Option::None`.
15+
///
16+
/// ### Why is this bad?
17+
///
18+
/// A programmer checking if some `foo` is `None` via a comparison `foo == None`
19+
/// is usually inspired from other programming languages (e.g. `foo is None`
20+
/// in Python).
21+
/// Checking if a value of type `Option<T>` is (not) equal to `None` in that
22+
/// way relies on `T: PartialEq` to do the comparison, which is unneeded.
23+
///
24+
/// ### Example
25+
/// ```rust
26+
/// fn foo(f: Option<u32>) -> &'static str {
27+
/// if f != None { "yay" } else { "nay" }
28+
/// }
29+
/// ```
30+
/// Use instead:
31+
/// ```rust
32+
/// fn foo(f: Option<u32>) -> &'static str {
33+
/// if f.is_some() { "yay" } else { "nay" }
34+
/// }
35+
/// ```
36+
#[clippy::version = "1.64.0"]
37+
pub PARTIALEQ_TO_NONE,
38+
style,
39+
"Binary comparison to `Option<T>::None` relies on `T: PartialEq`, which is unneeded"
40+
}
41+
declare_lint_pass!(PartialeqToNone => [PARTIALEQ_TO_NONE]);
42+
43+
impl<'tcx> LateLintPass<'tcx> for PartialeqToNone {
44+
fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) {
45+
// Skip expanded code, as we have no control over it anyway...
46+
if e.span.from_expansion() {
47+
return;
48+
}
49+
50+
// If the expression is of type `Option`
51+
let is_ty_option =
52+
|expr: &Expr<'_>| is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(expr).peel_refs(), sym::Option);
53+
54+
// If the expression is a literal `Option::None`
55+
let is_none_ctor = |expr: &Expr<'_>| {
56+
matches!(&peel_hir_expr_refs(expr).0.kind,
57+
ExprKind::Path(p) if is_lang_ctor(cx, p, LangItem::OptionNone))
58+
};
59+
60+
let mut applicability = Applicability::MachineApplicable;
61+
62+
if let ExprKind::Binary(op, left_side, right_side) = e.kind {
63+
// All other comparisons (e.g. `>= None`) have special meaning wrt T
64+
let is_eq = match op.node {
65+
BinOpKind::Eq => true,
66+
BinOpKind::Ne => false,
67+
_ => return,
68+
};
69+
70+
// We are only interested in comparisons between `Option` and a literal `Option::None`
71+
let scrutinee = match (
72+
is_none_ctor(left_side) && is_ty_option(right_side),
73+
is_none_ctor(right_side) && is_ty_option(left_side),
74+
) {
75+
(true, false) => right_side,
76+
(false, true) => left_side,
77+
_ => return,
78+
};
79+
80+
// Peel away refs/derefs (as long as we don't cross manual deref impls), as
81+
// autoref/autoderef will take care of those
82+
let sugg = format!(
83+
"{}.{}",
84+
sugg::Sugg::hir_with_applicability(cx, peel_ref_operators(cx, scrutinee), "..", &mut applicability)
85+
.maybe_par(),
86+
if is_eq { "is_none()" } else { "is_some()" }
87+
);
88+
89+
span_lint_and_sugg(
90+
cx,
91+
PARTIALEQ_TO_NONE,
92+
e.span,
93+
"binary comparison to literal `Option::None`",
94+
if is_eq {
95+
"use `Option::is_none()` instead"
96+
} else {
97+
"use `Option::is_some()` instead"
98+
},
99+
sugg,
100+
applicability,
101+
);
102+
}
103+
}
104+
}

tests/ui/ifs_same_cond.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ fn ifs_same_cond() {
3232
};
3333

3434
let mut v = vec![1];
35-
if v.pop() == None {
35+
if v.pop().is_none() {
3636
// ok, functions
37-
} else if v.pop() == None {
37+
} else if v.pop().is_none() {
3838
}
3939

4040
if v.len() == 42 {

tests/ui/manual_assert.edition2018.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fn main() {
1717
let c = Some(2);
1818
if !a.is_empty()
1919
&& a.len() == 3
20-
&& c != None
20+
&& c.is_some()
2121
&& !a.is_empty()
2222
&& a.len() == 3
2323
&& !a.is_empty()

tests/ui/manual_assert.edition2021.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fn main() {
1717
let c = Some(2);
1818
if !a.is_empty()
1919
&& a.len() == 3
20-
&& c != None
20+
&& c.is_some()
2121
&& !a.is_empty()
2222
&& a.len() == 3
2323
&& !a.is_empty()

tests/ui/manual_assert.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fn main() {
1111
let c = Some(2);
1212
if !a.is_empty()
1313
&& a.len() == 3
14-
&& c != None
14+
&& c.is_some()
1515
&& !a.is_empty()
1616
&& a.len() == 3
1717
&& !a.is_empty()

tests/ui/manual_assert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fn main() {
1717
let c = Some(2);
1818
if !a.is_empty()
1919
&& a.len() == 3
20-
&& c != None
20+
&& c.is_some()
2121
&& !a.is_empty()
2222
&& a.len() == 3
2323
&& !a.is_empty()

tests/ui/partialeq_to_none.fixed

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// run-rustfix
2+
#![warn(clippy::partialeq_to_none)]
3+
4+
struct Foobar;
5+
6+
impl PartialEq<Option<()>> for Foobar {
7+
fn eq(&self, _: &Option<()>) -> bool {
8+
false
9+
}
10+
}
11+
12+
#[allow(dead_code)]
13+
fn foo(f: Option<u32>) -> &'static str {
14+
if f.is_some() { "yay" } else { "nay" }
15+
}
16+
17+
fn foobar() -> Option<()> {
18+
None
19+
}
20+
21+
fn bar() -> Result<(), ()> {
22+
Ok(())
23+
}
24+
25+
fn optref() -> &'static &'static Option<()> {
26+
&&None
27+
}
28+
29+
fn main() {
30+
let x = Some(0);
31+
32+
let _ = x.is_none();
33+
let _ = x.is_some();
34+
let _ = x.is_none();
35+
let _ = x.is_some();
36+
37+
if foobar().is_none() {}
38+
39+
if bar().ok().is_some() {}
40+
41+
let _ = Some(1 + 2).is_some();
42+
43+
let _ = { Some(0) }.is_none();
44+
45+
let _ = {
46+
/*
47+
This comment runs long
48+
*/
49+
Some(1)
50+
}.is_some();
51+
52+
// Should not trigger, as `Foobar` is not an `Option` and has no `is_none`
53+
let _ = Foobar == None;
54+
55+
let _ = optref().is_none();
56+
let _ = optref().is_some();
57+
let _ = optref().is_none();
58+
let _ = optref().is_some();
59+
60+
let x = Box::new(Option::<()>::None);
61+
let _ = (*x).is_some();
62+
}

tests/ui/partialeq_to_none.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// run-rustfix
2+
#![warn(clippy::partialeq_to_none)]
3+
4+
struct Foobar;
5+
6+
impl PartialEq<Option<()>> for Foobar {
7+
fn eq(&self, _: &Option<()>) -> bool {
8+
false
9+
}
10+
}
11+
12+
#[allow(dead_code)]
13+
fn foo(f: Option<u32>) -> &'static str {
14+
if f != None { "yay" } else { "nay" }
15+
}
16+
17+
fn foobar() -> Option<()> {
18+
None
19+
}
20+
21+
fn bar() -> Result<(), ()> {
22+
Ok(())
23+
}
24+
25+
fn optref() -> &'static &'static Option<()> {
26+
&&None
27+
}
28+
29+
fn main() {
30+
let x = Some(0);
31+
32+
let _ = x == None;
33+
let _ = x != None;
34+
let _ = None == x;
35+
let _ = None != x;
36+
37+
if foobar() == None {}
38+
39+
if bar().ok() != None {}
40+
41+
let _ = Some(1 + 2) != None;
42+
43+
let _ = { Some(0) } == None;
44+
45+
let _ = {
46+
/*
47+
This comment runs long
48+
*/
49+
Some(1)
50+
} != None;
51+
52+
// Should not trigger, as `Foobar` is not an `Option` and has no `is_none`
53+
let _ = Foobar == None;
54+
55+
let _ = optref() == &&None;
56+
let _ = &&None != optref();
57+
let _ = **optref() == None;
58+
let _ = &None != *optref();
59+
60+
let x = Box::new(Option::<()>::None);
61+
let _ = None != *x;
62+
}

0 commit comments

Comments
 (0)