Skip to content

Conversation

@ematipico
Copy link
Member

Summary

Part of #6611

This PR improves the inference engine by updating the type of those bindings that are assigned to multiple values.

The implementation is quite naive for now, so if there more than one assignments, we create new types for each one of them. You can see it from the snapshots.

cc @arendjr

Test Plan

Added new tests

Docs

@changeset-bot
Copy link

changeset-bot bot commented Nov 16, 2025

🦋 Changeset detected

Latest commit: 7b39a52

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

@ematipico ematipico requested review from a team and arendjr November 16, 2025 13:33
@github-actions github-actions bot added A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Type-Inference Area: type inference labels Nov 16, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 16, 2025

Walkthrough

This pull request enhances Biome's type inference engine to better handle variables assigned multiple values. It introduces a new public API method union_with on the TypeResolver trait for creating union types, and refactors the module graph collector to detect and process writable references. The noUnnecessaryConditions rule is improved to avoid false positives when variables are mutated inside modules. Test coverage is expanded with new cases for type widening via single and multiple assignments.

Suggested reviewers

  • siketyan

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the primary change: improving type inference for variables assigned multiple values, which aligns with the changeset and code modifications.
Description check ✅ Passed The description clearly relates to the changeset, explaining the inference engine improvements and multiple value assignments, with reference to issue #6611.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/inference-assignments

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

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 (4)
crates/biome_js_type_info/src/resolver.rs (1)

725-731: union_with helper is fine, but note the “naive” union growth

The implementation matches optional and does what the PR describes: it always creates a fresh two‑member Union from current_type and ty and registers it.

That’s perfectly OK for the current “naive” widening, but it does mean repeated widening will keep allocating new union types without flattening or deduplication. If unions ever start to get large or performance becomes an issue, this might be a good place to introduce flattening/reuse.

.changeset/upset-cameras-walk.md (1)

5-5: Tiny wording tweak (optional)

Pure style, but this reads a bit smoother without the comma:

-Improved the type inference engine, by resolving types for variables that are assigned to multiple values.
+Improved the type inference engine by resolving types for variables that are assigned to multiple values.
crates/biome_module_graph/tests/spec_tests.rs (1)

2132-2163: New widening snapshot test is consistent and focused

This is a clean minimal case for “boolean literal + reassignment” widening; setup mirrors other snapshot tests and the relative "index.ts" path is used consistently, so behaviour should be stable.

If you want to align style with nearby tests, you could optionally write:

let snapshot = ModuleGraphSnapshot::new(module_graph.as_ref(), &fs).with_resolver(resolver.as_ref());

but the current &module_graph form is perfectly fine thanks to deref‑coercion.

crates/biome_module_graph/src/js_module_info/collector.rs (1)

609-648: Code duplication in the if-else branches.

Lines 613–616 and 643–648 perform identical lookups from typed_bindings. The else branch is redundant; you can simplify by initialising ty once (lines 613–616), applying the writable-reference union conditionally, then returning ty at the end.

Apply this refactor to eliminate duplication:

-                let ty = if let Some(typed_bindings) = decl
+                let mut ty = if let Some(typed_bindings) = decl
                     .as_js_variable_declaration()
                     .and_then(|decl| self.variable_declarations.get(decl.syntax()))
                 {
-                    let mut ty = typed_bindings
+                    typed_bindings
                         .iter()
                         .find_map(|(name, ty)| (name == binding_name).then(|| ty.clone()))
-                        .unwrap_or_default();
-
+                        .unwrap_or_default()
+                } else {
+                    let data = TypeData::from_any_js_declaration(self, scope_id, &decl);
+                    self.reference_to_owned_data(data)
+                };
+
                     if self.has_writable_reference(&binding) {
                         let references = self.get_writable_references(&binding);
                         for reference in references {
                             let Some(node) = self.binding_node_by_start.get(&reference.range_start)
                             else {
                                 continue;
                             };
                             for ancestor in node.ancestors().skip(1) {
                                 if let Some(assignment) =
                                     JsAssignmentExpression::cast_ref(&ancestor)
                                     && let Ok(right) = assignment.right()
                                 {
                                     let data =
                                         TypeData::from_any_js_expression(self, scope_id, &right);
                                     let assigned_type = self.reference_to_owned_data(data);
                                     ty = ResolvedTypeId::new(
                                         self.level(),
                                         self.union_with(ty.clone(), assigned_type).into(),
                                     )
                                     .into();
                                 }
                             }
                         }
-
-                        ty
-                    } else {
-                        typed_bindings
-                            .iter()
-                            .find_map(|(name, ty)| (name == binding_name).then(|| ty.clone()))
-                            .unwrap_or_default()
                     }
-                } else {
-                    let data = TypeData::from_any_js_declaration(self, scope_id, &decl);
-                    self.reference_to_owned_data(data)
-                };
📜 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 47d940e and 06c85ff.

