Skip to content

Commit 8cfb959

Browse files
authored
fix: identity_op suggestions use correct parenthesis (#13647)
The `identity_op` lint was suggesting code fixes that resulted in incorrect or broken code, due to missing parenthesis in the fix that changed the semantics of the code. For a binary expression, `left op right`, if the `left` was redundant, it would check if the right side needed parenthesis, but if the `right` was redundant, it would just assume that the left side did not need parenthesis. This can result in rustfix generating broken code and failing, or generating code that has different behavior than before the fix. e.g. `-(x + y + 0)` would turn into `-x + y`, changing the behavior, and `1u64 + (x + y + 0i32) as u64` where `x: i32` and `y: i32` would turn into `1u64 + x + y as u64`, creating an error where `x` cannot be added to the other values, as it was never cast to `u64`. This commit fixes both of these problems by always checking the non-redundant child of a binary expression for needed parenthesis. fixes #13470 changelog: [`identity_op`]: Fix suggested code that is broken or has changed behavior
2 parents f712eb5 + ef0f1ca commit 8cfb959

File tree

4 files changed

+245
-105
lines changed

4 files changed

+245
-105
lines changed

clippy_lints/src/operators/identity_op.rs

+88-102
Original file line numberDiff line numberDiff line change
@@ -42,83 +42,43 @@ pub(crate) fn check<'tcx>(
4242

4343
match op {
4444
BinOpKind::Add | BinOpKind::BitOr | BinOpKind::BitXor => {
45-
let _ = check_op(
46-
cx,
47-
left,
48-
0,
49-
expr.span,
50-
peeled_right_span,
51-
needs_parenthesis(cx, expr, right),
52-
right_is_coerced_to_value,
53-
) || check_op(
54-
cx,
55-
right,
56-
0,
57-
expr.span,
58-
peeled_left_span,
59-
Parens::Unneeded,
60-
left_is_coerced_to_value,
61-
);
45+
if is_redundant_op(cx, left, 0) {
46+
let paren = needs_parenthesis(cx, expr, right);
47+
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
48+
} else if is_redundant_op(cx, right, 0) {
49+
let paren = needs_parenthesis(cx, expr, left);
50+
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
51+
}
6252
},
6353
BinOpKind::Shl | BinOpKind::Shr | BinOpKind::Sub => {
64-
let _ = check_op(
65-
cx,
66-
right,
67-
0,
68-
expr.span,
69-
peeled_left_span,
70-
Parens::Unneeded,
71-
left_is_coerced_to_value,
72-
);
54+
if is_redundant_op(cx, right, 0) {
55+
let paren = needs_parenthesis(cx, expr, left);
56+
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
57+
}
7358
},
7459
BinOpKind::Mul => {
75-
let _ = check_op(
76-
cx,
77-
left,
78-
1,
79-
expr.span,
80-
peeled_right_span,
81-
needs_parenthesis(cx, expr, right),
82-
right_is_coerced_to_value,
83-
) || check_op(
84-
cx,
85-
right,
86-
1,
87-
expr.span,
88-
peeled_left_span,
89-
Parens::Unneeded,
90-
left_is_coerced_to_value,
91-
);
60+
if is_redundant_op(cx, left, 1) {
61+
let paren = needs_parenthesis(cx, expr, right);
62+
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
63+
} else if is_redundant_op(cx, right, 1) {
64+
let paren = needs_parenthesis(cx, expr, left);
65+
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
66+
}
9267
},
9368
BinOpKind::Div => {
94-
let _ = check_op(
95-
cx,
96-
right,
97-
1,
98-
expr.span,
99-
peeled_left_span,
100-
Parens::Unneeded,
101-
left_is_coerced_to_value,
102-
);
69+
if is_redundant_op(cx, right, 1) {
70+
let paren = needs_parenthesis(cx, expr, left);
71+
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
72+
}
10373
},
10474
BinOpKind::BitAnd => {
105-
let _ = check_op(
106-
cx,
107-
left,
108-
-1,
109-
expr.span,
110-
peeled_right_span,
111-
needs_parenthesis(cx, expr, right),
112-
right_is_coerced_to_value,
113-
) || check_op(
114-
cx,
115-
right,
116-
-1,
117-
expr.span,
118-
peeled_left_span,
119-
Parens::Unneeded,
120-
left_is_coerced_to_value,
121-
);
75+
if is_redundant_op(cx, left, -1) {
76+
let paren = needs_parenthesis(cx, expr, right);
77+
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
78+
} else if is_redundant_op(cx, right, -1) {
79+
let paren = needs_parenthesis(cx, expr, left);
80+
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
81+
}
12282
},
12383
BinOpKind::Rem => check_remainder(cx, left, right, expr.span, left.span),
12484
_ => (),
@@ -138,43 +98,70 @@ enum Parens {
13898
Unneeded,
13999
}
140100

141-
/// Checks if `left op right` needs parenthesis when reduced to `right`
101+
/// Checks if a binary expression needs parenthesis when reduced to just its
102+
/// right or left child.
103+
///
104+
/// e.g. `-(x + y + 0)` cannot be reduced to `-x + y`, as the behavior changes silently.
105+
/// e.g. `1u64 + ((x + y + 0i32) as u64)` cannot be reduced to `1u64 + x + y as u64`, since
106+
/// the the cast expression will not apply to the same expression.
142107
/// e.g. `0 + if b { 1 } else { 2 } + if b { 3 } else { 4 }` cannot be reduced
143108
/// to `if b { 1 } else { 2 } + if b { 3 } else { 4 }` where the `if` could be
144-
/// interpreted as a statement
109+
/// interpreted as a statement. The same behavior happens for `match`, `loop`,
110+
/// and blocks.
111+
/// e.g. `2 * (0 + { a })` can be reduced to `2 * { a }` without the need for parenthesis,
112+
/// but `1 * ({ a } + 4)` cannot be reduced to `{ a } + 4`, as a block at the start of a line
113+
/// will be interpreted as a statement instead of an expression.
145114
///
146-
/// See #8724
147-
fn needs_parenthesis(cx: &LateContext<'_>, binary: &Expr<'_>, right: &Expr<'_>) -> Parens {
148-
match right.kind {
115+
/// See #8724, #13470
116+
fn needs_parenthesis(cx: &LateContext<'_>, binary: &Expr<'_>, child: &Expr<'_>) -> Parens {
117+
match child.kind {
149118
ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) => {
150-
// ensure we're checking against the leftmost expression of `right`
151-
//
152-
// ~~~ `lhs`
153-
// 0 + {4} * 2
154-
// ~~~~~~~ `right`
155-
return needs_parenthesis(cx, binary, lhs);
119+
// For casts and binary expressions, we want to add parenthesis if
120+
// the parent HIR node is an expression, or if the parent HIR node
121+
// is a Block or Stmt, and the new left hand side would need
122+
// parenthesis be treated as a statement rather than an expression.
123+
if let Some((_, parent)) = cx.tcx.hir().parent_iter(binary.hir_id).next() {
124+
match parent {
125+
Node::Expr(_) => return Parens::Needed,
126+
Node::Block(_) | Node::Stmt(_) => {
127+
// ensure we're checking against the leftmost expression of `child`
128+
//
129+
// ~~~~~~~~~~~ `binary`
130+
// ~~~ `lhs`
131+
// 0 + {4} * 2
132+
// ~~~~~~~ `child`
133+
return needs_parenthesis(cx, binary, lhs);
134+
},
135+
_ => return Parens::Unneeded,
136+
}
137+
}
138+
},
139+
ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Block(..) | ExprKind::Loop(..) => {
140+
// For if, match, block, and loop expressions, we want to add parenthesis if
141+
// the closest ancestor node that is not an expression is a block or statement.
142+
// This would mean that the rustfix suggestion will appear at the start of a line, which causes
143+
// these expressions to be interpreted as statements if they do not have parenthesis.
144+
let mut prev_id = binary.hir_id;
145+
for (_, parent) in cx.tcx.hir().parent_iter(binary.hir_id) {
146+
if let Node::Expr(expr) = parent
147+
&& let ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) | ExprKind::Unary(_, lhs) = expr.kind
148+
&& lhs.hir_id == prev_id
149+
{
150+
// keep going until we find a node that encompasses left of `binary`
151+
prev_id = expr.hir_id;
152+
continue;
153+
}
154+
155+
match parent {
156+
Node::Block(_) | Node::Stmt(_) => return Parens::Needed,
157+
_ => return Parens::Unneeded,
158+
};
159+
}
160+
},
161+
_ => {
162+
return Parens::Unneeded;
156163
},
157-
ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Block(..) | ExprKind::Loop(..) => {},
158-
_ => return Parens::Unneeded,
159-
}
160-
161-
let mut prev_id = binary.hir_id;
162-
for (_, node) in cx.tcx.hir().parent_iter(binary.hir_id) {
163-
if let Node::Expr(expr) = node
164-
&& let ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) = expr.kind
165-
&& lhs.hir_id == prev_id
166-
{
167-
// keep going until we find a node that encompasses left of `binary`
168-
prev_id = expr.hir_id;
169-
continue;
170-
}
171-
172-
match node {
173-
Node::Block(_) | Node::Stmt(_) => break,
174-
_ => return Parens::Unneeded,
175-
};
176164
}
177-
178165
Parens::Needed
179166
}
180167

@@ -199,7 +186,7 @@ fn check_remainder(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>, span
199186
}
200187
}
201188

