Skip to content

Commit

Permalink
fix: identity_op suggestions use correct parenthesis (#13647)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Jarcho authored Nov 9, 2024
2 parents f712eb5 + ef0f1ca commit 8cfb959
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 105 deletions.
190 changes: 88 additions & 102 deletions clippy_lints/src/operators/identity_op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,83 +42,43 @@ pub(crate) fn check<'tcx>(

match op {
BinOpKind::Add | BinOpKind::BitOr | BinOpKind::BitXor => {
let _ = check_op(
cx,
left,
0,
expr.span,
peeled_right_span,
needs_parenthesis(cx, expr, right),
right_is_coerced_to_value,
) || check_op(
cx,
right,
0,
expr.span,
peeled_left_span,
Parens::Unneeded,
left_is_coerced_to_value,
);
if is_redundant_op(cx, left, 0) {
let paren = needs_parenthesis(cx, expr, right);
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
} else if is_redundant_op(cx, right, 0) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::Shl | BinOpKind::Shr | BinOpKind::Sub => {
let _ = check_op(
cx,
right,
0,
expr.span,
peeled_left_span,
Parens::Unneeded,
left_is_coerced_to_value,
);
if is_redundant_op(cx, right, 0) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::Mul => {
let _ = check_op(
cx,
left,
1,
expr.span,
peeled_right_span,
needs_parenthesis(cx, expr, right),
right_is_coerced_to_value,
) || check_op(
cx,
right,
1,
expr.span,
peeled_left_span,
Parens::Unneeded,
left_is_coerced_to_value,
);
if is_redundant_op(cx, left, 1) {
let paren = needs_parenthesis(cx, expr, right);
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
} else if is_redundant_op(cx, right, 1) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::Div => {
let _ = check_op(
cx,
right,
1,
expr.span,
peeled_left_span,
Parens::Unneeded,
left_is_coerced_to_value,
);
if is_redundant_op(cx, right, 1) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::BitAnd => {
let _ = check_op(
cx,
left,
-1,
expr.span,
peeled_right_span,
needs_parenthesis(cx, expr, right),
right_is_coerced_to_value,
) || check_op(
cx,
right,
-1,
expr.span,
peeled_left_span,
Parens::Unneeded,
left_is_coerced_to_value,
);
if is_redundant_op(cx, left, -1) {
let paren = needs_parenthesis(cx, expr, right);
span_ineffective_operation(cx, expr.span, peeled_right_span, paren, right_is_coerced_to_value);
} else if is_redundant_op(cx, right, -1) {
let paren = needs_parenthesis(cx, expr, left);
span_ineffective_operation(cx, expr.span, peeled_left_span, paren, left_is_coerced_to_value);
}
},
BinOpKind::Rem => check_remainder(cx, left, right, expr.span, left.span),
_ => (),
Expand All @@ -138,43 +98,70 @@ enum Parens {
Unneeded,
}

/// Checks if `left op right` needs parenthesis when reduced to `right`
/// Checks if a binary expression needs parenthesis when reduced to just its
/// right or left child.
///
/// e.g. `-(x + y + 0)` cannot be reduced to `-x + y`, as the behavior changes silently.
/// e.g. `1u64 + ((x + y + 0i32) as u64)` cannot be reduced to `1u64 + x + y as u64`, since
/// the the cast expression will not apply to the same expression.
/// e.g. `0 + if b { 1 } else { 2 } + if b { 3 } else { 4 }` cannot be reduced
/// to `if b { 1 } else { 2 } + if b { 3 } else { 4 }` where the `if` could be
/// interpreted as a statement
/// interpreted as a statement. The same behavior happens for `match`, `loop`,
/// and blocks.
/// e.g. `2 * (0 + { a })` can be reduced to `2 * { a }` without the need for parenthesis,
/// but `1 * ({ a } + 4)` cannot be reduced to `{ a } + 4`, as a block at the start of a line
/// will be interpreted as a statement instead of an expression.
///
/// See #8724
fn needs_parenthesis(cx: &LateContext<'_>, binary: &Expr<'_>, right: &Expr<'_>) -> Parens {
match right.kind {
/// See #8724, #13470
fn needs_parenthesis(cx: &LateContext<'_>, binary: &Expr<'_>, child: &Expr<'_>) -> Parens {
match child.kind {
ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) => {
// ensure we're checking against the leftmost expression of `right`
//
// ~~~ `lhs`
// 0 + {4} * 2
// ~~~~~~~ `right`
return needs_parenthesis(cx, binary, lhs);
// For casts and binary expressions, we want to add parenthesis if
// the parent HIR node is an expression, or if the parent HIR node
// is a Block or Stmt, and the new left hand side would need
// parenthesis be treated as a statement rather than an expression.
if let Some((_, parent)) = cx.tcx.hir().parent_iter(binary.hir_id).next() {
match parent {
Node::Expr(_) => return Parens::Needed,
Node::Block(_) | Node::Stmt(_) => {
// ensure we're checking against the leftmost expression of `child`
//
// ~~~~~~~~~~~ `binary`
// ~~~ `lhs`
// 0 + {4} * 2
// ~~~~~~~ `child`
return needs_parenthesis(cx, binary, lhs);
},
_ => return Parens::Unneeded,
}
}
},
ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Block(..) | ExprKind::Loop(..) => {
// For if, match, block, and loop expressions, we want to add parenthesis if
// the closest ancestor node that is not an expression is a block or statement.
// This would mean that the rustfix suggestion will appear at the start of a line, which causes
// these expressions to be interpreted as statements if they do not have parenthesis.
let mut prev_id = binary.hir_id;
for (_, parent) in cx.tcx.hir().parent_iter(binary.hir_id) {
if let Node::Expr(expr) = parent
&& let ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) | ExprKind::Unary(_, lhs) = expr.kind
&& lhs.hir_id == prev_id
{
// keep going until we find a node that encompasses left of `binary`
prev_id = expr.hir_id;
continue;
}

match parent {
Node::Block(_) | Node::Stmt(_) => return Parens::Needed,
_ => return Parens::Unneeded,
};
}
},
_ => {
return Parens::Unneeded;
},
ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Block(..) | ExprKind::Loop(..) => {},
_ => return Parens::Unneeded,
}

let mut prev_id = binary.hir_id;
for (_, node) in cx.tcx.hir().parent_iter(binary.hir_id) {
if let Node::Expr(expr) = node
&& let ExprKind::Binary(_, lhs, _) | ExprKind::Cast(lhs, _) = expr.kind
&& lhs.hir_id == prev_id
{
// keep going until we find a node that encompasses left of `binary`
prev_id = expr.hir_id;
continue;
}

match node {
Node::Block(_) | Node::Stmt(_) => break,
_ => return Parens::Unneeded,
};
}

Parens::Needed
}

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

fn check_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span, parens: Parens, is_erased: bool) -> bool {
fn is_redundant_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8) -> bool {
if let Some(Constant::Int(v)) = ConstEvalCtxt::new(cx).eval_simple(e).map(Constant::peel_refs) {
let check = match *cx.typeck_results().expr_ty(e).peel_refs().kind() {
ty::Int(ity) => unsext(cx.tcx, -1_i128, ity),
Expand All @@ -212,7 +199,6 @@ fn check_op(cx: &LateContext<'_>, e: &Expr<'_>, m: i8, span: Span, arg: Span, pa
1 => v == 1,
_ => unreachable!(),
} {
span_ineffective_operation(cx, span, arg, parens, is_erased);
return true;
}
}
Expand Down
46 changes: 45 additions & 1 deletion tests/ui/identity_op.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ fn main() {
//~^ ERROR: this operation has no effect
(match a { 0 => 10, _ => 20 }) + if b { 3 } else { 4 };
//~^ ERROR: this operation has no effect
(if b { 1 } else { 2 });
((if b { 1 } else { 2 }));
//~^ ERROR: this operation has no effect

({ a }) + 3;
Expand Down Expand Up @@ -212,3 +212,47 @@ fn issue_12050() {
//~^ ERROR: this operation has no effect
}
}

fn issue_13470() {
let x = 1i32;
let y = 1i32;
// Removes the + 0i32 while keeping the parentheses around x + y so the cast operation works
let _: u64 = (x + y) as u64;
//~^ ERROR: this operation has no effect
// both of the next two lines should look the same after rustfix
let _: u64 = 1u64 & (x + y) as u64;
//~^ ERROR: this operation has no effect
// Same as above, but with extra redundant parenthesis
let _: u64 = 1u64 & ((x + y)) as u64;
//~^ ERROR: this operation has no effect
// Should maintain parenthesis even if the surrounding expr has the same precedence
let _: u64 = 5u64 + ((x + y)) as u64;
//~^ ERROR: this operation has no effect

// If we don't maintain the parens here, the behavior changes
let _ = -(x + y);
//~^ ERROR: this operation has no effect
// Similarly, we need to maintain parens here
let _ = -(x / y);
//~^ ERROR: this operation has no effect
// Maintain parenthesis if the parent expr is of higher precedence
let _ = 2i32 * (x + y);
//~^ ERROR: this operation has no effect
// Maintain parenthesis if the parent expr is the same precedence
// as not all operations are associative
let _ = 2i32 - (x - y);
//~^ ERROR: this operation has no effect
// But make sure that inner parens still exist
let z = 1i32;
let _ = 2 + (x + (y * z));
//~^ ERROR: this operation has no effect
// Maintain parenthesis if the parent expr is of lower precedence
// This is for clarity, and clippy will not warn on these being unnecessary
let _ = 2i32 + (x * y);
//~^ ERROR: this operation has no effect

let x = 1i16;
let y = 1i16;
let _: u64 = 1u64 + ((x as i32 + y as i32) as u64);
//~^ ERROR: this operation has no effect
}
44 changes: 44 additions & 0 deletions tests/ui/identity_op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,47 @@ fn issue_12050() {
//~^ ERROR: this operation has no effect
}
}

fn issue_13470() {
let x = 1i32;
let y = 1i32;
// Removes the + 0i32 while keeping the parentheses around x + y so the cast operation works
let _: u64 = (x + y + 0i32) as u64;
//~^ ERROR: this operation has no effect
// both of the next two lines should look the same after rustfix
let _: u64 = 1u64 & (x + y + 0i32) as u64;
//~^ ERROR: this operation has no effect
// Same as above, but with extra redundant parenthesis
let _: u64 = 1u64 & ((x + y) + 0i32) as u64;
//~^ ERROR: this operation has no effect
// Should maintain parenthesis even if the surrounding expr has the same precedence
let _: u64 = 5u64 + ((x + y) + 0i32) as u64;
//~^ ERROR: this operation has no effect

// If we don't maintain the parens here, the behavior changes
let _ = -(x + y + 0i32);
//~^ ERROR: this operation has no effect
// Similarly, we need to maintain parens here
let _ = -(x / y / 1i32);
//~^ ERROR: this operation has no effect
// Maintain parenthesis if the parent expr is of higher precedence
let _ = 2i32 * (x + y + 0i32);
//~^ ERROR: this operation has no effect
// Maintain parenthesis if the parent expr is the same precedence
// as not all operations are associative
let _ = 2i32 - (x - y - 0i32);
//~^ ERROR: this operation has no effect
// But make sure that inner parens still exist
let z = 1i32;
let _ = 2 + (x + (y * z) + 0);
//~^ ERROR: this operation has no effect
// Maintain parenthesis if the parent expr is of lower precedence
// This is for clarity, and clippy will not warn on these being unnecessary
let _ = 2i32 + (x * y * 1i32);
//~^ ERROR: this operation has no effect

let x = 1i16;
let y = 1i16;
let _: u64 = 1u64 + ((x as i32 + y as i32) as u64 + 0u64);
//~^ ERROR: this operation has no effect
}
Loading

0 comments on commit 8cfb959

Please sign in to comment.