@@ -3,9 +3,13 @@ use biome_analyze::{
33} ;
44use biome_console:: markup;
55use biome_diagnostics:: Severity ;
6+ use biome_js_factory:: make;
7+ #[ expect( unused_imports) ]
68use biome_js_syntax:: {
7- AnyJsExpression , JsCallArgumentList , JsCallArguments , JsCallExpression , JsNewExpression ,
8- JsSyntaxNode , JsUnaryOperator , is_in_boolean_context, is_negation,
9+ AnyJsExpression , JsAssignmentExpression , JsBinaryExpression , JsCallArgumentList ,
10+ JsCallArguments , JsCallExpression , JsConditionalExpression , JsLogicalExpression ,
11+ JsNewExpression , JsParenthesizedExpression , JsSequenceExpression , JsSyntaxNode ,
12+ JsUnaryExpression , JsUnaryOperator , T , is_in_boolean_context, is_negation,
913} ;
1014use biome_rowan:: { AstNode , AstSeparatedList , BatchMutationExt } ;
1115use biome_rule_options:: no_extra_boolean_cast:: NoExtraBooleanCastOptions ;
@@ -188,7 +192,30 @@ impl Rule for NoExtraBooleanCast {
188192 ExtraBooleanCastType :: DoubleNegation => "Remove redundant double-negation" ,
189193 ExtraBooleanCastType :: BooleanCall => "Remove redundant `Boolean` call" ,
190194 } ;
191- mutation. replace_node ( node. clone ( ) , node_to_replace. clone ( ) ) ;
195+
196+ // Check if the Boolean call is inside a unary negation and the argument needs parentheses
197+ let mut replacement = node_to_replace. clone ( ) ;
198+
199+ // Only wrap in parentheses if this is a Boolean call inside a logical NOT with complex expression
200+ if matches ! ( extra_boolean_cast_type, ExtraBooleanCastType :: BooleanCall ) {
201+ let is_negated_boolean_call = node
202+ . syntax ( )
203+ . parent ( )
204+ . and_then ( JsUnaryExpression :: cast)
205+ . and_then ( |expr| expr. operator ( ) . ok ( ) )
206+ . is_some_and ( |op| op == JsUnaryOperator :: LogicalNot ) ;
207+
208+ if is_negated_boolean_call && needs_parentheses_when_negated ( node_to_replace) {
209+ replacement =
210+ AnyJsExpression :: JsParenthesizedExpression ( make:: js_parenthesized_expression (
211+ make:: token ( T ! [ '(' ] ) ,
212+ replacement,
213+ make:: token ( T ! [ ')' ] ) ,
214+ ) ) ;
215+ }
216+ }
217+
218+ mutation. replace_node ( node. clone ( ) , replacement) ;
192219
193220 Some ( JsRuleAction :: new (
194221 ctx. metadata ( ) . action_category ( ctx. category ( ) , ctx. group ( ) ) ,
@@ -199,6 +226,27 @@ impl Rule for NoExtraBooleanCast {
199226 }
200227}
201228
229+ /// Determines if an expression needs parentheses when it becomes the operand of a unary negation.
230+ /// This is needed to preserve operator precedence for expressions like binary expressions.
231+ fn needs_parentheses_when_negated ( expr : & AnyJsExpression ) -> bool {
232+ match expr {
233+ // Binary expressions like `a + b` need parentheses in `!(a + b)` to maintain precedence
234+ AnyJsExpression :: JsBinaryExpression ( _) => true ,
235+ // Logical expressions like `a && b` need parentheses in `!(a && b)` to maintain precedence
236+ AnyJsExpression :: JsLogicalExpression ( _) => true ,
237+ // Conditional expressions like `a ? b : c` need parentheses
238+ AnyJsExpression :: JsConditionalExpression ( _) => true ,
239+ // Assignment expressions need parentheses
240+ AnyJsExpression :: JsAssignmentExpression ( _) => true ,
241+ // Sequence expressions (comma operator) need parentheses
242+ AnyJsExpression :: JsSequenceExpression ( _) => true ,
243+ // Logical expressions that are already parenthesized don't need additional ones
244+ AnyJsExpression :: JsParenthesizedExpression ( _) => false ,
245+ // Simple expressions like identifiers, literals, calls don't need parentheses
246+ _ => false ,
247+ }
248+ }
249+
202250/// Check if the SyntaxNode is a Double Negation. Including the edge case
203251/// ```js
204252/// !(!x)
0 commit comments