Skip to content

Commit a0b7681

Browse files
committed
fix: identity_op suggestions use correct parenthesis
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 either rustfix generating broken code and failing, or 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 broken code where `x` cannot be added to the other values, as it was never cast to `u64`. This commit fixes both of these cases by always checking the non-redundant child of a binary expression for needed parenthesis, and makes it so if we need parenthesis, but they already exist, we don't add any redundant ones. Fixes rust-lang#13470
1 parent 52b8324 commit a0b7681

File tree

4 files changed

+252
-105
lines changed

4 files changed

+252
-105
lines changed

clippy_lints/src/operators/identity_op.rs

+109-102
Original file line numberDiff line numberDiff line change
@@ -42,83 +42,53 @@ 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+
span_ineffective_operation(
56+
cx,
57+
expr.span,
58+
peeled_left_span,
59+
Parens::Unneeded,
60+
left_is_coerced_to_value,
61+
);
62+
}
7363
},
7464
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-
);
65+
if is_redundant_op(cx, left, 1) {
66+
let paren = needs_parenthesis(cx, expr, right);
67+
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
68+
} else if is_redundant_op(cx, right, 1) {
69+
let paren = needs_parenthesis(cx, expr, left);
70+
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
71+
}
9272
},
9373
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-
);
74+
if is_redundant_op(cx, right, 1) {
75+
span_ineffective_operation(
76+
cx,
77+
expr.span,
78+
peeled_left_span,
79+
Parens::Unneeded,
80+
left_is_coerced_to_value,
81+
);
82+
}
10383
},
10484
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-
);
85+
if is_redundant_op(cx, left, -1) {
86+
let paren = needs_parenthesis(cx, expr, right);
87+
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
88+
} else if is_redundant_op(cx, right, -1) {
89+
let paren = needs_parenthesis(cx, expr, left);
90+
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
91+
}
12292
},
12393
BinOpKind::Rem => check_remainder(cx, left, right, expr.span, left.span),
12494
_ => (),
@@ -138,43 +108,75 @@ enum Parens {
138108
Unneeded,
139109
}
140110

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

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-
};
170+
match parent {
171+
Node::Block(_) | Node::Stmt(_) => return Parens::Needed,
172+
_ => return Parens::Unneeded,
173+
};
174+
}
175+
},
176+
_ => {
177+
return Parens::Unneeded;
178+
},
176179
}
177-
178180
Parens::Needed
179181
}
180182

@@ -199,7 +201,7 @@ fn check_remainder(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>, span
199201
}
200202
}
201203

202-
fn check_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span, parens: Parens, is_erased: bool) -> bool {
204+
fn is_redundant_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8) -> bool {
203205
if let Some(Constant::Int(v)) = ConstEvalCtxt::new(cx).eval_simple(e).map(Constant::peel_refs) {
204206
let check = match *cx.typeck_results().expr_ty(e).peel_refs().kind() {
205207
ty::Int(ity) => unsext(cx.tcx, -1_i128, ity),
@@ -212,7 +214,6 @@ fn check_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span, pa
212214
1 => v == 1,
213215
_ => unreachable!(),
214216
} {
215-
span_ineffective_operation(cx, span, arg, parens, is_erased);
216217
return true;
217218
}
218219
}
@@ -234,7 +235,13 @@ fn span_ineffective_operation(
234235
expr_snippet.into_owned()
235236
};
236237
let suggestion = match parens {
237-
Parens::Needed => format!("({expr_snippet})"),
238+
Parens::Needed => {
239+
if !expr_snippet.starts_with('(') && !expr_snippet.ends_with(')') {
240+
format!("({expr_snippet})")
241+
} else {
242+
expr_snippet
243+
}
244+
},
238245
Parens::Unneeded => expr_snippet,
239246
};
240247

tests/ui/identity_op.fixed

+41-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ fn main() {
149149

150150
2 * { a };
151151
//~^ ERROR: this operation has no effect
152-
(({ a } + 4));
152+
({ a } + 4);
153153
//~^ ERROR: this operation has no effect
154154
1;
155155
//~^ ERROR: this operation has no effect
@@ -212,3 +212,43 @@ 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+
// Maintain parenthesis if the parent expr is of higher precedence
236+
let _ = 2i32 * (x + y);
237+
//~^ ERROR: this operation has no effect
238+
// No need for parenthesis if the parent expr is of equal precedence
239+
let _ = 2i32 + x + y;
240+
//~^ ERROR: this operation has no effect
241+
// But make sure that inner parens still exist
242+
let z = 1i32;
243+
let _ = 2 + x + (y * z);
244+
//~^ ERROR: this operation has no effect
245+
// Maintain parenthesis if the parent expr is of lower precedence
246+
// This is for clarity, and clippy will not warn on these being unnecessary
247+
let _ = 2i32 + (x * y);
248+
//~^ ERROR: this operation has no effect
249+
250+
let x = 1i16;
251+
let y = 1i16;
252+
let _: u64 = 1u64 + (x as i32 + y as i32) as u64;
253+
//~^ ERROR: this operation has no effect
254+
}

tests/ui/identity_op.rs

+40
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,43 @@ 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+
// Maintain parenthesis if the parent expr is of higher precedence
236+
let _ = 2i32 * (x + y + 0i32);
237+
//~^ ERROR: this operation has no effect
238+
// No need for parenthesis if the parent expr is of equal precedence
239+
let _ = 2i32 + (x + y + 0i32);
240+
//~^ ERROR: this operation has no effect
241+
// But make sure that inner parens still exist
242+
let z = 1i32;
243+
let _ = 2 + (x + (y * z) + 0);
244+
//~^ ERROR: this operation has no effect
245+
// Maintain parenthesis if the parent expr is of lower precedence
246+
// This is for clarity, and clippy will not warn on these being unnecessary
247+
let _ = 2i32 + (x * y * 1i32);
248+
//~^ ERROR: this operation has no effect
249+
250+
let x = 1i16;
251+
let y = 1i16;
252+
let _: u64 = 1u64 + ((x as i32 + y as i32) as u64 + 0u64);
253+
//~^ ERROR: this operation has no effect
254+
}

0 commit comments

Comments
 (0)