Skip to content

feat(lint): add useNullishCoalescing nursery rule#8952

Merged
dyc3 merged 2 commits intobiomejs:mainfrom
pkallos:feat/8043-use-nullish-coalescing
Feb 24, 2026
Merged

feat(lint): add useNullishCoalescing nursery rule#8952
dyc3 merged 2 commits intobiomejs:mainfrom
pkallos:feat/8043-use-nullish-coalescing

Conversation

@pkallos
Copy link
Contributor

@pkallos pkallos commented Feb 2, 2026

AI Disclosure: This PR was developed with AI assistance (Claude).

Summary

Adds the useNullishCoalescing nursery rule, which suggests using the nullish coalescing operator (??) instead of logical OR (||) when the left operand may be nullish.

This is the first in a series of (proposed) incremental PRs to bring prefer-nullish-coalescing functionality to Biome, as discussed this comment in #8089. The approach is to start with the core || -> ?? detection, get it validated in nursery, then follow up with enhancements for ||= -> ??=, ternary expressions, and if-statement patterns.

The ?? operator only checks for null and undefined, while || checks for any falsy value including 0, '', and false. This can prevent bugs where legitimate falsy values are incorrectly treated as missing.

// Invalid - should use ??
declare const x: string | null;
const value = x || 'default';

// Valid
const value = x ?? 'default';

Implementation details:

  • Uses Typed<JsLogicalExpression> query to only trigger when the left operand contains null or undefined in its type
  • Only offers an automatic fix when type analysis confirms the replacement is safe (left operand can only be truthy or nullish, not other falsy values)
  • By default ignores || in conditional test positions (if/while/for/do-while/ternary) where falsy-checking may be intentional. Configurable via ignoreConditionalTests option.

Inspired by @typescript-eslint/prefer-nullish-coalescing. Addresses #8043. Builds on the work in #8089.

Test Plan

  • Spec tests covering valid cases (non-nullish types, conditional test positions, already using ??)
  • Spec tests covering invalid cases (nullish types triggering diagnostic with safe fix)
  • Spec tests for ignoreConditionalTests: false option
  • All tests pass: cargo test -p biome_js_analyze --test spec_tests specs::nursery::use_nullish_coalescing

Docs

Documentation is included in the rule's doc comments with examples for invalid and valid cases. The rule options (ignoreConditionalTests) are documented inline.

@changeset-bot
Copy link

changeset-bot bot commented Feb 2, 2026

🦋 Changeset detected

Latest commit: f39f564

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Feb 2, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

Walkthrough

Adds a new nursery lint rule useNullishCoalescing for JavaScript/TypeScript that flags uses of || where ?? is more appropriate, emits diagnostics, and offers a safe autofix when type- and syntax-safe. Introduces the rule implementation, a public state type, and rule options (UseNullishCoalescingOptions with ignore_conditional_tests defaulting to true via an accessor). Adds a public module export, test fixtures (valid/invalid/commentTrivia) and an options JSON to disable ignoring conditional tests, plus a changeset.

Suggested labels

A-Type-Inference

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(lint): add useNullishCoalescing nursery rule' clearly and concisely describes the main change—adding a new linting rule to the nursery.
Description check ✅ Passed The description thoroughly explains the purpose, implementation details, and testing approach for the new useNullishCoalescing rule, directly matching the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

Mostly just some housekeeping things, I don't see any obvious deal breakers with the actual impl.

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

I haven't checked the whole rule, but since this rule plans to use types, I ask you to direct this rule towards the next branch. There, there's a new domain called Types that you must use

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 3, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 156 skipped benchmarks1


Comparing pkallos:feat/8043-use-nullish-coalescing (f39f564) with main (b834078)

Open in CodSpeed

Footnotes

  1. 156 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@pkallos
Copy link
Contributor Author

pkallos commented Feb 3, 2026

I haven't checked the whole rule, but since this rule plans to use types, I ask you to direct this rule towards the next branch. There, there's a new domain called Types that you must use

