+
Skip to content

Conversation

qraqras
Copy link
Contributor

@qraqras qraqras commented Sep 3, 2025

Summary

Closes #7381

Now the useOptionalChain rule recognizes optional chaining using Yoda expressions (e.g., undefined !== foo && foo.bar).

Test Plan

Added snap tests.

Using the shell script below, Yoda expressions versions of the existing tests were generated, and the snapshots of the Yoda versions were compared with the originals to confirm identical results.

#!/bin/bash
shopt -s nullglob

output_file_prefix="yoda_expressions"

# Remove existing output files
for f in ${output_file_prefix}_*; do
    case "$f" in
        *.snap) continue ;;
    esac
    rm -f -- "$f"
done

# Process each target file
for file in *.js *.ts *.jsx *.tsx; do
    if [[ -f "$file" ]]; then
        output_file="${output_file_prefix}_${file}"
        swap_occurred=false
        lines=()
        while IFS= read -r line; do
            trailing_semicolon=""
            # Remove trailing semicolons
            if [[ $line =~ \;$ ]]; then
                trailing_semicolon=";"
                line=${line%;}
            fi
            read -r -a words <<< "$line"
            # Swap nullish operands
            for ((i=2; i<${#words[@]}; i++)); do
                word=${words[i]}
                if [[ $word == "undefined" || $word == "null" ]]; then
                    if [[ ${words[i-1]} == "!=" || ${words[i-1]} == "!==" ]]; then
                        temp=${words[i-2]}
                        words[i-2]=$word
                        words[i]=$temp
                        swap_occurred=true
                    fi
                fi
            done
            # Restore trailing semicolons
            lines+=("${words[*]}${trailing_semicolon}")
        done < "$file"
        # Write to output file if any swaps occurred
        if [[ $swap_occurred == true ]]; then
            for line in "${lines[@]}"; do
                echo "$line" >> "$output_file"
            done
        fi
    fi
done

shopt -u nullglob

Docs

changeset only

Copy link

changeset-bot bot commented Sep 3, 2025

🦋 Changeset detected

Latest commit: f2296b3

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

Copy link
Contributor

coderabbitai bot commented Sep 3, 2025

Walkthrough

Introduces a new JsBinaryExpression API extract_optional_chain_like that returns the non-nullish operand when comparing against null/undefined (covers both normal and Yoda-style forms). The useOptionalChain lint logic was switched to use this extractor in normalization and logical-AND chain analysis. Several new test fixtures were added to exercise Yoda-style null/undefined checks with logical AND, optional chaining, element access, calls, private fields, and other edge cases. A changeset entry documents a patch release referencing issue #7381. No public package APIs aside from the internal syntax extension method signature were changed.

Assessment against linked issues

Objective Addressed Explanation
Make useOptionalChain work when operands are swapped/Yoda-style (e.g., undefined !== foo && foo.bar) [#7381]

Suggested reviewers

  • ematipico
  • dyc3

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a2bcc33 and f2296b3.

📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Parser conformance
  • GitHub Check: Documentation
  • GitHub Check: Check Dependencies
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: End-to-end tests
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Test Node.js API
  • GitHub Check: autofix
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added A-Linter Area: linter A-Parser Area: parser L-JavaScript Language: JavaScript and super languages labels Sep 3, 2025
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: 0

🧹 Nitpick comments (10)
.changeset/big-keys-play.md (1)

5-5: Polish the changeset (tense split, final full stop, canonical docs URL, add example).

Bring it in line with our changeset conventions: past tense for the action, present tense for current behaviour, sentences ending with a full stop, and include a representative example. Also swap the Japanese docs URL for the canonical one.

-Fixed [#7381](https://github.com/biomejs/biome/issues/7381), now the [`useOptionalChain`](https://biomejs.dev/ja/linter/rules/use-optional-chain/) rule recognizes optional chaining using Yoda expressions (e.g., `undefined !== foo && foo.bar`)
+Fixed [#7381](https://github.com/biomejs/biome/issues/7381).
+The [`useOptionalChain`](https://biomejs.dev/linter/rules/use-optional-chain/) rule now recognises optional chaining when written as Yoda expressions.
+
+#### Example
+```js
+// Before
+undefined !== foo && foo.bar
+// After
+foo?.bar
+```
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases5.js (1)

56-66: Add ‘void 0’ Yoda variants to exercise undefined aliasing.

Many codebases still use void 0 for undefined. Adding a couple of lines ensures the extractor treats it equivalently.

 // chained calls
 undefined != foo && undefined != foo.bar && undefined != foo.bar.baz && foo.bar.baz.buzz()
 undefined != foo && undefined != foo.bar && undefined != foo.bar.baz && undefined != foo.bar.baz.buzz && foo.bar.baz.buzz()
 undefined != foo.bar && undefined != foo.bar.baz && undefined != foo.bar.baz.buzz && foo.bar.baz.buzz()
+
+// 'void 0' variants (alias of undefined)
+void 0 != foo && foo.bar
+void 0 != foo.bar && foo.bar.baz
+void 0 != foo && foo()
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases4.js (1)

48-55: Consider a couple of parenthesised Yoda forms and ‘void 0’ strict variants.

Parentheses and void 0 are cheap extra edges that have bitten normalisers before.

 undefined !== foo && undefined !== foo.bar && undefined !== foo.bar.baz && foo.bar.baz.buzz
 undefined !== foo.bar && undefined !== foo.bar.baz && foo.bar.baz.buzz
+
+// Parenthesised expressions
+(null !== (foo)) && (foo).bar
+(undefined !== (foo?.bar)) && (foo?.bar).baz
+
+// 'void 0' strict variants
+void 0 !== foo && foo.bar
+void 0 !== foo.bar && foo.bar.baz
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases6.jsx (1)

21-27: Private fields outside a declaring class may trigger early errors—please confirm parser mode.

Access like foo.#bar is an early error unless the private names are declared in the containing class. If our test harness disables early errors, we're fine; otherwise, consider wrapping these in a minimal class to keep fixtures parseable.

Happy to propose a wrapped variant if needed.

crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts (3)

36-39: Remove duplicated valid case.

foo["some long"] && foo["some long string"].baz; appears twice (Lines 36 and 38). Drop one to keep the fixture lean.

Apply:

-foo["some long"] && foo["some long string"].baz;
 foo[`some long`] && foo[`some long string`].baz;
-foo["some long"] && foo["some long string"].baz;

46-47: Track or enable the commented-out case.

The FIXME for (new foo() || {}).bar suggests an expected no-diagnostic. Either enable it (if now supported) or link a tracking issue in a comment for future work.

Happy to raise a follow-up issue and add a focused test once confirmed.


1-1: Test filename convention.

Our snapshots typically use files prefixed with valid* / invalid*. Consider renaming this to start with valid (e.g., valid.yoda_expressions.ts) for consistency with the harness.

crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases3.js (1)

26-62: Add non‑strict and undefined Yoda coverage.

Great breadth. To fully exercise the extractor, add a couple of != cases and undefined !== … variants, e.g.:

  • undefined != foo && foo.bar
  • undefined !== foo.bar && foo.bar.baz
crates/biome_js_syntax/src/expr_ext.rs (1)

345-369: Recognise void … as undefined (and consider globalThis.undefined).

is_nullish only checks literal null and identifier undefined. Many codebases use void 0 (or void any) as undefined. Optionally recognise it; likewise globalThis.undefined/window.undefined if you wish to be generous.

Apply minimal support for void:

 fn is_nullish(expression: &AnyJsExpression) -> bool {
-    expression
-        .as_static_value()
-        .is_some_and(|x| x.is_null_or_undefined())
+    expression
+        .as_static_value()
+        .is_some_and(|x| x.is_null_or_undefined())
+        || matches!(
+            expression,
+            AnyJsExpression::JsUnaryExpression(u)
+                if u.operator().is_ok_and(|op| matches!(op, JsUnaryOperator::Void))
+        )
 }

If you also want globalThis.undefined:

- fn is_nullish(expression: &AnyJsExpression) -> bool {
+ fn is_nullish(expression: &AnyJsExpression) -> bool {
     // existing checks …
+    || crate::global_identifier(expression)
+        .is_some_and(|(_, name)| matches!(name, StaticValue::String(tok) if tok.text_trimmed() == "undefined"))
 }
crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs (1)

564-567: Minor readability tweak for nested ??.

ok()?? is concise but slightly opaque. Consider expanding for clarity.

-AnyJsExpression::JsBinaryExpression(expression) => {
-    expression.extract_optional_chain_like().ok()??
-}
+AnyJsExpression::JsBinaryExpression(expression) => {
+    match expression.extract_optional_chain_like() {
+        Ok(Some(expr)) => expr,
+        _ => return None,
+    }
+}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 099507e and a2bcc33.

⛔ Files ignored due to path filters (5)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases3.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases4.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases5.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases6.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (8)
  • .changeset/big-keys-play.md (1 hunks)
  • crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs (2 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases3.js (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases4.js (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases5.js (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases6.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts (1 hunks)
  • crates/biome_js_syntax/src/expr_ext.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
crates/biome_*_{syntax,parser,formatter,analyze,factory,semantic}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the per-language crate structure: biome_{lang}_{syntax,parser,formatter,analyze,factory,semantic}

Files:

  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases6.jsx
  • crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases3.js
  • crates/biome_js_syntax/src/expr_ext.rs
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases4.js
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases5.js
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases6.jsx
  • crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases3.js
  • crates/biome_js_syntax/src/expr_ext.rs
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases4.js
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases5.js
**/tests/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place test files under a tests/ directory in each crate

Files:

  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases6.jsx
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases3.js
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases4.js
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases5.js
.changeset/*.md

📄 CodeRabbit inference engine (CONTRIBUTING.md)

.changeset/*.md: Create changesets with just new-changeset; store them in .changeset/ with correct frontmatter (package keys and change type).
In changeset descriptions, follow content conventions: user-facing changes only; past tense for what you did; present tense for current behavior; link issues for fixes; link rules/assists; include representative code blocks; end every sentence with a period.
When adding headers in a changeset, only use #### or ##### levels.

Files:

  • .changeset/big-keys-play.md
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Format Rust and TOML files before committing (use just f/just format).

Files:

  • crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs
  • crates/biome_js_syntax/src/expr_ext.rs
🧠 Learnings (2)
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/**/lib/src/**/nursery/**/*.rs : Avoid `unwrap`/`expect`; use `?`, `ok()?`, and combinators (`map`, `and_then`, `filter`) to handle `Result`/`Option` when navigating the CST

Applied to files:

  • crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs
📚 Learning: 2025-08-17T08:56:30.831Z
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-08-17T08:56:30.831Z
Learning: Applies to crates/biome_analyze/crates/**/tests/specs/**/{invalid*,valid*}.{js,jsx,ts,tsx,css,graphql,jsonc} : Place snapshot test cases under `tests/specs/<group>/<ruleName>/` using files prefixed with `invalid` and `valid`

Applied to files:

  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (23)
  • GitHub Check: autofix
  • GitHub Check: Bench (biome_package)
  • GitHub Check: Bench (biome_configuration)
  • GitHub Check: Bench (biome_module_graph)
  • GitHub Check: Bench (biome_html_parser)
  • GitHub Check: Bench (biome_html_formatter)
  • GitHub Check: Bench (biome_graphql_formatter)
  • GitHub Check: Bench (biome_json_parser)
  • GitHub Check: Bench (biome_css_formatter)
  • GitHub Check: Bench (biome_json_formatter)
  • GitHub Check: Bench (biome_json_analyze)
  • GitHub Check: Bench (biome_graphql_parser)
  • GitHub Check: Bench (biome_css_analyze)
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_css_parser)
  • GitHub Check: Bench (biome_js_formatter)
  • GitHub Check: Test Node.js API
  • GitHub Check: Documentation
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Check Dependencies
  • GitHub Check: Parser conformance
🔇 Additional comments (6)
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases5.js (1)

1-66: Solid coverage of loose/nullish Yoda cases.

Nice spread of chained members, element access, and calls. This will catch regressions quickly.

crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases4.js (1)

1-57: Good breadth on strict inequality forms.

Covers the important shapes (dot/bracket, calls, partial optional chains). Looks great.

crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_logicalAndCases6.jsx (1)

36-63: Nice touch on preserving whitespace, JSX, and comments.

These tend to regress; lovely to see them covered.

crates/biome_js_syntax/src/expr_ext.rs (2)

323-343: Nice normalisation: Yoda expressions now handled.

Returning the non-nullish side via Option<AnyJsExpression> is a clean API for callers.


323-343: All internal references to is_optional_chain_like have been removed; only extract_optional_chain_like remains—no semver break within this workspace.

crates/biome_js_analyze/src/lint/complexity/use_optional_chain.rs (1)

266-270: Good switch to the extractor.

Normalising with extract_optional_chain_like()? simplifies the flow and covers Yoda cases.

Copy link

codspeed-hq bot commented Sep 3, 2025

CodSpeed Performance Report

Merging #7387 will not alter performance

Comparing qraqras:dev/7381 (f2296b3) with main (ab06a7e)

Summary

✅ 133 untouched benchmarks

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.

Snapshots look good. Nice! Please fix the lint :)

@qraqras
Copy link
Contributor Author

qraqras commented Sep 4, 2025

Whoops — I'm ready now.

@qraqras
Copy link
Contributor Author

qraqras commented Sep 4, 2025

benchmark tests failed... 😞

@dyc3 dyc3 merged commit 923674d into biomejs:main Sep 4, 2025
36 of 54 checks passed
@github-actions github-actions bot mentioned this pull request Sep 4, 2025
@qraqras qraqras deleted the dev/7381 branch September 8, 2025 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter A-Parser Area: parser L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💅 useOptionalChain doesn't work when operands are swapped.

3 participants

点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载