Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a14fcca
test: add test case for noExtraBooleanCast parentheses preservation
JeetuSuthar Aug 17, 2025
4e5ffe6
docs: improve changeset description following guidelines
JeetuSuthar Aug 17, 2025
57b2305
docs: update changeset to clarify test-only addition per review
JeetuSuthar Aug 17, 2025
32ba1e7
chore: remove changeset .
JeetuSuthar Aug 17, 2025
27c1c87
fix: preserve parentheses in noExtraBooleanCast rule when removing Bo…
JeetuSuthar Aug 17, 2025
3b2aebe
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 17, 2025
2196fdc
fix: add support for logical and sequence expressions in parentheses …
JeetuSuthar Aug 17, 2025
040defa
Resolve merge conflict and integrate CodeRabbit review feedback
JeetuSuthar Aug 17, 2025
600d5fa
Fix unused imports warning with allow attribute
JeetuSuthar Aug 17, 2025
0500c6b
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 17, 2025
1617b75
Fix unused imports warning with allow attribute
JeetuSuthar Aug 17, 2025
d2e9574
refactor action function and remove test files
JeetuSuthar Aug 17, 2025
4fedd9f
Fix clippy lint: use #[expect] instead of #[allow]
JeetuSuthar Aug 17, 2025
fbac80e
[autofix.ci] apply automated fixes
autofix-ci[bot] Aug 17, 2025
7a79776
test: update
JeetuSuthar Aug 17, 2025
3e8c850
Merge update
JeetuSuthar Aug 17, 2025
70066e5
fix
JeetuSuthar Aug 17, 2025
81ad304
Merge branch 'main' into fix-boolean-cast-parentheses
ematipico Aug 21, 2025
e1f57f3
Update .changeset/fix-boolean-cast-parentheses.md
JeetuSuthar Aug 21, 2025
4f07e18
Remove test files that shouldn't be in the PR
JeetuSuthar Aug 21, 2025
196f806
Merge remote changes and remove unwanted test files
JeetuSuthar Aug 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/fix-boolean-cast-parentheses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@biomejs/biome": patch
---

Fixed [#7225](https://github.com/biomejs/biome/issues/7225): The noExtraBooleanCast rule now preserves parentheses when removing Boolean calls inside negations.
Comment thread
JeetuSuthar marked this conversation as resolved.
Outdated

```js
// Before
!Boolean(b0 && b1)
// After
!(b0 && b1) // instead of !b0 && b1
```
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ use biome_analyze::{
};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_js_factory::make;
use biome_js_syntax::{
AnyJsExpression, JsCallArgumentList, JsCallArguments, JsCallExpression, JsNewExpression,
JsSyntaxNode, JsUnaryOperator, is_in_boolean_context, is_negation,
AnyJsExpression, JsAssignmentExpression, JsBinaryExpression, JsCallArgumentList,
JsCallArguments, JsCallExpression, JsConditionalExpression, JsLogicalExpression, JsNewExpression,
JsParenthesizedExpression, JsSequenceExpression, JsSyntaxNode, JsUnaryExpression, JsUnaryOperator, T,
is_in_boolean_context, is_negation,
};
use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt};
use biome_rule_options::no_extra_boolean_cast::NoExtraBooleanCastOptions;
Expand Down Expand Up @@ -188,7 +191,36 @@ impl Rule for NoExtraBooleanCast {
ExtraBooleanCastType::DoubleNegation => "Remove redundant double-negation",
ExtraBooleanCastType::BooleanCall => "Remove redundant `Boolean` call",
};
mutation.replace_node(node.clone(), node_to_replace.clone());

// Check if the Boolean call is inside a unary negation and the argument needs parentheses
let replacement = if matches!(extra_boolean_cast_type, ExtraBooleanCastType::BooleanCall) {
// Check if this Boolean call is inside a unary negation
if let Some(unary_expr) = node.syntax().parent().and_then(JsUnaryExpression::cast) {
if matches!(unary_expr.operator(), Ok(JsUnaryOperator::LogicalNot)) {
// Check if the argument is a complex expression that needs parentheses

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can definitely reduce the indentation here a bit. Something like:

if let Some(unary_expr) = node.syntax().parent().and_then(JsUnaryExpression::cast).map(|e| e.operator()).ok().is_some_and(|op| op == JsUnaryOperator::LogicalNot)

if needs_parentheses_when_negated(node_to_replace) {
// Wrap in parentheses to preserve operator precedence
AnyJsExpression::JsParenthesizedExpression(
make::js_parenthesized_expression(
make::token(T!['(']),
node_to_replace.clone(),
make::token(T![')']),
),
)
} else {
node_to_replace.clone()
}
} else {
node_to_replace.clone()
}
} else {
node_to_replace.clone()
}
} else {
node_to_replace.clone()
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And you can definitely refactor this so that these elses aren't necessary.

Something like this probably:

let mut replacement = ...;

if cond {
    replacement = AnyJsExpression::JsParenthesizedExpression(
                            make::js_parenthesized_expression(
                                make::token(T!['(']),
                                replacement,
                                make::token(T![')']),
                            ),
                        )
}

mutation.replace_node(node.clone(), replacement);


mutation.replace_node(node.clone(), replacement);

Some(JsRuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
Expand All @@ -199,6 +231,27 @@ impl Rule for NoExtraBooleanCast {
}
}

/// Determines if an expression needs parentheses when it becomes the operand of a unary negation.
/// This is needed to preserve operator precedence for expressions like binary expressions.
fn needs_parentheses_when_negated(expr: &AnyJsExpression) -> bool {
match expr {
// Binary expressions like `a + b` need parentheses in `!(a + b)` to maintain precedence
AnyJsExpression::JsBinaryExpression(_) => true,
// Logical expressions like `a && b` need parentheses in `!(a && b)` to maintain precedence
AnyJsExpression::JsLogicalExpression(_) => true,
// Conditional expressions like `a ? b : c` need parentheses
AnyJsExpression::JsConditionalExpression(_) => true,
// Assignment expressions need parentheses
AnyJsExpression::JsAssignmentExpression(_) => true,
// Sequence expressions (comma operator) need parentheses
AnyJsExpression::JsSequenceExpression(_) => true,
// Logical expressions that are already parenthesized don't need additional ones
AnyJsExpression::JsParenthesizedExpression(_) => false,
// Simple expressions like identifiers, literals, calls don't need parentheses
_ => false,
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Check if the SyntaxNode is a Double Negation. Including the edge case
/// ```js
/// !(!x)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ new Boolean(!!x);

!!!x;

!Boolean(x);
!Boolean(x);

// Test case for issue #7225 - should preserve parentheses
const b0 = false;
const b1 = false;
const boolean = !Boolean(b0 && b1);
Empty file added reproduction_test.js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: to remove

Empty file.
Empty file added test_boolean_cast_bug.js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: to remove

Empty file.
Empty file added test_case.js

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: to remove

Empty file.
Loading