Skip to content

Commit c41c410

Browse files
committed
Auto merge of rust-lang#8786 - Alexendoo:identity-op-suggestions, r=dswij,xFrednet
`identity_op`: add parenthesis to suggestions where required changelog: [`identity_op`]: add parenthesis to suggestions where required Follow up to rust-lang#8730, wraps the cases we can't lint as-is in parenthesis rather than ignoring them Catches a couple new FPs with mixed operator precedences and `as` casts ```rust // such as 0 + { a } * 2; 0 + a as usize; ``` The suggestions are now applied using `span_lint_and_sugg` rather than appearing in just the message and have a `run-rustfix` test
2 parents 6269ab1 + ee8fae3 commit c41c410

File tree

6 files changed

+377
-202
lines changed

6 files changed

+377
-202
lines changed

clippy_lints/src/identity_op.rs

+72-53
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
use clippy_utils::get_parent_expr;
2-
use clippy_utils::source::snippet;
3-
use rustc_hir::{BinOp, BinOpKind, Expr, ExprKind};
1+
use clippy_utils::consts::{constant_full_int, constant_simple, Constant, FullInt};
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::source::snippet_with_applicability;
4+
use clippy_utils::{clip, unsext};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{BinOp, BinOpKind, Expr, ExprKind, Node};
47
use rustc_lint::{LateContext, LateLintPass};
58
use rustc_middle::ty;
69
use rustc_session::{declare_lint_pass, declare_tool_lint};
710
use rustc_span::source_map::Span;
811

9-
use clippy_utils::consts::{constant_full_int, constant_simple, Constant, FullInt};
10-
use clippy_utils::diagnostics::span_lint;
11-
use clippy_utils::{clip, unsext};
12-
1312
declare_clippy_lint! {
1413
/// ### What it does
1514
/// Checks for identity operations, e.g., `x + 0`.
@@ -23,11 +22,6 @@ declare_clippy_lint! {
2322
/// # let x = 1;
2423
/// x / 1 + 0 * 1 - 0 | 0;
2524
/// ```
26-
///
27-
/// ### Known problems
28-
/// False negatives: `f(0 + if b { 1 } else { 2 } + 3);` is reducible to
29-
/// `f(if b { 1 } else { 2 } + 3);`. But the lint doesn't trigger for the code.
30-
/// See [#8724](https://github.com/rust-lang/rust-clippy/issues/8724)
3125
#[clippy::version = "pre 1.29.0"]
3226
pub IDENTITY_OP,
3327
complexity,
@@ -45,56 +39,73 @@ impl<'tcx> LateLintPass<'tcx> for IdentityOp {
4539
if !is_allowed(cx, *cmp, left, right) {
4640
match cmp.node {
4741
BinOpKind::Add | BinOpKind::BitOr | BinOpKind::BitXor => {
48-
if reducible_to_right(cx, expr, right) {
49-
check(cx, left, 0, expr.span, right.span);
50-
}
51-
check(cx, right, 0, expr.span, left.span);
42+
check(cx, left, 0, expr.span, right.span, needs_parenthesis(cx, expr, right));
43+
check(cx, right, 0, expr.span, left.span, Parens::Unneeded);
5244
},
5345
BinOpKind::Shl | BinOpKind::Shr | BinOpKind::Sub => {
54-
check(cx, right, 0, expr.span, left.span);
46+
check(cx, right, 0, expr.span, left.span, Parens::Unneeded);
5547
},
5648
BinOpKind::Mul => {
57-
if reducible_to_right(cx, expr, right) {
58-
check(cx, left, 1, expr.span, right.span);
59-
}
60-
check(cx, right, 1, expr.span, left.span);
49+
check(cx, left, 1, expr.span, right.span, needs_parenthesis(cx, expr, right));
50+
check(cx, right, 1, expr.span, left.span, Parens::Unneeded);
6151
},
62-
BinOpKind::Div => check(cx, right, 1, expr.span, left.span),
52+
BinOpKind::Div => check(cx, right, 1, expr.span, left.span, Parens::Unneeded),
6353
BinOpKind::BitAnd => {
64-
if reducible_to_right(cx, expr, right) {
65-
check(cx, left, -1, expr.span, right.span);
66-
}
67-
check(cx, right, -1, expr.span, left.span);
68-
},
69-
BinOpKind::Rem => {
70-
// Don't call reducible_to_right because N % N is always reducible to 1
71-
check_remainder(cx, left, right, expr.span, left.span);
54+
check(cx, left, -1, expr.span, right.span, needs_parenthesis(cx, expr, right));
55+
check(cx, right, -1, expr.span, left.span, Parens::Unneeded);
7256
},
57+
BinOpKind::Rem => check_remainder(cx, left, right, expr.span, left.span),
7358
_ => (),
7459
}
7560
}
7661
}
7762
}
7863
}
7964

