Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Empty file.
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.

```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,13 @@ use biome_analyze::{
};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_js_factory::make;
#[expect(unused_imports)]
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 +192,30 @@ 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 mut replacement = node_to_replace.clone();

// Only wrap in parentheses if this is a Boolean call inside a logical NOT with complex expression
if matches!(extra_boolean_cast_type, ExtraBooleanCastType::BooleanCall) {
let is_negated_boolean_call = node
.syntax()
.parent()
.and_then(JsUnaryExpression::cast)
.and_then(|expr| expr.operator().ok())
.is_some_and(|op| op == JsUnaryOperator::LogicalNot);

if is_negated_boolean_call && needs_parentheses_when_negated(node_to_replace) {
replacement =
AnyJsExpression::JsParenthesizedExpression(make::js_parenthesized_expression(
make::token(T!['(']),
replacement,
make::token(T![')']),
));
}
}

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

Some(JsRuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
Expand All @@ -199,6 +226,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,
}
}

/// 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);
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ new Boolean(!!x);
!!!x;

!Boolean(x);

// Test case for issue #7225 - should preserve parentheses
const b0 = false;
const b1 = false;
const boolean = !Boolean(b0 && b1);
```

# Diagnostics
Expand Down Expand Up @@ -226,6 +231,8 @@ invalid.js:23:2 lint/complexity/noExtraBooleanCast FIXABLE ━━━━━━
22 │
> 23 │ !Boolean(x);
│ ^^^^^^^^^^
24 │
25 │ // Test case for issue #7225 - should preserve parentheses

i It is not necessary to use `Boolean` call when a value will already be coerced to a boolean.

Expand All @@ -235,3 +242,22 @@ invalid.js:23:2 lint/complexity/noExtraBooleanCast FIXABLE ━━━━━━
│ -------- -

```

```
invalid.js:28:18 lint/complexity/noExtraBooleanCast FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Avoid redundant `Boolean` call

26 │ const b0 = false;
27 │ const b1 = false;
> 28 │ const boolean = !Boolean(b0 && b1);
│ ^^^^^^^^^^^^^^^^^

i It is not necessary to use `Boolean` call when a value will already be coerced to a boolean.

i Safe fix: Remove redundant `Boolean` call

28 │ const·boolean·=·!Boolean(b0·&&·b1);
│ -------

```
Loading