Will do!

@ematipico and @dyc3 thank you very much for the prompt and thorough feedback, appreciate your time on this!

@pkallos pkallos changed the base branch from main to next February 3, 2026 20:47
@pkallos pkallos force-pushed the feat/8043-use-nullish-coalescing branch from bf492f8 to 098f864 Compare February 3, 2026 20:47
@pkallos
Copy link
Contributor Author

pkallos commented Feb 3, 2026

Alright, I have made the following changes to address feedback:

  • include issue_number metadata in the rule definition
  • moved rule to the Types domain
  • added comment trivia test cases in their own file
  • refactored some reusable conditional checks in is_in_test_position() (.text_trimmed_range().contains_range(logical_range))

Hopefully not an issue but I also then amended/squashed the commits, and then rebased the branch onto next and moved the PR so it's targeted into the next branch.

There's one outstanding issue which is about the implementation of the ignore_conditional_tests option in this rule, I responded to the comment and tried to complete the option definition and docs, but I am happy to remove the option instead and schedule it for a future PR if that's the preference, please let me know.

Thank you team!

@pkallos pkallos requested review from dyc3 and ematipico February 3, 2026 21:20
@pkallos pkallos force-pushed the feat/8043-use-nullish-coalescing branch from b2d3bf1 to 7716eb5 Compare February 19, 2026 20:31
@pkallos
Copy link
Contributor Author

pkallos commented Feb 19, 2026

Hi @ematipico and @dyc3, please let me know if there's anything else I should do here!

Copy link
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

Looks good. We've since merged next back into main, so we can put this back to merge into the main branch.

@pkallos pkallos changed the base branch from next to main February 20, 2026 21:33
Adds the useNullishCoalescing nursery rule, which suggests using ??
instead of || when the left operand may be nullish.

- Uses Typed<JsLogicalExpression> for type-aware analysis
- Only offers safe fix when type analysis confirms replacement is safe
- Ignores conditional test positions by default (configurable)
- Preserves comment trivia in fixes

Addresses biomejs#8043
@pkallos pkallos force-pushed the feat/8043-use-nullish-coalescing branch from 7716eb5 to addcb34 Compare February 20, 2026 22:24
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs (1)

185-214: Minor: two separate .parent() calls could be unified.

The parent node is fetched twice (lines 187–189 and 192–195). Not a real performance concern since it's O(1), but extracting the parent once into a local would make the intent slightly clearer.