80-
/// Checks if `left op ..right` can be actually reduced to `right`
81-
/// e.g. `0 + if b { 1 } else { 2 } + if b { 3 } else { 4 }`
82-
/// cannot be reduced to `if b { 1 } else { 2 } + if b { 3 } else { 4 }`
65+
#[derive(Copy, Clone)]
66+
enum Parens {
67+
Needed,
68+
Unneeded,
69+
}
70+
71+
/// Checks if `left op right` needs parenthesis when reduced to `right`
72+
/// e.g. `0 + if b { 1 } else { 2 } + if b { 3 } else { 4 }` cannot be reduced
73+
/// to `if b { 1 } else { 2 } + if b { 3 } else { 4 }` where the `if` could be
74+
/// interpreted as a statement
75+
///
8376
/// See #8724
84-
fn reducible_to_right(cx: &LateContext<'_>, binary: &Expr<'_>, right: &Expr<'_>) -> bool {
85-
if let ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Block(..) | ExprKind::Loop(..) = right.kind {
86-
is_toplevel_binary(cx, binary)
87-
} else {
88-
true
77+
fn needs_parenthesis(cx: &LateContext<'_>, binary: &Expr<'_>, right: &Expr<'_>) -> Parens {
78+
match right.kind {
79+
ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) => {
80+
// ensure we're checking against the leftmost expression of `right`
81+
//
82+
// ~~~ `lhs`
83+
// 0 + {4} * 2
84+
// ~~~~~~~ `right`
85+
return needs_parenthesis(cx, binary, lhs);
86+
},
87+
ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Block(..) | ExprKind::Loop(..) => {},
88+
_ => return Parens::Unneeded,
8989
}
90-
}
9190

92-
fn is_toplevel_binary(cx: &LateContext<'_>, must_be_binary: &Expr<'_>) -> bool {
93-
if let Some(parent) = get_parent_expr(cx, must_be_binary) && let ExprKind::Binary(..) = &parent.kind {
94-
false
95-
} else {
96-
true
91+
let mut prev_id = binary.hir_id;
92+
for (_, node) in cx.tcx.hir().parent_iter(binary.hir_id) {
93+
if let Node::Expr(expr) = node
94+
&& let ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) = expr.kind
95+
&& lhs.hir_id == prev_id
96+
{
97+
// keep going until we find a node that encompasses left of `binary`
98+
prev_id = expr.hir_id;
99+
continue;
100+
}
101+
102+
match node {
103+
Node::Block(_) | Node::Stmt(_) => break,
104+
_ => return Parens::Unneeded,
105+
};
97106
}
107+
108+
Parens::Needed
98109
}
99110

100111
fn is_allowed(cx: &LateContext<'_>, cmp: BinOp, left: &Expr<'_>, right: &Expr<'_>) -> bool {
@@ -115,11 +126,11 @@ fn check_remainder(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>, span
115126
(Some(FullInt::U(lv)), Some(FullInt::U(rv))) => lv < rv,
116127
_ => return,
117128
} {
118-
span_ineffective_operation(cx, span, arg);
129+
span_ineffective_operation(cx, span, arg, Parens::Unneeded);
119130
}
120131
}
121132

122-
fn check(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span) {
133+
fn check(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span, parens: Parens) {
123134
if let Some(Constant::Int(v)) = constant_simple(cx, cx.typeck_results(), e).map(Constant::peel_refs) {
124135
let check = match *cx.typeck_results().expr_ty(e).peel_refs().kind() {
125136
ty::Int(ity) => unsext(cx.tcx, -1_i128, ity),
@@ -132,19 +143,27 @@ fn check(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span) {
132143
1 => v == 1,
133144
_ => unreachable!(),
134145
} {
135-
span_ineffective_operation(cx, span, arg);
146+
span_ineffective_operation(cx, span, arg, parens);
136147
}
137148
}
138149
}
139150

140-
fn span_ineffective_operation(cx: &LateContext<'_>, span: Span, arg: Span) {
141-
span_lint(
151+
fn span_ineffective_operation(cx: &LateContext<'_>, span: Span, arg: Span, parens: Parens) {
152+
let mut applicability = Applicability::MachineApplicable;
153+
let expr_snippet = snippet_with_applicability(cx, arg, "..", &mut applicability);
154+
155+
let suggestion = match parens {
156+
Parens::Needed => format!("({expr_snippet})"),
157+
Parens::Unneeded => expr_snippet.into_owned(),
158+
};
159+
160+
span_lint_and_sugg(
142161
cx,
143162
IDENTITY_OP,
144163
span,
145-
&format!(
146-
"the operation is ineffective. Consider reducing it to `{}`",
147-
snippet(cx, arg, "..")
148-
),
164+
"this operation has no effect",
165+
"consider reducing it to",
166+
suggestion,
167+
applicability,
149168
);
150169
}

tests/ui/identity_op.fixed

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// run-rustfix
2+
3+
#![warn(clippy::identity_op)]
4+
#![allow(
5+
clippy::eq_op,
6+
clippy::no_effect,
7+
clippy::unnecessary_operation,
8+
clippy::op_ref,
9+
clippy::double_parens,
10+
unused
11+
)]
12+
13+
use std::fmt::Write as _;
14+
15+
const ONE: i64 = 1;
16+
const NEG_ONE: i64 = -1;
17+
const ZERO: i64 = 0;
18+
19+
struct A(String);
20+
21+
impl std::ops::Shl<i32> for A {
22+
type Output = A;
23+
fn shl(mut self, other: i32) -> Self {
24+
let _ = write!(self.0, "{}", other);
25+
self
26+
}
27+
}
28+
29+
struct Length(u8);
30+
struct Meter;
31+
32+
impl core::ops::Mul<Meter> for u8 {
33+
type Output = Length;
34+
fn mul(self, _: Meter) -> Length {
35+
Length(self)
36+
}
37+
}
38+
39+
#[rustfmt::skip]
40+
fn main() {
41+
let x = 0;
42+
43+
x;
44+
x;
45+
x + 1;
46+
x;
47+
1 + x;
48+
x - ZERO; //no error, as we skip lookups (for now)
49+
x;
50+
((ZERO)) | x; //no error, as we skip lookups (for now)
51+
52+
x;
53+
x;
54+
x / ONE; //no error, as we skip lookups (for now)
55+
56+
x / 2; //no false positive
57+
58+
x & NEG_ONE; //no error, as we skip lookups (for now)
59+
x;
60+
61+
let u: u8 = 0;
62+
u;
63+
64+
1 << 0; // no error, this case is allowed, see issue 3430
65+
42;
66+
1;
67+
42;
68+
&x;
69+
x;
70+
71+
let mut a = A("".into());
72+
let b = a << 0; // no error: non-integer
73+
74+
1 * Meter; // no error: non-integer
75+
76+
2;
77+
-2;
78+
2 + x;
79+
-2 + x;
80+
x + 1;
81+
(x + 1) % 3; // no error
82+
4 % 3; // no error
83+
4 % -3; // no error
84+
85+
// See #8724
86+
let a = 0;
87+
let b = true;
88+
(if b { 1 } else { 2 });
89+
(if b { 1 } else { 2 }) + if b { 3 } else { 4 };
90+
(match a { 0 => 10, _ => 20 });
91+
(match a { 0 => 10, _ => 20 }) + match a { 0 => 30, _ => 40 };
92+
(if b { 1 } else { 2 }) + match a { 0 => 30, _ => 40 };
93+
(match a { 0 => 10, _ => 20 }) + if b { 3 } else { 4 };
94+
(if b { 1 } else { 2 });
95+
96+
({ a }) + 3;
97+
({ a } * 2);
98+
(loop { let mut c = 0; if c == 10 { break c; } c += 1; }) + { a * 2 };
99+
100+
fn f(_: i32) {
101+
todo!();
102+
}
103+
f(a + { 8 * 5 });
104+
f(if b { 1 } else { 2 } + 3);
105+
const _: i32 = { 2 * 4 } + 3;
106+
const _: i32 = { 1 + 2 * 3 } + 3;
107+
108+
a as usize;
109+
let _ = a as usize;
110+
({ a } as usize);
111+
112+
2 * { a };
113+
(({ a } + 4));
114+
1;
115+
}
116+
117+
pub fn decide(a: bool, b: bool) -> u32 {
118+
(if a { 1 } else { 2 }) + if b { 3 } else { 5 }
119+
}

tests/ui/identity_op.rs

+36-27
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
// run-rustfix
2+
3+
#![warn(clippy::identity_op)]
4+
#![allow(
5+
clippy::eq_op,
6+
clippy::no_effect,
7+
clippy::unnecessary_operation,
8+
clippy::op_ref,
9+
clippy::double_parens,
10+
unused
11+
)]
12+
113
use std::fmt::Write as _;
214

315
const ONE: i64 = 1;
@@ -24,14 +36,6 @@ impl core::ops::Mul<Meter> for u8 {
2436
}
2537
}
2638