⛔ Files ignored due to path filters (2)
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryConditions/validAssignment.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_widening_via_assignment.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (7)
  • .changeset/lovely-sloths-chew.md (1 hunks)
  • .changeset/upset-cameras-walk.md (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryConditions/validAssignment.ts (1 hunks)
  • crates/biome_js_type_info/src/resolver.rs (1 hunks)
  • crates/biome_module_graph/src/js_module_info/collector.rs (2 hunks)
  • crates/biome_module_graph/tests/snapshots/test_widening_via_assignment_multiple_values.snap.new (1 hunks)
  • crates/biome_module_graph/tests/spec_tests.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-20T16:24:59.781Z
Learnt from: arendjr
Repo: biomejs/biome PR: 7266
File: crates/biome_js_type_info/src/type.rs:94-102
Timestamp: 2025-08-20T16:24:59.781Z
Learning: In crates/biome_js_type_info/src/type.rs, the flattened_union_variants() method returns TypeReference instances that already have the correct module IDs applied to them. These references should be used directly with resolver.resolve_reference() without applying additional module ID transformations, as variant references may originate from nested unions in different modules.

Applied to files:

  • crates/biome_js_type_info/src/resolver.rs
  • crates/biome_module_graph/src/js_module_info/collector.rs
  • crates/biome_module_graph/tests/snapshots/test_widening_via_assignment_multiple_values.snap.new
🧬 Code graph analysis (2)
crates/biome_module_graph/tests/spec_tests.rs (3)
crates/biome_fs/src/fs/memory.rs (1)
  • default (37-49)
crates/biome_test_utils/src/lib.rs (1)
  • get_added_paths (169-190)
crates/biome_module_graph/src/js_module_info/module_resolver.rs (1)
  • for_module (76-91)
crates/biome_module_graph/src/js_module_info/collector.rs (3)
crates/biome_js_type_info/src/type_data.rs (8)
  • ty (545-550)
  • index (45-47)
  • index (1431-1433)
  • index (1472-1476)
  • reference (343-345)
  • new (39-43)
  • new (1425-1429)
  • new (1460-1470)
crates/biome_module_graph/src/js_module_info.rs (1)
  • binding (264-266)
crates/biome_js_type_info/src/local_inference.rs (3)
  • from_any_js_expression (425-602)
  • from_any_js_expression (2173-2180)
  • from_any_js_expression (2343-2425)
🪛 LanguageTool
.changeset/lovely-sloths-chew.md

[uncategorized] ~5-~5: Possible missing comma found.
Context: ... of the rule noUnnecessaryConditions. Now the rule doesn't isn't triggered for va...

(AI_HYDRA_LEO_MISSING_COMMA)


[grammar] ~5-~5: Two consecutive contractions are very uncommon. Did you maybe just mean “doesn't” or “isn't”?
Context: ...noUnnecessaryConditions. Now the rule doesn't isn't triggered for variables that are mutate...

(DON_T_AREN_T)

⏰ 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). (12)
  • GitHub Check: autofix
  • GitHub Check: Bench (biome_module_graph)
  • GitHub Check: Test Node.js API
  • GitHub Check: Documentation
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: End-to-end tests
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Check Dependencies
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_js_formatter)
  • GitHub Check: Bench (biome_js_parser)
🔇 Additional comments (7)
crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryConditions/validAssignment.ts (1)

1-9: Targeted regression fixture looks good

This neatly captures the “mutated in the same module before the condition” case for noUnnecessaryConditions. No changes needed from my side.

crates/biome_module_graph/tests/spec_tests.rs (1)

2165-2200: Good coverage for multi‑value widening

Nice to see a dedicated case where a binding starts as undefined and is then assigned both a string and a number in separate functions; that should exercise the “multiple assignments → union” path nicely. Snapshot wiring matches the previous test and looks correct.

Nothing I’d change here.

crates/biome_module_graph/tests/snapshots/test_widening_via_assignment_multiple_values.snap.new (1)

1-99: Snapshot looks good!

The progressive type widening from undefinedundefined | stringundefined | string | number correctly reflects the multiple assignment behavior. The structure is clear and validates the new inference logic.

crates/biome_module_graph/src/js_module_info/collector.rs (4)

6-9: LGTM!

Import changes appropriately include JsAssignmentExpression for the new assignment-tracking logic.


578-578: Clone is acceptable here.

Cloning the binding provides access to metadata needed for the improved inference. Since this occurs once per binding during collection, the performance cost is reasonable.


584-598: Helper methods look solid.

Clean, focused implementations for detecting and collecting writable references. These support the new type-widening logic nicely.


654-654: Early return is appropriate.

Returning after resolving the type from variable declarations prevents unnecessary traversal of further ancestors and maintains the existing control-flow pattern.

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 16, 2025

