diff --git a/.changeset/use-nullish-coalescing-ternary.md b/.changeset/use-nullish-coalescing-ternary.md new file mode 100644 index 000000000000..a656152dff61 --- /dev/null +++ b/.changeset/use-nullish-coalescing-ternary.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +`useNullishCoalescing` now also detects ternary expressions that check for `null` or `undefined` and suggests rewriting them with `??`. A new `ignoreTernaryTests` option allows disabling this behavior. diff --git a/crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs b/crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs index 1b3b705f1bc9..819cabc02ab5 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs @@ -1,4 +1,4 @@ -use crate::{JsRuleAction, services::typed::Typed}; +use crate::{JsRuleAction, services::typed::Typed, utils::is_node_equal}; use biome_analyze::{ FixKind, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule, }; @@ -7,13 +7,13 @@ use biome_diagnostics::Severity; use biome_js_factory::make; use biome_js_syntax::{ AnyJsAssignmentPattern, AnyJsExpression, JsAssignmentExpression, JsAssignmentOperator, - JsConditionalExpression, JsDoWhileStatement, JsForStatement, JsIfStatement, - JsLogicalExpression, JsLogicalOperator, JsParenthesizedExpression, JsSyntaxKind, - JsWhileStatement, T, + JsBinaryOperator, JsConditionalExpression, JsDoWhileStatement, JsForStatement, + JsIfStatement, JsLogicalExpression, JsLogicalOperator, JsParenthesizedExpression, + JsSyntaxKind, JsWhileStatement, OperatorPrecedence, T, }; -use biome_js_type_info::ConditionalType; +use biome_js_type_info::{ConditionalType, TypeData}; use biome_rowan::{ - AstNode, BatchMutationExt, TextRange, declare_node_union, + AstNode, BatchMutationExt, TextRange, declare_node_union, trim_leading_trivia_pieces, }; use biome_rule_options::use_nullish_coalescing::UseNullishCoalescingOptions; @@ -32,6 +32,10 @@ declare_lint_rule! { /// For `||=` assignment expressions, the same logic applies: `a ||= b` is /// flagged when `a` is possibly nullish and can be rewritten as `a ??= b`. /// + /// For ternary expressions, this rule detects patterns like `x !== null ? x : y` + /// and suggests rewriting them as `x ?? y`. This applies to strict and loose + /// equality checks against `null` or `undefined`, including compound checks. + /// /// By default, `||` expressions in conditional test positions (if/while/for/ternary) /// are ignored, as the falsy-checking behavior is often intentional there. This can /// be disabled with the `ignoreConditionalTests` option. @@ -55,6 +59,16 @@ declare_lint_rule! { /// x ||= 'default'; // should use ??= /// ``` /// + /// ```ts,expect_diagnostic + /// declare const x: string | null; + /// const value = x !== null ? x : 'default'; // should use ?? + /// ``` + /// + /// ```ts,expect_diagnostic + /// declare const x: string | null; + /// const value = x == null ? 'default' : x; // should use ?? + /// ``` + /// /// ### Valid /// /// ```ts @@ -83,6 +97,53 @@ declare_lint_rule! { /// y ??= 'default'; /// ``` /// + /// ## Options + /// + /// ### ignoreConditionalTests + /// + /// Ignore `||` expressions inside conditional test positions + /// (if/while/for/do-while/ternary conditions), where falsy-checking + /// behavior may be intentional. + /// + /// **Default:** `true` + /// + /// Setting this to `false` will report `||` in conditional tests: + /// + /// ```json,options + /// { + /// "options": { + /// "ignoreConditionalTests": false + /// } + /// } + /// ``` + /// + /// ```ts,use_options + /// declare const cond: string | null; + /// if (cond || 'fallback') {} + /// ``` + /// + /// ### ignoreTernaryTests + /// + /// Ignore ternary expressions that check for `null` or `undefined` + /// and could be replaced with `??`. + /// + /// **Default:** `false` + /// + /// Setting this to `true` will suppress ternary diagnostics: + /// + /// ```json,options + /// { + /// "options": { + /// "ignoreTernaryTests": true + /// } + /// } + /// ``` + /// + /// ```ts,use_options + /// declare const x: string | null; + /// const value = x !== null ? x : 'default'; // no diagnostic + /// ``` + /// pub UseNullishCoalescing { version: "2.4.5", name: "useNullishCoalescing", @@ -97,7 +158,7 @@ declare_lint_rule! { } declare_node_union! { - pub UseNullishCoalescingQuery = JsLogicalExpression | JsAssignmentExpression + pub UseNullishCoalescingQuery = JsLogicalExpression | JsAssignmentExpression | JsConditionalExpression } pub enum UseNullishCoalescingState { @@ -109,6 +170,13 @@ pub enum UseNullishCoalescingState { operator_range: TextRange, can_fix: bool, }, + Ternary { + test_range: TextRange, + checked_expr: AnyJsExpression, + fallback_expr: AnyJsExpression, + is_positive: bool, + can_fix: bool, + }, } impl Rule for UseNullishCoalescing { @@ -125,6 +193,9 @@ impl Rule for UseNullishCoalescing { UseNullishCoalescingQuery::JsAssignmentExpression(assignment) => { run_logical_or_assignment(ctx, assignment) } + UseNullishCoalescingQuery::JsConditionalExpression(ternary) => { + run_ternary(ctx, ternary) + } } } @@ -154,41 +225,120 @@ impl Rule for UseNullishCoalescing { "The ""||="" operator assigns when the left side is falsy, while ""??="" only assigns when it is null or undefined." }), ), + UseNullishCoalescingState::Ternary { test_range, .. } => Some( + RuleDiagnostic::new( + rule_category!(), + *test_range, + markup! { + "Prefer ""??"" over a ternary expression checking for nullish." + }, + ), + ), } } fn action(ctx: &RuleContext, state: &Self::State) -> Option { - let (can_fix, replacement_token, message) = match state { - UseNullishCoalescingState::LogicalOr { can_fix, .. } => ( - *can_fix, - T![??], - markup! { "Use ""??"" instead." }.to_owned(), - ), - UseNullishCoalescingState::LogicalOrAssignment { can_fix, .. } => ( - *can_fix, - T![??=], - markup! { "Use ""??="" instead." }.to_owned(), - ), - }; - - if !can_fix { - return None; - } + let query = ctx.query(); + let mut mutation = ctx.root().begin(); - let old_token = match ctx.query() { - UseNullishCoalescingQuery::JsLogicalExpression(node) => node.operator_token().ok()?, - UseNullishCoalescingQuery::JsAssignmentExpression(node) => { - node.operator_token().ok()? + let message = match (state, query) { + ( + UseNullishCoalescingState::LogicalOr { can_fix, .. }, + UseNullishCoalescingQuery::JsLogicalExpression(logical), + ) => { + if !can_fix { + return None; + } + let old_token = logical.operator_token().ok()?; + + let new_token = make::token(T![??]) + .with_leading_trivia_pieces(old_token.leading_trivia().pieces()) + .with_trailing_trivia_pieces(old_token.trailing_trivia().pieces()); + + mutation.replace_token(old_token, new_token); + markup! { "Use ""??"" instead." }.to_owned() + } + ( + UseNullishCoalescingState::LogicalOrAssignment { can_fix, .. }, + UseNullishCoalescingQuery::JsAssignmentExpression(assignment), + ) => { + if !can_fix { + return None; + } + let old_token = assignment.operator_token().ok()?; + + let new_token = make::token(T![??=]) + .with_leading_trivia_pieces(old_token.leading_trivia().pieces()) + .with_trailing_trivia_pieces(old_token.trailing_trivia().pieces()); + + mutation.replace_token(old_token, new_token); + markup! { "Use ""??="" instead." }.to_owned() } + ( + UseNullishCoalescingState::Ternary { + checked_expr, + fallback_expr, + is_positive, + can_fix, + .. + }, + UseNullishCoalescingQuery::JsConditionalExpression(ternary), + ) => { + if !can_fix { + return None; + } + + // Strip trailing whitespace. Ternary layout trivia is not appropriate for `??`. + let checked_expr = checked_expr.clone().trim_trailing_trivia()?; + let fallback_expr = fallback_expr.clone().trim_trailing_trivia()?; + + // Transfer trivia from the ? and : tokens to the branch expressions they precede. + let question = ternary.question_mark_token().ok()?; + let colon = ternary.colon_token().ok()?; + let question_trivia = + trim_leading_trivia_pieces(question.trailing_trivia().pieces()); + let colon_trivia = + trim_leading_trivia_pieces(colon.trailing_trivia().pieces()); + + let (checked_expr, fallback_expr) = if *is_positive { + ( + checked_expr.prepend_trivia_pieces(question_trivia)?, + fallback_expr.prepend_trivia_pieces(colon_trivia)?, + ) + } else { + ( + checked_expr.prepend_trivia_pieces(colon_trivia)?, + fallback_expr.prepend_trivia_pieces(question_trivia)?, + ) + }; + + let checked = maybe_parenthesize_for_nullish(checked_expr); + let fallback = maybe_parenthesize_for_nullish(fallback_expr); + + let new_expr = make::js_logical_expression( + checked, + make::token_decorated_with_space(T![??]), + fallback, + ); + + // Transfer leading/trailing trivia from the original ternary + let new_expr = AnyJsExpression::from(new_expr) + .prepend_trivia_pieces( + ternary.syntax().first_leading_trivia()?.pieces(), + )? + .append_trivia_pieces( + ternary.syntax().last_trailing_trivia()?.pieces(), + )?; + + mutation.replace_node_discard_trivia( + AnyJsExpression::from(ternary.clone()), + new_expr, + ); + markup! { "Use ""??"" instead." }.to_owned() + } + _ => return None, }; - let new_token = make::token(replacement_token) - .with_leading_trivia_pieces(old_token.leading_trivia().pieces()) - .with_trailing_trivia_pieces(old_token.trailing_trivia().pieces()); - - let mut mutation = ctx.root().begin(); - mutation.replace_token(old_token, new_token); - Some(JsRuleAction::new( ctx.metadata().action_category(ctx.category(), ctx.group()), ctx.metadata().applicability(), @@ -285,6 +435,10 @@ fn is_possibly_nullish(ty: &biome_js_type_info::Type) -> bool { } fn is_safe_syntax_context_for_replacement(logical: &JsLogicalExpression) -> bool { + // Check if the expression is wrapped in parentheses. If it is, the replacement + // is safe even inside another logical expression because the parens preserve + // grouping. If it's NOT parenthesized and sits inside another logical expression, + // the replacement could change precedence, so we skip the fix. let is_parenthesized = logical .syntax() .parent() @@ -339,21 +493,21 @@ fn is_in_test_position(logical: &JsLogicalExpression) -> bool { for ancestor in logical.syntax().ancestors() { let is_in_test = match ancestor.kind() { - JsSyntaxKind::JS_IF_STATEMENT => { - test_contains_logical(JsIfStatement::cast_ref(&ancestor).and_then(|s| s.test().ok())) - } - JsSyntaxKind::JS_WHILE_STATEMENT => { - test_contains_logical(JsWhileStatement::cast_ref(&ancestor).and_then(|s| s.test().ok())) - } - JsSyntaxKind::JS_FOR_STATEMENT => { - test_contains_logical(JsForStatement::cast_ref(&ancestor).and_then(|s| s.test())) - } - JsSyntaxKind::JS_DO_WHILE_STATEMENT => { - test_contains_logical(JsDoWhileStatement::cast_ref(&ancestor).and_then(|s| s.test().ok())) - } - JsSyntaxKind::JS_CONDITIONAL_EXPRESSION => { - test_contains_logical(JsConditionalExpression::cast_ref(&ancestor).and_then(|s| s.test().ok())) - } + JsSyntaxKind::JS_IF_STATEMENT => test_contains_logical( + JsIfStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()), + ), + JsSyntaxKind::JS_WHILE_STATEMENT => test_contains_logical( + JsWhileStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()), + ), + JsSyntaxKind::JS_FOR_STATEMENT => test_contains_logical( + JsForStatement::cast_ref(&ancestor).and_then(|s| s.test()), + ), + JsSyntaxKind::JS_DO_WHILE_STATEMENT => test_contains_logical( + JsDoWhileStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()), + ), + JsSyntaxKind::JS_CONDITIONAL_EXPRESSION => test_contains_logical( + JsConditionalExpression::cast_ref(&ancestor).and_then(|s| s.test().ok()), + ), _ => false, }; if is_in_test { @@ -362,3 +516,241 @@ fn is_in_test_position(logical: &JsLogicalExpression) -> bool { } false } + +fn run_ternary( + ctx: &RuleContext, + ternary: &JsConditionalExpression, +) -> Option { + if ctx.options().ignore_ternary_tests() { + return None; + } + + let test = ternary.test().ok()?; + let consequent = ternary.consequent().ok()?; + let alternate = ternary.alternate().ok()?; + + let (checked_expr, fallback_expr, is_positive, check_kind) = + check_ternary_nullish_pattern(&test, &consequent, &alternate)?; + + // The fix is unsafe when the checked expression contains calls or `new`, because + // the ternary evaluates it twice (test + branch) while `??` evaluates it once. + let has_side_effects = contains_call_or_new_expression(&checked_expr); + + let can_fix = !has_side_effects + && match check_kind { + // Loose equality and compound strict checks cover both null and undefined + NullishCheckKind::Loose | NullishCheckKind::Compound => true, + // A single strict check only covers one nullish variant. The fix to `??` + // is safe only if the type cannot be the opposite variant. + NullishCheckKind::StrictSingle(lit) => { + let ty = ctx.type_of_expression(&checked_expr); + match lit { + NullishLiteral::Null => !type_has_undefined(&ty), + NullishLiteral::Undefined => !type_has_null(&ty), + } + } + }; + + Some(UseNullishCoalescingState::Ternary { + test_range: test.syntax().text_trimmed_range(), + checked_expr, + fallback_expr, + is_positive, + can_fix, + }) +} + +/// Returns `(checked, fallback, is_positive, check_kind)` if the ternary can be converted to `??`. +/// `is_positive` is true when checked = consequent (e.g. `x !== null ? x : y`). +fn check_ternary_nullish_pattern( + test: &AnyJsExpression, + consequent: &AnyJsExpression, + alternate: &AnyJsExpression, +) -> Option<(AnyJsExpression, AnyJsExpression, bool, NullishCheckKind)> { + // `x !== null ? x : y` or `x != null ? x : y` + if let Some((_, kind)) = match_nullish_check( + test, + consequent, + JsBinaryOperator::Inequality, + JsBinaryOperator::StrictInequality, + JsLogicalOperator::LogicalAnd, + ) { + return Some((consequent.clone(), alternate.clone(), true, kind)); + } + + // `x === null ? y : x` or `x == null ? y : x` + if let Some((_, kind)) = match_nullish_check( + test, + alternate, + JsBinaryOperator::Equality, + JsBinaryOperator::StrictEquality, + JsLogicalOperator::LogicalOr, + ) { + return Some((alternate.clone(), consequent.clone(), false, kind)); + } + + None +} + +/// Returns the checked expression and the kind of nullish check if the test matches +/// a nullish comparison pattern (loose, strict, or compound) using the given operator family. +fn match_nullish_check( + test: &AnyJsExpression, + value: &AnyJsExpression, + loose_op: JsBinaryOperator, + strict_op: JsBinaryOperator, + compound_logical_op: JsLogicalOperator, +) -> Option<(AnyJsExpression, NullishCheckKind)> { + // Loose: `x != null` or `x == null` + if let Some((checked, _)) = extract_nullish_comparison_operand(test, loose_op) + && expressions_equivalent(&checked, value) + { + return Some((checked, NullishCheckKind::Loose)); + } + + // Strict: `x !== null` or `x === undefined` + if let Some((checked, lit)) = extract_nullish_comparison_operand(test, strict_op) + && expressions_equivalent(&checked, value) + { + return Some((checked, NullishCheckKind::StrictSingle(lit))); + } + + // Compound: `x !== null && x !== undefined` or `x === null || x === undefined` + if let Some(logical) = test.as_js_logical_expression() + && logical.operator().ok()? == compound_logical_op + { + let left = logical.left().ok()?; + let right = logical.right().ok()?; + + if let (Some((checked_left, lit_left)), Some((checked_right, lit_right))) = ( + extract_nullish_comparison_operand(&left, strict_op), + extract_nullish_comparison_operand(&right, strict_op), + ) && expressions_equivalent(&checked_left, &checked_right) + && expressions_equivalent(&checked_left, value) + { + // Both sides must test different literals (one null, one undefined) + // to be a true compound check. If both test the same literal, + // treat it as a single strict check. + if lit_left != lit_right { + return Some((checked_left, NullishCheckKind::Compound)); + } + return Some((checked_left, NullishCheckKind::StrictSingle(lit_left))); + } + } + + None +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum NullishLiteral { + Null, + Undefined, +} + +#[derive(Clone, Copy)] +enum NullishCheckKind { + /// Loose equality: `!= null` or `== null` (covers both null and undefined) + Loose, + /// Single strict equality: `!== null` or `=== undefined` (covers only one) + StrictSingle(NullishLiteral), + /// Compound strict: `!== null && !== undefined` (covers both) + Compound, +} + +/// Returns the non-nullish operand of a binary comparison against `null` or `undefined`. +/// +/// Checks the right operand first because the conventional form places the literal +/// on the right (`x !== null`). The left check handles the reversed form (`null !== x`). +fn extract_nullish_comparison_operand( + expr: &AnyJsExpression, + expected_op: JsBinaryOperator, +) -> Option<(AnyJsExpression, NullishLiteral)> { + let binary = expr.as_js_binary_expression()?; + if binary.operator().ok()? != expected_op { + return None; + } + let left = binary.left().ok()?; + let right = binary.right().ok()?; + if let Some(lit) = nullish_literal_kind(&right) { + return Some((left, lit)); + } + if let Some(lit) = nullish_literal_kind(&left) { + return Some((right, lit)); + } + None +} + +fn nullish_literal_kind(expr: &AnyJsExpression) -> Option { + use biome_js_syntax::static_value::StaticValue; + match expr.as_static_value()? { + StaticValue::Null(_) => Some(NullishLiteral::Null), + StaticValue::Undefined(_) => Some(NullishLiteral::Undefined), + _ => None, + } +} + +fn expressions_equivalent(a: &AnyJsExpression, b: &AnyJsExpression) -> bool { + is_node_equal(a.syntax(), b.syntax()) +} + +/// Returns true if the expression contains a call or `new` expression. +/// +/// In a ternary like `foo() !== null ? foo() : y`, the subject is evaluated twice +/// (once in the test, once in the branch). Replacing with `foo() ?? y` evaluates it +/// once, which is a semantic change if the call has side effects. +fn contains_call_or_new_expression(expr: &AnyJsExpression) -> bool { + expr.syntax().descendants().any(|node| { + matches!( + node.kind(), + JsSyntaxKind::JS_CALL_EXPRESSION | JsSyntaxKind::JS_NEW_EXPRESSION + ) + }) +} + +fn type_has_null(ty: &biome_js_type_info::Type) -> bool { + let is_null = |t: &biome_js_type_info::Type| { + t.resolved_data() + .is_some_and(|d| matches!(d.as_raw_data(), TypeData::Null)) + }; + if ty.is_union() { + ty.flattened_union_variants().any(|v| is_null(&v)) + } else { + is_null(ty) + } +} + +fn type_has_undefined(ty: &biome_js_type_info::Type) -> bool { + let is_undef = |t: &biome_js_type_info::Type| { + t.resolved_data() + .is_some_and(|d| matches!(d.as_raw_data(), TypeData::Undefined | TypeData::VoidKeyword)) + }; + if ty.is_union() { + ty.flattened_union_variants().any(|v| is_undef(&v)) + } else { + is_undef(ty) + } +} + +/// Wraps the expression in parentheses if it would be invalid or change meaning +/// as an operand of `??`. This covers two cases: +/// - Expressions with lower precedence than `??` (conditional, assignment, yield, comma) +/// - `||` and `&&` expressions, which cannot be mixed with `??` without parentheses +fn maybe_parenthesize_for_nullish(expr: AnyJsExpression) -> AnyJsExpression { + if expr + .precedence() + .is_ok_and(|p| p < OperatorPrecedence::Coalesce) + { + return make::parenthesized(expr).into(); + } + if let Some(logical) = expr.as_js_logical_expression() + && logical.operator().is_ok_and(|op| { + matches!( + op, + JsLogicalOperator::LogicalOr | JsLogicalOperator::LogicalAnd + ) + }) + { + return make::parenthesized(expr).into(); + } + expr +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.options.json b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.options.json new file mode 100644 index 000000000000..109d171a6190 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.options.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "rules": { + "nursery": { + "useNullishCoalescing": { + "level": "error", + "options": { + "ignoreTernaryTests": true + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts new file mode 100644 index 000000000000..0481bd1f8ba6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts @@ -0,0 +1,10 @@ +// should not generate diagnostics for ternary (ignoreTernaryTests: true) + +declare const a: string | null; +const r1 = a !== null ? a : 'default'; + +declare const b: string | undefined; +const r2 = b === undefined ? 'default' : b; + +declare const c: string | null; +const r3 = c != null ? c : 'default'; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts.snap new file mode 100644 index 000000000000..607144576a7d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts.snap @@ -0,0 +1,19 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 155 +expression: ignoreTernaryTestsEnabled.ts +--- +# Input +```ts +// should not generate diagnostics for ternary (ignoreTernaryTests: true) + +declare const a: string | null; +const r1 = a !== null ? a : 'default'; + +declare const b: string | undefined; +const r2 = b === undefined ? 'default' : b; + +declare const c: string | null; +const r3 = c != null ? c : 'default'; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts new file mode 100644 index 000000000000..c3af745696f9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts @@ -0,0 +1,11 @@ +// Comments around ternary parts should be preserved +declare const x: string | null; +const a = x !== null ? /* non-null value */ x : /* fallback */ 'default'; + +// Comment before test +declare const y: string | null; +const b = /* check */ y === null ? 'fallback' : y; + +// Negative form with comments on both branches +declare const z: string | null; +const c = z === null ? /* fallback */ 'default' : /* value */ z; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts.snap new file mode 100644 index 000000000000..cfe11350f4d8 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts.snap @@ -0,0 +1,103 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 155 +expression: ternaryCommentTrivia.ts +--- +# Input +```ts +// Comments around ternary parts should be preserved +declare const x: string | null; +const a = x !== null ? /* non-null value */ x : /* fallback */ 'default'; + +// Comment before test +declare const y: string | null; +const b = /* check */ y === null ? 'fallback' : y; + +// Negative form with comments on both branches +declare const z: string | null; +const c = z === null ? /* fallback */ 'default' : /* value */ z; + +``` + +# Diagnostics +``` +ternaryCommentTrivia.ts:3:11 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 1 │ // Comments around ternary parts should be preserved + 2 │ declare const x: string | null; + > 3 │ const a = x !== null ? /* non-null value */ x : /* fallback */ 'default'; + │ ^^^^^^^^^^ + 4 │ + 5 │ // Comment before test + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 1 1 │ // Comments around ternary parts should be preserved + 2 2 │ declare const x: string | null; + 3 │ - const·a·=·x·!==·null·?·/*·non-null·value·*/·x·:·/*·fallback·*/·'default'; + 3 │ + const·a·=·/*·non-null·value·*/·x·??·/*·fallback·*/·'default'; + 4 4 │ + 5 5 │ // Comment before test + + +``` + +``` +ternaryCommentTrivia.ts:7:23 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 5 │ // Comment before test + 6 │ declare const y: string | null; + > 7 │ const b = /* check */ y === null ? 'fallback' : y; + │ ^^^^^^^^^^ + 8 │ + 9 │ // Negative form with comments on both branches + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 5 5 │ // Comment before test + 6 6 │ declare const y: string | null; + 7 │ - const·b·=·/*·check·*/·y·===·null·?·'fallback'·:·y; + 7 │ + const·b·=·/*·check·*/·y·??·'fallback'; + 8 8 │ + 9 9 │ // Negative form with comments on both branches + + +``` + +``` +ternaryCommentTrivia.ts:11:11 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 9 │ // Negative form with comments on both branches + 10 │ declare const z: string | null; + > 11 │ const c = z === null ? /* fallback */ 'default' : /* value */ z; + │ ^^^^^^^^^^ + 12 │ + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 9 9 │ // Negative form with comments on both branches + 10 10 │ declare const z: string | null; + 11 │ - const·c·=·z·===·null·?·/*·fallback·*/·'default'·:·/*·value·*/·z; + 11 │ + const·c·=·/*·value·*/·z·??·/*·fallback·*/·'default'; + 12 12 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts new file mode 100644 index 000000000000..f8ec5c61c65f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts @@ -0,0 +1,149 @@ +// Strict !== null +declare const a: string | null; +const r1 = a !== null ? a : 'default'; + +// Strict !== undefined +declare const b: string | undefined; +const r2 = b !== undefined ? b : 'fallback'; + +// Loose != null +declare const c: string | null; +const r3 = c != null ? c : 'default'; + +// Inverted === null +declare const d: string | null; +const r4 = d === null ? 'default' : d; + +// Inverted === undefined +declare const e: string | undefined; +const r5 = e === undefined ? 'fallback' : e; + +// Inverted == null +declare const f: string | null; +const r6 = f == null ? 'default' : f; + +// Compound !== null && !== undefined +declare const g: string | null | undefined; +const r7 = g !== null && g !== undefined ? g : 'default'; + +// Compound === null || === undefined +declare const h: string | null | undefined; +const r8 = h === null || h === undefined ? 'default' : h; + +// Null on left side +declare const i: string | null; +const r9 = null !== i ? i : 'default'; + +// Undefined on left side +declare const j: string | undefined; +const r10 = undefined !== j ? j : 'fallback'; + +// Member access +declare const obj: { prop: string | null }; +const r11 = obj.prop !== null ? obj.prop : 'default'; + +// Loose != undefined +declare const l: string | undefined; +const r12 = l != undefined ? l : 'default'; + +// Inverted, null on left +declare const n: string | null; +const r13 = null === n ? 'default' : n; + +// Strict single with null | undefined (diagnostic only, no fix) +declare const nu: string | null | undefined; +const r17 = nu !== null ? nu : 'default'; + +// Fallback is conditional (needs parens) +declare const p: string | null; +const r14 = p !== null ? p : p ? 'a' : 'b'; + +// Fallback is logical OR (needs parens, ?? cannot mix with ||) +declare const q: string | null; +const r15 = q !== null ? q : q || 'default'; + +// Fallback is logical AND (needs parens, ?? cannot mix with &&) +declare const s: string | null; +const r16 = s !== null ? s : s && 'value'; + +// Call expression in subject (diagnostic only, no fix: side-effect safety) +declare function foo(): string | null; +const r18 = foo() !== null ? foo() : 'default'; + +// New expression in subject (diagnostic only, no fix: side-effect safety) +declare class Bar { value: string | null } +const r25 = new Bar().value !== null ? new Bar().value : 'default'; + +// Undefined literal type +const undef: undefined = undefined; +const r19 = undef !== undefined ? undef : 'fallback'; + +// Number | undefined (non-string union) +declare const maybeNum: number | undefined; +const r20 = maybeNum !== undefined ? maybeNum : 42; + +// Loose != null with triple union (covers both null and undefined) +declare const tripleUnion: string | null | undefined; +const r21 = tripleUnion != null ? tripleUnion : 'default'; + +// Optional property (a?: string creates string | undefined) +interface Config { timeout?: number; } +declare const config: Config; +const r22 = config.timeout !== undefined ? config.timeout : 3000; + +// Array element access +declare const arr: (number | null)[]; +const r23 = arr[0] !== null ? arr[0] : 0; + +// Function return context +function getVal(x: string | null): string { + return x !== null ? x : 'default'; +} + +// Nested in parentheses +declare const paren: string | null; +const r24 = (paren !== null ? paren : 'default').toUpperCase(); + +// --- StrictSingle fixability: opposite nullish variant in type --- + +// Checks !== null but type has undefined (not null): no fix +declare const ss1: string | undefined; +const r26 = ss1 !== null ? ss1 : 'default'; + +// Checks !== undefined but type has null (not undefined): no fix +declare const ss2: string | null; +const r27 = ss2 !== undefined ? ss2 : 'default'; + +// Inverted: checks === null but type has undefined: no fix +declare const ss3: string | undefined; +const r28 = ss3 === null ? 'default' : ss3; + +// Inverted: checks === undefined but type has null: no fix +declare const ss4: string | null; +const r29 = ss4 === undefined ? 'default' : ss4; + +// Checks !== null, type has only null: fix IS safe +declare const ss5: string | null; +const r30 = ss5 !== null ? ss5 : 'default'; + +// Checks !== undefined, type has only undefined: fix IS safe +declare const ss6: string | undefined; +const r31 = ss6 !== undefined ? ss6 : 'default'; + +// Checks !== null, type has void (acts as undefined): no fix +declare const ss7: string | void; +const r32 = ss7 !== null ? ss7 : 'default'; + +// --- Compound fixability: duplicate vs complementary literals --- + +// Both sides check null (not complementary): no fix +declare const cp1: string | null | undefined; +const r33 = cp1 !== null && cp1 !== null ? cp1 : 'default'; + +// Complementary: one null, one undefined: fix IS safe +declare const cp2: string | null | undefined; +const r34 = cp2 !== null && cp2 !== undefined ? cp2 : 'default'; + +// Complementary reversed order: fix IS safe +declare const cp3: string | null | undefined; +const r35 = cp3 !== undefined && cp3 !== null ? cp3 : 'default'; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts.snap new file mode 100644 index 000000000000..aa1233630bea --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts.snap @@ -0,0 +1,1095 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: ternaryInvalid.ts +--- +# Input +```ts +// Strict !== null +declare const a: string | null; +const r1 = a !== null ? a : 'default'; + +// Strict !== undefined +declare const b: string | undefined; +const r2 = b !== undefined ? b : 'fallback'; + +// Loose != null +declare const c: string | null; +const r3 = c != null ? c : 'default'; + +// Inverted === null +declare const d: string | null; +const r4 = d === null ? 'default' : d; + +// Inverted === undefined +declare const e: string | undefined; +const r5 = e === undefined ? 'fallback' : e; + +// Inverted == null +declare const f: string | null; +const r6 = f == null ? 'default' : f; + +// Compound !== null && !== undefined +declare const g: string | null | undefined; +const r7 = g !== null && g !== undefined ? g : 'default'; + +// Compound === null || === undefined +declare const h: string | null | undefined; +const r8 = h === null || h === undefined ? 'default' : h; + +// Null on left side +declare const i: string | null; +const r9 = null !== i ? i : 'default'; + +// Undefined on left side +declare const j: string | undefined; +const r10 = undefined !== j ? j : 'fallback'; + +// Member access +declare const obj: { prop: string | null }; +const r11 = obj.prop !== null ? obj.prop : 'default'; + +// Loose != undefined +declare const l: string | undefined; +const r12 = l != undefined ? l : 'default'; + +// Inverted, null on left +declare const n: string | null; +const r13 = null === n ? 'default' : n; + +// Strict single with null | undefined (diagnostic only, no fix) +declare const nu: string | null | undefined; +const r17 = nu !== null ? nu : 'default'; + +// Fallback is conditional (needs parens) +declare const p: string | null; +const r14 = p !== null ? p : p ? 'a' : 'b'; + +// Fallback is logical OR (needs parens, ?? cannot mix with ||) +declare const q: string | null; +const r15 = q !== null ? q : q || 'default'; + +// Fallback is logical AND (needs parens, ?? cannot mix with &&) +declare const s: string | null; +const r16 = s !== null ? s : s && 'value'; + +// Call expression in subject (diagnostic only, no fix: side-effect safety) +declare function foo(): string | null; +const r18 = foo() !== null ? foo() : 'default'; + +// New expression in subject (diagnostic only, no fix: side-effect safety) +declare class Bar { value: string | null } +const r25 = new Bar().value !== null ? new Bar().value : 'default'; + +// Undefined literal type +const undef: undefined = undefined; +const r19 = undef !== undefined ? undef : 'fallback'; + +// Number | undefined (non-string union) +declare const maybeNum: number | undefined; +const r20 = maybeNum !== undefined ? maybeNum : 42; + +// Loose != null with triple union (covers both null and undefined) +declare const tripleUnion: string | null | undefined; +const r21 = tripleUnion != null ? tripleUnion : 'default'; + +// Optional property (a?: string creates string | undefined) +interface Config { timeout?: number; } +declare const config: Config; +const r22 = config.timeout !== undefined ? config.timeout : 3000; + +// Array element access +declare const arr: (number | null)[]; +const r23 = arr[0] !== null ? arr[0] : 0; + +// Function return context +function getVal(x: string | null): string { + return x !== null ? x : 'default'; +} + +// Nested in parentheses +declare const paren: string | null; +const r24 = (paren !== null ? paren : 'default').toUpperCase(); + +// --- StrictSingle fixability: opposite nullish variant in type --- + +// Checks !== null but type has undefined (not null): no fix +declare const ss1: string | undefined; +const r26 = ss1 !== null ? ss1 : 'default'; + +// Checks !== undefined but type has null (not undefined): no fix +declare const ss2: string | null; +const r27 = ss2 !== undefined ? ss2 : 'default'; + +// Inverted: checks === null but type has undefined: no fix +declare const ss3: string | undefined; +const r28 = ss3 === null ? 'default' : ss3; + +// Inverted: checks === undefined but type has null: no fix +declare const ss4: string | null; +const r29 = ss4 === undefined ? 'default' : ss4; + +// Checks !== null, type has only null: fix IS safe +declare const ss5: string | null; +const r30 = ss5 !== null ? ss5 : 'default'; + +// Checks !== undefined, type has only undefined: fix IS safe +declare const ss6: string | undefined; +const r31 = ss6 !== undefined ? ss6 : 'default'; + +// Checks !== null, type has void (acts as undefined): no fix +declare const ss7: string | void; +const r32 = ss7 !== null ? ss7 : 'default'; + +// --- Compound fixability: duplicate vs complementary literals --- + +// Both sides check null (not complementary): no fix +declare const cp1: string | null | undefined; +const r33 = cp1 !== null && cp1 !== null ? cp1 : 'default'; + +// Complementary: one null, one undefined: fix IS safe +declare const cp2: string | null | undefined; +const r34 = cp2 !== null && cp2 !== undefined ? cp2 : 'default'; + +// Complementary reversed order: fix IS safe +declare const cp3: string | null | undefined; +const r35 = cp3 !== undefined && cp3 !== null ? cp3 : 'default'; + +``` + +# Diagnostics +``` +ternaryInvalid.ts:3:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 1 │ // Strict !== null + 2 │ declare const a: string | null; + > 3 │ const r1 = a !== null ? a : 'default'; + │ ^^^^^^^^^^ + 4 │ + 5 │ // Strict !== undefined + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 1 1 │ // Strict !== null + 2 2 │ declare const a: string | null; + 3 │ - const·r1·=·a·!==·null·?·a·:·'default'; + 3 │ + const·r1·=·a·??·'default'; + 4 4 │ + 5 5 │ // Strict !== undefined + + +``` + +``` +ternaryInvalid.ts:7:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 5 │ // Strict !== undefined + 6 │ declare const b: string | undefined; + > 7 │ const r2 = b !== undefined ? b : 'fallback'; + │ ^^^^^^^^^^^^^^^ + 8 │ + 9 │ // Loose != null + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 5 5 │ // Strict !== undefined + 6 6 │ declare const b: string | undefined; + 7 │ - const·r2·=·b·!==·undefined·?·b·:·'fallback'; + 7 │ + const·r2·=·b·??·'fallback'; + 8 8 │ + 9 9 │ // Loose != null + + +``` + +``` +ternaryInvalid.ts:11:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 9 │ // Loose != null + 10 │ declare const c: string | null; + > 11 │ const r3 = c != null ? c : 'default'; + │ ^^^^^^^^^ + 12 │ + 13 │ // Inverted === null + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 9 9 │ // Loose != null + 10 10 │ declare const c: string | null; + 11 │ - const·r3·=·c·!=·null·?·c·:·'default'; + 11 │ + const·r3·=·c·??·'default'; + 12 12 │ + 13 13 │ // Inverted === null + + +``` + +``` +ternaryInvalid.ts:15:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 13 │ // Inverted === null + 14 │ declare const d: string | null; + > 15 │ const r4 = d === null ? 'default' : d; + │ ^^^^^^^^^^ + 16 │ + 17 │ // Inverted === undefined + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 13 13 │ // Inverted === null + 14 14 │ declare const d: string | null; + 15 │ - const·r4·=·d·===·null·?·'default'·:·d; + 15 │ + const·r4·=·d·??·'default'; + 16 16 │ + 17 17 │ // Inverted === undefined + + +``` + +``` +ternaryInvalid.ts:19:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 17 │ // Inverted === undefined + 18 │ declare const e: string | undefined; + > 19 │ const r5 = e === undefined ? 'fallback' : e; + │ ^^^^^^^^^^^^^^^ + 20 │ + 21 │ // Inverted == null + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 17 17 │ // Inverted === undefined + 18 18 │ declare const e: string | undefined; + 19 │ - const·r5·=·e·===·undefined·?·'fallback'·:·e; + 19 │ + const·r5·=·e·??·'fallback'; + 20 20 │ + 21 21 │ // Inverted == null + + +``` + +``` +ternaryInvalid.ts:23:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 21 │ // Inverted == null + 22 │ declare const f: string | null; + > 23 │ const r6 = f == null ? 'default' : f; + │ ^^^^^^^^^ + 24 │ + 25 │ // Compound !== null && !== undefined + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 21 21 │ // Inverted == null + 22 22 │ declare const f: string | null; + 23 │ - const·r6·=·f·==·null·?·'default'·:·f; + 23 │ + const·r6·=·f·??·'default'; + 24 24 │ + 25 25 │ // Compound !== null && !== undefined + + +``` + +``` +ternaryInvalid.ts:27:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 25 │ // Compound !== null && !== undefined + 26 │ declare const g: string | null | undefined; + > 27 │ const r7 = g !== null && g !== undefined ? g : 'default'; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 28 │ + 29 │ // Compound === null || === undefined + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 25 25 │ // Compound !== null && !== undefined + 26 26 │ declare const g: string | null | undefined; + 27 │ - const·r7·=·g·!==·null·&&·g·!==·undefined·?·g·:·'default'; + 27 │ + const·r7·=·g·??·'default'; + 28 28 │ + 29 29 │ // Compound === null || === undefined + + +``` + +``` +ternaryInvalid.ts:31:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 29 │ // Compound === null || === undefined + 30 │ declare const h: string | null | undefined; + > 31 │ const r8 = h === null || h === undefined ? 'default' : h; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 32 │ + 33 │ // Null on left side + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 29 29 │ // Compound === null || === undefined + 30 30 │ declare const h: string | null | undefined; + 31 │ - const·r8·=·h·===·null·||·h·===·undefined·?·'default'·:·h; + 31 │ + const·r8·=·h·??·'default'; + 32 32 │ + 33 33 │ // Null on left side + + +``` + +``` +ternaryInvalid.ts:35:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 33 │ // Null on left side + 34 │ declare const i: string | null; + > 35 │ const r9 = null !== i ? i : 'default'; + │ ^^^^^^^^^^ + 36 │ + 37 │ // Undefined on left side + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 33 33 │ // Null on left side + 34 34 │ declare const i: string | null; + 35 │ - const·r9·=·null·!==·i·?·i·:·'default'; + 35 │ + const·r9·=·i·??·'default'; + 36 36 │ + 37 37 │ // Undefined on left side + + +``` + +``` +ternaryInvalid.ts:39:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 37 │ // Undefined on left side + 38 │ declare const j: string | undefined; + > 39 │ const r10 = undefined !== j ? j : 'fallback'; + │ ^^^^^^^^^^^^^^^ + 40 │ + 41 │ // Member access + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 37 37 │ // Undefined on left side + 38 38 │ declare const j: string | undefined; + 39 │ - const·r10·=·undefined·!==·j·?·j·:·'fallback'; + 39 │ + const·r10·=·j·??·'fallback'; + 40 40 │ + 41 41 │ // Member access + + +``` + +``` +ternaryInvalid.ts:43:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 41 │ // Member access + 42 │ declare const obj: { prop: string | null }; + > 43 │ const r11 = obj.prop !== null ? obj.prop : 'default'; + │ ^^^^^^^^^^^^^^^^^ + 44 │ + 45 │ // Loose != undefined + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 41 41 │ // Member access + 42 42 │ declare const obj: { prop: string | null }; + 43 │ - const·r11·=·obj.prop·!==·null·?·obj.prop·:·'default'; + 43 │ + const·r11·=·obj.prop·??·'default'; + 44 44 │ + 45 45 │ // Loose != undefined + + +``` + +``` +ternaryInvalid.ts:47:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 45 │ // Loose != undefined + 46 │ declare const l: string | undefined; + > 47 │ const r12 = l != undefined ? l : 'default'; + │ ^^^^^^^^^^^^^^ + 48 │ + 49 │ // Inverted, null on left + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 45 45 │ // Loose != undefined + 46 46 │ declare const l: string | undefined; + 47 │ - const·r12·=·l·!=·undefined·?·l·:·'default'; + 47 │ + const·r12·=·l·??·'default'; + 48 48 │ + 49 49 │ // Inverted, null on left + + +``` + +``` +ternaryInvalid.ts:51:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 49 │ // Inverted, null on left + 50 │ declare const n: string | null; + > 51 │ const r13 = null === n ? 'default' : n; + │ ^^^^^^^^^^ + 52 │ + 53 │ // Strict single with null | undefined (diagnostic only, no fix) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 49 49 │ // Inverted, null on left + 50 50 │ declare const n: string | null; + 51 │ - const·r13·=·null·===·n·?·'default'·:·n; + 51 │ + const·r13·=·n·??·'default'; + 52 52 │ + 53 53 │ // Strict single with null | undefined (diagnostic only, no fix) + + +``` + +``` +ternaryInvalid.ts:55:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 53 │ // Strict single with null | undefined (diagnostic only, no fix) + 54 │ declare const nu: string | null | undefined; + > 55 │ const r17 = nu !== null ? nu : 'default'; + │ ^^^^^^^^^^^ + 56 │ + 57 │ // Fallback is conditional (needs parens) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:59:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 57 │ // Fallback is conditional (needs parens) + 58 │ declare const p: string | null; + > 59 │ const r14 = p !== null ? p : p ? 'a' : 'b'; + │ ^^^^^^^^^^ + 60 │ + 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 57 57 │ // Fallback is conditional (needs parens) + 58 58 │ declare const p: string | null; + 59 │ - const·r14·=·p·!==·null·?·p·:·p·?·'a'·:·'b'; + 59 │ + const·r14·=·p·??·(p·?·'a'·:·'b'); + 60 60 │ + 61 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||) + + +``` + +``` +ternaryInvalid.ts:63:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||) + 62 │ declare const q: string | null; + > 63 │ const r15 = q !== null ? q : q || 'default'; + │ ^^^^^^^^^^ + 64 │ + 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 61 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||) + 62 62 │ declare const q: string | null; + 63 │ - const·r15·=·q·!==·null·?·q·:·q·||·'default'; + 63 │ + const·r15·=·q·??·(q·||·'default'); + 64 64 │ + 65 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&) + + +``` + +``` +ternaryInvalid.ts:63:32 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Use ?? instead of ||. + + 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||) + 62 │ declare const q: string | null; + > 63 │ const r15 = q !== null ? q : q || 'default'; + │ ^^ + 64 │ + 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&) + + i The || operator checks for all falsy values (including 0, '', and false), while ?? only checks for null and undefined. + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:67:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&) + 66 │ declare const s: string | null; + > 67 │ const r16 = s !== null ? s : s && 'value'; + │ ^^^^^^^^^^ + 68 │ + 69 │ // Call expression in subject (diagnostic only, no fix: side-effect safety) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 65 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&) + 66 66 │ declare const s: string | null; + 67 │ - const·r16·=·s·!==·null·?·s·:·s·&&·'value'; + 67 │ + const·r16·=·s·??·(s·&&·'value'); + 68 68 │ + 69 69 │ // Call expression in subject (diagnostic only, no fix: side-effect safety) + + +``` + +``` +ternaryInvalid.ts:71:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 69 │ // Call expression in subject (diagnostic only, no fix: side-effect safety) + 70 │ declare function foo(): string | null; + > 71 │ const r18 = foo() !== null ? foo() : 'default'; + │ ^^^^^^^^^^^^^^ + 72 │ + 73 │ // New expression in subject (diagnostic only, no fix: side-effect safety) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:75:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 73 │ // New expression in subject (diagnostic only, no fix: side-effect safety) + 74 │ declare class Bar { value: string | null } + > 75 │ const r25 = new Bar().value !== null ? new Bar().value : 'default'; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + 76 │ + 77 │ // Undefined literal type + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:79:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 77 │ // Undefined literal type + 78 │ const undef: undefined = undefined; + > 79 │ const r19 = undef !== undefined ? undef : 'fallback'; + │ ^^^^^^^^^^^^^^^^^^^ + 80 │ + 81 │ // Number | undefined (non-string union) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 77 77 │ // Undefined literal type + 78 78 │ const undef: undefined = undefined; + 79 │ - const·r19·=·undef·!==·undefined·?·undef·:·'fallback'; + 79 │ + const·r19·=·undef·??·'fallback'; + 80 80 │ + 81 81 │ // Number | undefined (non-string union) + + +``` + +``` +ternaryInvalid.ts:83:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 81 │ // Number | undefined (non-string union) + 82 │ declare const maybeNum: number | undefined; + > 83 │ const r20 = maybeNum !== undefined ? maybeNum : 42; + │ ^^^^^^^^^^^^^^^^^^^^^^ + 84 │ + 85 │ // Loose != null with triple union (covers both null and undefined) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 81 81 │ // Number | undefined (non-string union) + 82 82 │ declare const maybeNum: number | undefined; + 83 │ - const·r20·=·maybeNum·!==·undefined·?·maybeNum·:·42; + 83 │ + const·r20·=·maybeNum·??·42; + 84 84 │ + 85 85 │ // Loose != null with triple union (covers both null and undefined) + + +``` + +``` +ternaryInvalid.ts:87:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 85 │ // Loose != null with triple union (covers both null and undefined) + 86 │ declare const tripleUnion: string | null | undefined; + > 87 │ const r21 = tripleUnion != null ? tripleUnion : 'default'; + │ ^^^^^^^^^^^^^^^^^^^ + 88 │ + 89 │ // Optional property (a?: string creates string | undefined) + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 85 85 │ // Loose != null with triple union (covers both null and undefined) + 86 86 │ declare const tripleUnion: string | null | undefined; + 87 │ - const·r21·=·tripleUnion·!=·null·?·tripleUnion·:·'default'; + 87 │ + const·r21·=·tripleUnion·??·'default'; + 88 88 │ + 89 89 │ // Optional property (a?: string creates string | undefined) + + +``` + +``` +ternaryInvalid.ts:92:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 90 │ interface Config { timeout?: number; } + 91 │ declare const config: Config; + > 92 │ const r22 = config.timeout !== undefined ? config.timeout : 3000; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 93 │ + 94 │ // Array element access + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 90 90 │ interface Config { timeout?: number; } + 91 91 │ declare const config: Config; + 92 │ - const·r22·=·config.timeout·!==·undefined·?·config.timeout·:·3000; + 92 │ + const·r22·=·config.timeout·??·3000; + 93 93 │ + 94 94 │ // Array element access + + +``` + +``` +ternaryInvalid.ts:96:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 94 │ // Array element access + 95 │ declare const arr: (number | null)[]; + > 96 │ const r23 = arr[0] !== null ? arr[0] : 0; + │ ^^^^^^^^^^^^^^^ + 97 │ + 98 │ // Function return context + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:100:10 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 98 │ // Function return context + 99 │ function getVal(x: string | null): string { + > 100 │ return x !== null ? x : 'default'; + │ ^^^^^^^^^^ + 101 │ } + 102 │ + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 98 98 │ // Function return context + 99 99 │ function getVal(x: string | null): string { + 100 │ - ··return·x·!==·null·?·x·:·'default'; + 100 │ + ··return·x·??·'default'; + 101 101 │ } + 102 102 │ + + +``` + +``` +ternaryInvalid.ts:105:14 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 103 │ // Nested in parentheses + 104 │ declare const paren: string | null; + > 105 │ const r24 = (paren !== null ? paren : 'default').toUpperCase(); + │ ^^^^^^^^^^^^^^ + 106 │ + 107 │ // --- StrictSingle fixability: opposite nullish variant in type --- + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 103 103 │ // Nested in parentheses + 104 104 │ declare const paren: string | null; + 105 │ - const·r24·=·(paren·!==·null·?·paren·:·'default').toUpperCase(); + 105 │ + const·r24·=·(paren·??·'default').toUpperCase(); + 106 106 │ + 107 107 │ // --- StrictSingle fixability: opposite nullish variant in type --- + + +``` + +``` +ternaryInvalid.ts:111:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 109 │ // Checks !== null but type has undefined (not null): no fix + 110 │ declare const ss1: string | undefined; + > 111 │ const r26 = ss1 !== null ? ss1 : 'default'; + │ ^^^^^^^^^^^^ + 112 │ + 113 │ // Checks !== undefined but type has null (not undefined): no fix + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:115:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 113 │ // Checks !== undefined but type has null (not undefined): no fix + 114 │ declare const ss2: string | null; + > 115 │ const r27 = ss2 !== undefined ? ss2 : 'default'; + │ ^^^^^^^^^^^^^^^^^ + 116 │ + 117 │ // Inverted: checks === null but type has undefined: no fix + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:119:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 117 │ // Inverted: checks === null but type has undefined: no fix + 118 │ declare const ss3: string | undefined; + > 119 │ const r28 = ss3 === null ? 'default' : ss3; + │ ^^^^^^^^^^^^ + 120 │ + 121 │ // Inverted: checks === undefined but type has null: no fix + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:123:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 121 │ // Inverted: checks === undefined but type has null: no fix + 122 │ declare const ss4: string | null; + > 123 │ const r29 = ss4 === undefined ? 'default' : ss4; + │ ^^^^^^^^^^^^^^^^^ + 124 │ + 125 │ // Checks !== null, type has only null: fix IS safe + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:127:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 125 │ // Checks !== null, type has only null: fix IS safe + 126 │ declare const ss5: string | null; + > 127 │ const r30 = ss5 !== null ? ss5 : 'default'; + │ ^^^^^^^^^^^^ + 128 │ + 129 │ // Checks !== undefined, type has only undefined: fix IS safe + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 125 125 │ // Checks !== null, type has only null: fix IS safe + 126 126 │ declare const ss5: string | null; + 127 │ - const·r30·=·ss5·!==·null·?·ss5·:·'default'; + 127 │ + const·r30·=·ss5·??·'default'; + 128 128 │ + 129 129 │ // Checks !== undefined, type has only undefined: fix IS safe + + +``` + +``` +ternaryInvalid.ts:131:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 129 │ // Checks !== undefined, type has only undefined: fix IS safe + 130 │ declare const ss6: string | undefined; + > 131 │ const r31 = ss6 !== undefined ? ss6 : 'default'; + │ ^^^^^^^^^^^^^^^^^ + 132 │ + 133 │ // Checks !== null, type has void (acts as undefined): no fix + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 129 129 │ // Checks !== undefined, type has only undefined: fix IS safe + 130 130 │ declare const ss6: string | undefined; + 131 │ - const·r31·=·ss6·!==·undefined·?·ss6·:·'default'; + 131 │ + const·r31·=·ss6·??·'default'; + 132 132 │ + 133 133 │ // Checks !== null, type has void (acts as undefined): no fix + + +``` + +``` +ternaryInvalid.ts:135:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 133 │ // Checks !== null, type has void (acts as undefined): no fix + 134 │ declare const ss7: string | void; + > 135 │ const r32 = ss7 !== null ? ss7 : 'default'; + │ ^^^^^^^^^^^^ + 136 │ + 137 │ // --- Compound fixability: duplicate vs complementary literals --- + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:141:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 139 │ // Both sides check null (not complementary): no fix + 140 │ declare const cp1: string | null | undefined; + > 141 │ const r33 = cp1 !== null && cp1 !== null ? cp1 : 'default'; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 142 │ + 143 │ // Complementary: one null, one undefined: fix IS safe + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +ternaryInvalid.ts:145:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 143 │ // Complementary: one null, one undefined: fix IS safe + 144 │ declare const cp2: string | null | undefined; + > 145 │ const r34 = cp2 !== null && cp2 !== undefined ? cp2 : 'default'; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 146 │ + 147 │ // Complementary reversed order: fix IS safe + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 143 143 │ // Complementary: one null, one undefined: fix IS safe + 144 144 │ declare const cp2: string | null | undefined; + 145 │ - const·r34·=·cp2·!==·null·&&·cp2·!==·undefined·?·cp2·:·'default'; + 145 │ + const·r34·=·cp2·??·'default'; + 146 146 │ + 147 147 │ // Complementary reversed order: fix IS safe + + +``` + +``` +ternaryInvalid.ts:149:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Prefer ?? over a ternary expression checking for nullish. + + 147 │ // Complementary reversed order: fix IS safe + 148 │ declare const cp3: string | null | undefined; + > 149 │ const r35 = cp3 !== undefined && cp3 !== null ? cp3 : 'default'; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 150 │ + + i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Safe fix: Use ?? instead. + + 147 147 │ // Complementary reversed order: fix IS safe + 148 148 │ declare const cp3: string | null | undefined; + 149 │ - const·r35·=·cp3·!==·undefined·&&·cp3·!==·null·?·cp3·:·'default'; + 149 │ + const·r35·=·cp3·??·'default'; + 150 150 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts new file mode 100644 index 000000000000..7501ad093ad9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts @@ -0,0 +1,36 @@ +// should not generate diagnostics + +// Already using ?? +declare const x: string | null; +const a = x ?? 'default'; + +// Non-nullish comparison +declare const b: number; +const c = b > 0 ? b : 1; + +// Mismatched consequent +declare const d: string | null; +const e = d !== null ? 'something_else' : 'fallback'; + +// Non-nullish literal comparison +declare const f: string; +const g = f === 'foo' ? f : 'bar'; + +// Typeof check +declare const t: string | null; +const h = typeof t === 'string' ? t : 'default'; + +// Different variables in test and branches +declare const p: string | null; +declare const q: string; +const i = p !== null ? q : 'default'; + +// Mismatched (loose) +declare const m: string | null; +declare const n: string; +const j = m != null ? n : 'default'; + +// Compound with different variables +declare const s1: string | null; +declare const s2: string | null; +const k = s1 !== null && s2 !== undefined ? s1 : 'default'; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts.snap new file mode 100644 index 000000000000..2968312b4ba5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts.snap @@ -0,0 +1,44 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: ternaryValid.ts +--- +# Input +```ts +// should not generate diagnostics + +// Already using ?? +declare const x: string | null; +const a = x ?? 'default'; + +// Non-nullish comparison +declare const b: number; +const c = b > 0 ? b : 1; + +// Mismatched consequent +declare const d: string | null; +const e = d !== null ? 'something_else' : 'fallback'; + +// Non-nullish literal comparison +declare const f: string; +const g = f === 'foo' ? f : 'bar'; + +// Typeof check +declare const t: string | null; +const h = typeof t === 'string' ? t : 'default'; + +// Different variables in test and branches +declare const p: string | null; +declare const q: string; +const i = p !== null ? q : 'default'; + +// Mismatched (loose) +declare const m: string | null; +declare const n: string; +const j = m != null ? n : 'default'; + +// Compound with different variables +declare const s1: string | null; +declare const s2: string | null; +const k = s1 !== null && s2 !== undefined ? s1 : 'default'; + +``` diff --git a/crates/biome_rule_options/src/use_nullish_coalescing.rs b/crates/biome_rule_options/src/use_nullish_coalescing.rs index 841a52040ecb..f90d0764988c 100644 --- a/crates/biome_rule_options/src/use_nullish_coalescing.rs +++ b/crates/biome_rule_options/src/use_nullish_coalescing.rs @@ -13,10 +13,20 @@ pub struct UseNullishCoalescingOptions { /// /// Default: `true` pub ignore_conditional_tests: Option, + + /// Whether to ignore ternary expressions that could be simplified + /// using the nullish coalescing operator. + /// + /// Default: `false` + pub ignore_ternary_tests: Option, } impl UseNullishCoalescingOptions { pub fn ignore_conditional_tests(&self) -> bool { self.ignore_conditional_tests.unwrap_or(true) } + + pub fn ignore_ternary_tests(&self) -> bool { + self.ignore_ternary_tests.unwrap_or(false) + } } diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 3288cff34b0d..2c436968681f 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -7456,6 +7456,13 @@ that appear in places where the falsy-checking behavior may be intentional. Default: `true` */ ignoreConditionalTests?: boolean; + /** + * Whether to ignore ternary expressions that could be simplified +using the nullish coalescing operator. + +Default: `false` + */ + ignoreTernaryTests?: boolean; } export type UsePlaywrightValidDescribeCallbackOptions = {}; export type UseRegexpExecOptions = {}; diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 9674bfb4b581..1652d96df1a9 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -13935,6 +13935,11 @@ "description": "Whether to ignore `||` expressions in conditional test positions\n(if/while/for/do-while/ternary conditions).\n\nWhen `true` (the default), the rule will not report `||` expressions\nthat appear in places where the falsy-checking behavior may be intentional.\n\nDefault: `true`", "type": ["boolean", "null"], "default": null + }, + "ignoreTernaryTests": { + "description": "Whether to ignore ternary expressions that could be simplified\nusing the nullish coalescing operator.\n\nDefault: `false`", + "type": ["boolean", "null"], + "default": null } }, "additionalProperties": false