27-
#[allow(
28-
clippy::eq_op,
29-
clippy::no_effect,
30-
clippy::unnecessary_operation,
31-
clippy::op_ref,
32-
clippy::double_parens
33-
)]
34-
#[warn(clippy::identity_op)]
3539
#[rustfmt::skip]
3640
fn main() {
3741
let x = 0;
@@ -82,29 +86,34 @@ fn main() {
8286
let a = 0;
8387
let b = true;
8488
0 + if b { 1 } else { 2 };
85-
0 + if b { 1 } else { 2 } + if b { 3 } else { 4 }; // no error
89+
0 + if b { 1 } else { 2 } + if b { 3 } else { 4 };
8690
0 + match a { 0 => 10, _ => 20 };
87-
0 + match a { 0 => 10, _ => 20 } + match a { 0 => 30, _ => 40 }; // no error
88-
0 + if b { 1 } else { 2 } + match a { 0 => 30, _ => 40 }; // no error
89-
0 + match a { 0 => 10, _ => 20 } + if b { 3 } else { 4 }; // no error
90-
91-
0 + if b { 0 + 1 } else { 2 };
92-
0 + match a { 0 => 0 + 10, _ => 20 };
93-
0 + if b { 0 + 1 } else { 2 } + match a { 0 => 0 + 30, _ => 40 };
94-
95-
let _ = 0 + if 0 + 1 > 0 { 1 } else { 2 } + if 0 + 1 > 0 { 3 } else { 4 };
96-
let _ = 0 + match 0 + 1 { 0 => 10, _ => 20 } + match 0 + 1 { 0 => 30, _ => 40 };
97-
98-
0 + if b { 1 } else { 2 } + if b { 3 } else { 4 } + 0;
99-
100-
0 + { a } + 3; // no error
101-
0 + loop { let mut c = 0; if c == 10 { break c; } c += 1; } + { a * 2 }; // no error
102-
91+
0 + match a { 0 => 10, _ => 20 } + match a { 0 => 30, _ => 40 };
92+
0 + if b { 1 } else { 2 } + match a { 0 => 30, _ => 40 };
93+
0 + match a { 0 => 10, _ => 20 } + if b { 3 } else { 4 };
94+
(if b { 1 } else { 2 }) + 0;
95+
96+
0 + { a } + 3;
97+
0 + { a } * 2;
98+
0 + loop { let mut c = 0; if c == 10 { break c; } c += 1; } + { a * 2 };
99+
103100
fn f(_: i32) {
104101
todo!();
105102
}
106103
f(1 * a + { 8 * 5 });
107-
f(0 + if b { 1 } else { 2 } + 3); // no error
104+
f(0 + if b { 1 } else { 2 } + 3);
108105
const _: i32 = { 2 * 4 } + 0 + 3;
109-
const _: i32 = 0 + { 1 + 2 * 3 } + 3; // no error
106+
const _: i32 = 0 + { 1 + 2 * 3 } + 3;
107+
108+
0 + a as usize;
109+
let _ = 0 + a as usize;
110+
0 + { a } as usize;
111+
112+
2 * (0 + { a });
113+
1 * ({ a } + 4);
114+
1 * 1;
115+
}
116+
117+
pub fn decide(a: bool, b: bool) -> u32 {
118+
0 + if a { 1 } else { 2 } + if b { 3 } else { 5 }
110119
}

0 commit comments

Comments
 (0)