CodSpeed Performance Report

Merging #8119 will not alter performance

Comparing fix/inference-assignments (7b39a52) with main (1fdcaf0)

Summary

✅ 58 untouched
⏩ 95 skipped1

Footnotes

  1. 95 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.

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

📜 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 06c85ff and 7b39a52.

⛔ Files ignored due to path filters (1)
  • crates/biome_module_graph/tests/snapshots/test_widening_via_assignment_multiple_values.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (2)
  • .changeset/lovely-sloths-chew.md (1 hunks)
  • crates/biome_module_graph/src/js_module_info/collector.rs (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: arendjr
Repo: biomejs/biome PR: 7266
File: crates/biome_js_type_info/src/type.rs:94-102
Timestamp: 2025-08-20T16:24:59.781Z
Learning: In crates/biome_js_type_info/src/type.rs, the flattened_union_variants() method returns TypeReference instances that already have the correct module IDs applied to them. These references should be used directly with resolver.resolve_reference() without applying additional module ID transformations, as variant references may originate from nested unions in different modules.
📚 Learning: 2025-08-20T16:24:59.781Z
Learnt from: arendjr
Repo: biomejs/biome PR: 7266
File: crates/biome_js_type_info/src/type.rs:94-102
Timestamp: 2025-08-20T16:24:59.781Z
Learning: In crates/biome_js_type_info/src/type.rs, the flattened_union_variants() method returns TypeReference instances that already have the correct module IDs applied to them. These references should be used directly with resolver.resolve_reference() without applying additional module ID transformations, as variant references may originate from nested unions in different modules.

Applied to files:

  • crates/biome_module_graph/src/js_module_info/collector.rs
📚 Learning: 2025-08-05T14:43:29.581Z
Learnt from: dyc3
Repo: biomejs/biome PR: 7081
File: packages/@biomejs/biome/configuration_schema.json:7765-7781
Timestamp: 2025-08-05T14:43:29.581Z
Learning: The file `packages/biomejs/biome/configuration_schema.json` is auto-generated and should not be manually edited or reviewed for schema issues; any changes should be made at the code generation source.

Applied to files:

  • .changeset/lovely-sloths-chew.md
🧬 Code graph analysis (1)
crates/biome_module_graph/src/js_module_info/collector.rs (2)
crates/biome_module_graph/src/js_module_info.rs (1)
  • binding (264-266)
crates/biome_js_type_info/src/local_inference.rs (3)
  • from_any_js_expression (425-602)
  • from_any_js_expression (2173-2180)
  • from_any_js_expression (2343-2425)
🪛 LanguageTool
.changeset/lovely-sloths-chew.md

[uncategorized] ~5-~5: Possible missing comma found.
Context: ... of the rule noUnnecessaryConditions. Now the rule isn't triggered for variables ...

(AI_HYDRA_LEO_MISSING_COMMA)

⏰ 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). (6)
  • GitHub Check: Test Node.js API
  • GitHub Check: Bench (biome_module_graph)
  • GitHub Check: autofix
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_js_formatter)
🔇 Additional comments (2)
crates/biome_module_graph/src/js_module_info/collector.rs (2)

584-598: Clean helper methods.

These helper methods are well-structured and serve their purpose clearly.


618-641: The union_with method exists and is correctly used; however, scope handling requires architectural clarification.

The call to self.union_with(ty.clone(), assigned_type) on line 635 is valid—the method exists on the TypeResolver trait that JsModuleInfoCollector implements.

However, the scope concern remains: line 631 uses the binding's declaration scope (scope_id) to infer the assignment's RHS type, even when the assignment occurs in a nested scope (e.g., inside a function). Since scope_by_range is available elsewhere in the collector, consider whether you should look up the assignment's actual scope rather than using the binding's declaration scope. This matters especially for resolving identifiers referenced in complex RHS expressions within different scopes.

Comment on lines +643 to +648
} else {
typed_bindings
.iter()
.find_map(|(name, ty)| (name == binding_name).then(|| ty.clone()))
.unwrap_or_default()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate logic.

The else branch duplicates the exact logic from lines 613-616. Since ty is already computed, simply return it directly instead of re-computing.

Apply this diff:

     } else {
-        typed_bindings
-            .iter()
-            .find_map(|(name, ty)| (name == binding_name).then(|| ty.clone()))
-            .unwrap_or_default()
+        ty
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In crates/biome_module_graph/src/js_module_info/collector.rs around lines
643-648, the else branch re-runs the same find_map logic already used at lines
613-616; replace the duplicated lookup with returning the previously computed ty
value (use ty or ty.clone() as appropriate for ownership) instead of recomputing
the binding type.

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

Labels

A-Linter Area: linter A-Project Area: project A-Type-Inference Area: type inference L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants