Skip to content

Conversation

@qraqras
Copy link
Contributor

@qraqras qraqras commented Sep 6, 2025

Summary

Closes #7212

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

Test Plan

Added snap tests.

Docs

changeset only

@changeset-bot
Copy link

changeset-bot bot commented Sep 6, 2025

🦋 Changeset detected

Latest commit: 0ba5afc

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 6, 2025

Walkthrough

Refactors optional-chain detection in crates/biome_js_syntax/src/expr_ext.rs by splitting JsBinaryExpression::extract_optional_chain_like into two private helpers (nullish and typeof-based extraction). Adds support for typeof-based guards in both normal and Yoda order (e.g., typeof foo !== "undefined" and "undefined" !== typeof foo). Adds numerous tests under crates/biome_js_analyze/tests/specs/complexity/useOptionalChain covering typeof guards (normal/Yoda, single/double/template quotes, member/element access, calls). Adds changeset documenting fix for issue #7212. No public API signatures changed.

Assessment against linked issues

Objective Addressed Explanation
Consider typeof foo !== "undefined" in useOptionalChain (#7212)

Possibly related PRs

Suggested reviewers

  • dyc3
  • ematipico

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 954c20b and 0ba5afc.

📒 Files selected for processing (1)
  • .changeset/lazy-animals-burn.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/lazy-animals-burn.md
⏰ 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). (9)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Check Dependencies
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Documentation
  • GitHub Check: Test Node.js API
  • GitHub Check: autofix
  • GitHub Check: Parser conformance
✨ Finishing Touches
🧪 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

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

@github-actions github-actions bot added A-Linter Area: linter A-Parser Area: parser L-JavaScript Language: JavaScript and super languages labels Sep 6, 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/lazy-animals-burn.md (1)

5-6: Polish the changeset: trailing period, locale‑neutral docs link, and add a tiny example.

Keeps release notes crisp and consistent with the repo’s conventions.

-Fixed [#7212](https://github.com/biomejs/biome/issues/7212), now the [`useOptionalChain`](https://biomejs.dev/ja/linter/rules/use-optional-chain/) rule recognizes optional chaining using `typeof` (e.g., `typeof foo !== 'undefined' && foo.bar`).
+Fixed [#7212](https://github.com/biomejs/biome/issues/7212), now the [`useOptionalChain`](https://biomejs.dev/linter/rules/use-optional-chain/) rule recognizes optional chaining using `typeof` (e.g., `typeof foo !== 'undefined' && foo.bar`).
+
+#### Example
+
+```ts
+// Before
+typeof foo !== "undefined" && foo.bar;
+// After
+foo?.bar;
+```
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts (1)

46-48: Add parenthesised Yoda variants to harden parsing/normalisation.

Catches spacing/precedence quirks while remaining “valid” (always true) guards.

 undefined !== typeof foo && foo.bar;
 undefined != typeof foo && foo.bar;
+ (undefined !== typeof foo) && foo.bar;
+ (undefined != typeof foo) && foo.bar;
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/validCases.ts (1)

46-48: Mirror with parenthesised typeof variants (also always true).

Prevents accidental regressions in token handling.

 typeof foo !== undefined && foo.bar;
 typeof foo != undefined && foo.bar;
+ (typeof foo !== undefined) && foo.bar;
+ (typeof foo != undefined) && foo.bar;
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases1.ts (1)

1-8: Add a couple of parenthesised forms for lhs evaluation variance.

These help ensure we don’t overfit to a specific token layout.

 typeof foo !== 'undefined' && foo.bar;
 typeof foo.bar !== 'undefined' && foo.bar.baz;
+ (typeof foo !== 'undefined') && foo.bar;
+ typeof (foo) !== 'undefined' && foo();
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases2.ts (1)

1-8: Non‑strict variants: add parenthesised forms too.

Keeps parity with the strict set and exercises parser edges.

 typeof foo != 'undefined' && foo.bar;
 typeof foo.bar != 'undefined' && foo.bar.baz;
+ (typeof foo != 'undefined') && foo.bar;
+ typeof (foo) != 'undefined' && foo();
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases2.ts (1)

1-54: Great Yoda-style coverage; add a few parenthesised operands.

To guard against regressions when users wrap the typeof operand, add variants with parentheses.

+// parenthesised typeof operands
+'undefined' != typeof (foo) && foo.bar;
+'undefined' != typeof (foo.bar) && foo.bar.baz;
+'undefined' != typeof (foo[bar]) && foo[bar].baz;
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases1.ts (1)

1-54: Solid set; mirror strict-inequality cases with parentheses.

Parity with the loose-inequality fixture helps. Add parenthesised forms.

+// parenthesised typeof operands
+'undefined' !== typeof (foo) && foo.bar;
+'undefined' !== typeof (foo.bar) && foo.bar.baz;
+'undefined' !== typeof (foo[bar]) && foo[bar].baz;
crates/biome_js_syntax/src/expr_ext.rs (3)

349-391: Minor simplification opportunity in nullish extractor.

The two branches can be expressed as an XOR, returning the non-nullish side. Purely cosmetic.

-        let left_is_nullish = is_nullish(left);
-        let right_is_nullish = is_nullish(right);
-        // right only nullish: `foo !== undefined` -> return foo (left)
-        if !left_is_nullish && right_is_nullish {
-            return Ok(Some(left.clone()));
-        }
-        // left only nullish: `undefined !== foo` -> return foo (right)
-        if left_is_nullish && !right_is_nullish {
-            return Ok(Some(right.clone()));
-        }
+        let left_is_nullish = is_nullish(left);
+        let right_is_nullish = is_nullish(right);
+        if left_is_nullish ^ right_is_nullish {
+            return Ok(Some(if left_is_nullish { right.clone() } else { left.clone() }));
+        }

392-404: Docs: replace smart quotes with ASCII quotes.

The example line uses a curly quote, which isn’t valid JS and may confuse readers.

-    /// ”undefined” != typeof foo;  // -> Some(foo)
+    /// "undefined" != typeof foo;  // -> Some(foo)

414-422: Normalise typeof argument by stripping parentheses.

Returning the unparenthesised argument avoids spurious mismatches downstream.

-        fn typeof_argument(expression: &AnyJsExpression) -> SyntaxResult<Option<AnyJsExpression>> {
+        fn typeof_argument(expression: &AnyJsExpression) -> SyntaxResult<Option<AnyJsExpression>> {
             if let Some(unary) = expression.as_js_unary_expression() {
                 return Ok(match unary.operator()? {
-                    JsUnaryOperator::Typeof => Some(unary.argument()?),
+                    JsUnaryOperator::Typeof => {
+                        let arg = unary.argument()?;
+                        Some(arg.omit_parentheses())
+                    }
                     _ => None,
                 });
             }
             Ok(None)
         }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53ff5ae and 954c20b.

⛔ Files ignored due to path filters (6)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases1.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases2.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/validCases.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases1.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases2.ts.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/lazy-animals-burn.md (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases1.ts (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases2.ts (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/validCases.ts (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases1.ts (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases2.ts (1 hunks)
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts (1 hunks)
  • crates/biome_js_syntax/src/expr_ext.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
.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/lazy-animals-burn.md
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/typeofLogicalAndCases1.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases2.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases2.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases1.ts
  • crates/biome_js_syntax/src/expr_ext.rs
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases1.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases2.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases2.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases1.ts
  • crates/biome_js_syntax/src/expr_ext.rs
**/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/typeofLogicalAndCases1.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases2.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/validCases.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases2.ts
  • crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/yoda_expressions_typeofLogicalAndCases1.ts
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

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

Files:

  • crates/biome_js_syntax/src/expr_ext.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_js_type_info/CONTRIBUTING.md:0-0
Timestamp: 2025-08-11T11:50:12.090Z
Learning: Applies to crates/biome_js_type_info/src/**/*.rs : Use TypeData::Unknown for unimplemented inference and TypeData::UnknownKeyword for the explicit TypeScript unknown keyword; treat them semantically the same but keep them distinct for measurement
⏰ 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). (10)
  • GitHub Check: Test Node.js API
  • GitHub Check: autofix
  • GitHub Check: End-to-end tests
  • GitHub Check: Documentation
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Check Dependencies
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Parser conformance
🔇 Additional comments (2)
crates/biome_js_analyze/tests/specs/complexity/useOptionalChain/typeofLogicalAndCases1.ts (1)

1-8: Nothing to see—Yoda already quotes “undefined” in yoda_expressions_typeofLogicalAndCases1.ts and 2.ts.

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

323-347: Refactor reads clean and keeps behaviour scoped to !=/!==.

Delegating to specialised helpers is tidy and easy to extend. No functional concerns here.

@ematipico ematipico merged commit d042f18 into biomejs:main Sep 7, 2025
14 checks passed
@github-actions github-actions bot mentioned this pull request Sep 7, 2025
@qraqras qraqras deleted the dev/7212-2 branch September 8, 2025 07:42
kedevked pushed a commit to kedevked/biome that referenced this pull request Sep 22, 2025
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 consider typeof foo !== "undefined"

2 participants