202-
fn check_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span, parens: Parens, is_erased: bool) -> bool {
189+
fn is_redundant_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8) -> bool {
203190
if let Some(Constant::Int(v)) = ConstEvalCtxt::new(cx).eval_simple(e).map(Constant::peel_refs) {
204191
let check = match *cx.typeck_results().expr_ty(e).peel_refs().kind() {
205192
ty::Int(ity) => unsext(cx.tcx, -1_i128, ity),
@@ -212,7 +199,6 @@ fn check_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span, pa
212199
1 => v == 1,
213200
_ => unreachable!(),
214201
} {
215-
span_ineffective_operation(cx, span, arg, parens, is_erased);
216202
return true;
217203
}
218204
}

tests/ui/identity_op.fixed

+45-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ fn main() {
116116
//~^ ERROR: this operation has no effect
117117
(match a { 0 => 10, _ => 20 }) + if b { 3 } else { 4 };
118118
//~^ ERROR: this operation has no effect
119-
(if b { 1 } else { 2 });
119+
((if b { 1 } else { 2 }));
120120
//~^ ERROR: this operation has no effect
121121

122122
({ a }) + 3;
@@ -212,3 +212,47 @@ fn issue_12050() {
212212
//~^ ERROR: this operation has no effect
213213
}
214214
}
215+
216+
fn issue_13470() {
217+
let x = 1i32;
218+
let y = 1i32;
219+
// Removes the + 0i32 while keeping the parentheses around x + y so the cast operation works
220+
let _: u64 = (x + y) as u64;
221+
//~^ ERROR: this operation has no effect
222+
// both of the next two lines should look the same after rustfix
223+
let _: u64 = 1u64 & (x + y) as u64;
224+
//~^ ERROR: this operation has no effect
225+
// Same as above, but with extra redundant parenthesis
226+
let _: u64 = 1u64 & ((x + y)) as u64;
227+
//~^ ERROR: this operation has no effect
228+
// Should maintain parenthesis even if the surrounding expr has the same precedence
229+
let _: u64 = 5u64 + ((x + y)) as u64;
230+
//~^ ERROR: this operation has no effect
231+
232+
// If we don't maintain the parens here, the behavior changes
233+
let _ = -(x + y);
234+
//~^ ERROR: this operation has no effect
235+
// Similarly, we need to maintain parens here
236+
let _ = -(x / y);
237+
//~^ ERROR: this operation has no effect
238+
// Maintain parenthesis if the parent expr is of higher precedence
239+
let _ = 2i32 * (x + y);
240+
//~^ ERROR: this operation has no effect
241+
// Maintain parenthesis if the parent expr is the same precedence
242+
// as not all operations are associative
243+
let _ = 2i32 - (x - y);
244+
//~^ ERROR: this operation has no effect
245+
// But make sure that inner parens still exist
246+
let z = 1i32;
247+
let _ = 2 + (x + (y * z));
248+
//~^ ERROR: this operation has no effect
249+
// Maintain parenthesis if the parent expr is of lower precedence
250+
// This is for clarity, and clippy will not warn on these being unnecessary
251+
let _ = 2i32 + (x * y);
252+
//~^ ERROR: this operation has no effect
253+
254+
let x = 1i16;
255+
let y = 1i16;
256+
let _: u64 = 1u64 + ((x as i32 + y as i32) as u64);
257+
//~^ ERROR: this operation has no effect
258+
}

tests/ui/identity_op.rs

+44
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,47 @@ fn issue_12050() {
212212
//~^ ERROR: this operation has no effect
213213
}
214214
}
215+
216+
fn issue_13470() {
217+
let x = 1i32;
218+
let y = 1i32;
219+
// Removes the + 0i32 while keeping the parentheses around x + y so the cast operation works
220+
let _: u64 = (x + y + 0i32) as u64;
221+
//~^ ERROR: this operation has no effect
222+
// both of the next two lines should look the same after rustfix
223+
let _: u64 = 1u64 & (x + y + 0i32) as u64;
224+
//~^ ERROR: this operation has no effect
225+
// Same as above, but with extra redundant parenthesis
226+
let _: u64 = 1u64 & ((x + y) + 0i32) as u64;
227+
//~^ ERROR: this operation has no effect
228+
// Should maintain parenthesis even if the surrounding expr has the same precedence
229+
let _: u64 = 5u64 + ((x + y) + 0i32) as u64;
230+
//~^ ERROR: this operation has no effect
231+
232+
// If we don't maintain the parens here, the behavior changes
233+
let _ = -(x + y + 0i32);
234+
//~^ ERROR: this operation has no effect
235+
// Similarly, we need to maintain parens here
236+
let _ = -(x / y / 1i32);
237+
//~^ ERROR: this operation has no effect
238+
// Maintain parenthesis if the parent expr is of higher precedence
239+
let _ = 2i32 * (x + y + 0i32);
240+
//~^ ERROR: this operation has no effect
241+
// Maintain parenthesis if the parent expr is the same precedence
242+
// as not all operations are associative
243+
let _ = 2i32 - (x - y - 0i32);
244+
//~^ ERROR: this operation has no effect
245+
// But make sure that inner parens still exist
246+
let z = 1i32;
247+
let _ = 2 + (x + (y * z) + 0);
248+
//~^ ERROR: this operation has no effect
249+
// Maintain parenthesis if the parent expr is of lower precedence
250+
// This is for clarity, and clippy will not warn on these being unnecessary
251+
let _ = 2i32 + (x * y * 1i32);
252+
//~^ ERROR: this operation has no effect
253+
254+
let x = 1i16;
255+
let y = 1i16;
256+
let _: u64 = 1u64 + ((x as i32 + y as i32) as u64 + 0u64);
257+
//~^ ERROR: this operation has no effect
258+
}

0 commit comments

Comments
 (0)