♻️ Optional tidy-up
 fn is_safe_syntax_context_for_replacement(logical: &JsLogicalExpression) -> bool {
-    let is_parenthesized = logical
-        .syntax()
-        .parent()
-        .is_some_and(|parent| JsParenthesizedExpression::can_cast(parent.kind()));
+    let parent = logical.syntax().parent();
+    let is_parenthesized = parent
+        .as_ref()
+        .is_some_and(|p| JsParenthesizedExpression::can_cast(p.kind()));
 
-    if !is_parenthesized
-        && logical
-            .syntax()
-            .parent()
-            .is_some_and(|parent| JsLogicalExpression::can_cast(parent.kind()))
-    {
+    if !is_parenthesized
+        && parent
+            .as_ref()
+            .is_some_and(|p| JsLogicalExpression::can_cast(p.kind()))
+    {
         return false;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs` around
lines 185 - 214, In is_safe_syntax_context_for_replacement, avoid calling
logical.syntax().parent() twice: capture the parent once into a local (e.g., let
parent = logical.syntax().parent()) and reuse it for both the
JsParenthesizedExpression::can_cast and JsLogicalExpression::can_cast checks;
keep the rest of the logic (the left/right checks using
is_unparenthesized_and_or_expression on logical.left()/right()) unchanged.
crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/valid.ts (1)

1-53: Good coverage of the "no diagnostic" scenarios.

Covers ?? already in use, non-nullish types, conditional test positions, and non-nullish unions. One gap worth considering: a test case for any and unknown typed left operands would help document the rule's behaviour for those common types.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/valid.ts`
around lines 1 - 53, Add test cases exercising left operands typed as any and
unknown to document rule behavior: declare variables like maybeAny: any and
maybeUnknown: unknown and add expressions using both || and ?? (e.g., const aa =
maybeAny || 'fallback'; const ab = maybeAny ?? 'default'; const ba =
maybeUnknown || 'fallback'; const bb = maybeUnknown ?? 'default') so the spec
covers how the analyzer treats any/unknown for both nullish and logical-or
patterns; place them alongside existing cases (e.g., near maybeStr, x, y) and
ensure names (maybeAny, maybeUnknown, aa, ab, ba, bb) match so they’re easy to
locate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/chubby-trees-refuse.md:
- Around line 1-3: Update the changeset in .changeset/chubby-trees-refuse.md so
the top-level change type is "minor" instead of "patch" for the "@biomejs/biome"
entry; locate the front-matter block that lists "@biomejs/biome": patch and
change the value to "@biomejs/biome": minor to match the PR targeting the next
branch and the repository's change-type guidelines.

---

Nitpick comments:
In `@crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs`:
- Around line 185-214: In is_safe_syntax_context_for_replacement, avoid calling
logical.syntax().parent() twice: capture the parent once into a local (e.g., let
parent = logical.syntax().parent()) and reuse it for both the
JsParenthesizedExpression::can_cast and JsLogicalExpression::can_cast checks;
keep the rest of the logic (the left/right checks using
is_unparenthesized_and_or_expression on logical.left()/right()) unchanged.

In `@crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/valid.ts`:
- Around line 1-53: Add test cases exercising left operands typed as any and
unknown to document rule behavior: declare variables like maybeAny: any and
maybeUnknown: unknown and add expressions using both || and ?? (e.g., const aa =
maybeAny || 'fallback'; const ab = maybeAny ?? 'default'; const ba =
maybeUnknown || 'fallback'; const bb = maybeUnknown ?? 'default') so the spec
covers how the analyzer treats any/unknown for both nullish and logical-or
patterns; place them alongside existing cases (e.g., near maybeStr, x, y) and
ensure names (maybeAny, maybeUnknown, aa, ab, ba, bb) match so they’re easy to
locate.

@pkallos
Copy link
Contributor Author

pkallos commented Feb 21, 2026

OK awesome, thanks! I rebased it back onto main and adjusted the PR base to main.

@pkallos
Copy link
Contributor Author

pkallos commented Feb 24, 2026

Hope it's OK, I created a set of follow-on issues according to the plan I described in the original issue/PR, basically chunking out the work to bring this rule progressively into feature parity with the corresponding eslint rule.

Can start tackling those in order, lmk if there's anything else!

@dyc3 dyc3 merged commit 1d2ca15 into biomejs:main Feb 24, 2026
20 checks passed
@github-actions github-actions bot mentioned this pull request Feb 24, 2026
@hammadxcm
Copy link

Hey @pkallos @dyc3 @ematipico — I've implemented issue #9229 (ternary nullish checks) as a follow-on to this PR.

PR: #9240

It extends useNullishCoalescing to detect ternary expressions with explicit nullish checks and suggest ??:

  • Simple strict: a !== null ? a : ba ?? b
  • Simple loose: a != null ? a : ba ?? b
  • Inverted: a === null ? b : aa ?? b
  • Null/undefined on either side: null === a ? b : aa ?? b
  • Compound: a === null || a === undefined ? b : aa ?? b
  • Member expressions, computed members, parenthesized subjects

Also adds ignoreTernaryTests option (default: false) to disable ternary detection independently.

Fix safety is handled carefully:

  • Safe fix: loose equality and compound checks (covers both null + undefined, matching ?? semantics)
  • Unsafe fix: strict single checks (only covers one of null/undefined)
  • No fix: call expression subjects (ternary evaluates twice, ?? evaluates once)

Would appreciate a review when you get a chance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants