Skip to content

fix(inference): harden Salsa type inference - #10932

Merged
ematipico merged 1 commit into
mainfrom
02-salsa-inference-hardening
Jul 15, 2026
Merged

ematipico merged 1 commit into
mainfrom
02-salsa-inference-hardening

Conversation

@ematipico

@ematipico ematipico commented Jul 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Harden the Salsa-backed type-inference layer before migrating type-aware lint rules in the following PRs.

This introduces InferredType as a query API over interned Salsa types and adds normalised inference queries to TypedService for expressions, bindings, function and member return types, callable members, and call arguments. It also fixes resolution and normalisation gaps involving Promise chains, optional calls, tuples, unions, qualified names, computed well-known symbols, getter members, namespace exports, and branded primitive intersections.

Test Plan

Added module-graph regression tests for unresolved unions, shorthand members, multi-segment qualifiers, large namespace exports, Promise member chains, floating Promise shapes, optional calls, and branded intersections. Updated the affected inference snapshots.

Docs

This PR was created with AI assistance (OpenCode).

@changeset-bot

changeset-bot Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ff456b5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Organic activity

No automation signals detected in the analyzed events.

View full analysis →

This is an automated analysis by AgentScan

@ematipico
ematipico changed the base branch from 01-workspace-document-only to graphite-base/10932 July 13, 2026 18:34
@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 Jul 13, 2026
@ematipico
ematipico force-pushed the graphite-base/10932 branch from 5d9a0ea to 8589845 Compare July 14, 2026 13:41
@ematipico
ematipico force-pushed the 02-salsa-inference-hardening branch from dfdba71 to 9f77a8c Compare July 14, 2026 13:41
@ematipico
ematipico changed the base branch from graphite-base/10932 to 01-workspace-document-only July 14, 2026 13:42
@ematipico
ematipico changed the base branch from 01-workspace-document-only to graphite-base/10932 July 14, 2026 13:42
@ematipico
ematipico force-pushed the 02-salsa-inference-hardening branch from 9f77a8c to 2dfb40b Compare July 14, 2026 13:56
@ematipico
ematipico force-pushed the graphite-base/10932 branch from 8589845 to f7279d8 Compare July 14, 2026 13:56
@ematipico
ematipico changed the base branch from graphite-base/10932 to 01-workspace-document-only July 14, 2026 13:56
@ematipico
ematipico changed the base branch from 01-workspace-document-only to graphite-base/10932 July 14, 2026 13:56
@ematipico
ematipico force-pushed the 02-salsa-inference-hardening branch from 2dfb40b to 6ba5c8a Compare July 14, 2026 14:11
@ematipico
ematipico force-pushed the graphite-base/10932 branch from f7279d8 to 1cfdad0 Compare July 14, 2026 14:11
@ematipico
ematipico changed the base branch from graphite-base/10932 to 01-workspace-document-only July 14, 2026 14:11
@ematipico
ematipico changed the base branch from 01-workspace-document-only to graphite-base/10932 July 14, 2026 14:11
@ematipico
ematipico force-pushed the 02-salsa-inference-hardening branch from 6ba5c8a to 5171884 Compare July 15, 2026 08:35
@ematipico
ematipico changed the base branch from graphite-base/10932 to 01-workspace-document-only July 15, 2026 08:36
@ematipico
ematipico changed the base branch from 01-workspace-document-only to graphite-base/10932 July 15, 2026 08:36
@ematipico
ematipico force-pushed the 02-salsa-inference-hardening branch from 5171884 to be52ca5 Compare July 15, 2026 08:43
@ematipico
ematipico force-pushed the graphite-base/10932 branch from 716c2a2 to 724ae92 Compare July 15, 2026 08:43
@ematipico
ematipico changed the base branch from graphite-base/10932 to 01-workspace-document-only July 15, 2026 08:44
@ematipico
ematipico changed the base branch from 01-workspace-document-only to graphite-base/10932 July 15, 2026 08:44
@ematipico
ematipico force-pushed the graphite-base/10932 branch from 724ae92 to 899c60d Compare July 15, 2026 09:19
@ematipico
ematipico force-pushed the 02-salsa-inference-hardening branch from be52ca5 to ff456b5 Compare July 15, 2026 09:19
@ematipico
ematipico changed the base branch from graphite-base/10932 to main July 15, 2026 09:19
@ematipico
ematipico marked this pull request as ready for review July 15, 2026 09:22
@ematipico
ematipico requested review from a team July 15, 2026 09:30
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds a public InferredType API with predicates, return-type diagnostics, switch-case analysis, and stringification analysis. It introduces named computed-member types, including well-known symbol members, and updates local and global inference. Module-graph resolution gains support for unions, tuples, promises, globals, namespaces, qualifiers, getters, and typeof values. TypedService now uses bottom-up inference and normalisation for expression, return-type, callability, and argument queries. Additional semantic binding lookup and specification tests are included.

Possibly related PRs

  • biomejs/biome#10888: Introduces the Salsa-backed inference queries used by the updated typed service.

Suggested labels: A-Resolver

Suggested reviewers: dyc3

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: strengthening the Salsa-backed type-inference layer.
Description check ✅ Passed The description clearly describes the inference hardening, new APIs, and regression coverage in the diff.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 02-salsa-inference-hardening

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
crates/biome_js_type_info/src/inferred_type.rs (1)

10-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add rustdoc to the new public APIs.

  • crates/biome_js_type_info/src/inferred_type.rs#L10-L47: document enum variants, evidence fields, and result semantics.
  • crates/biome_js_semantic/src/semantic_model/model.rs#L347-L352: document invalid-ID behaviour and model ownership.

As per coding guidelines, “Use rustdoc documentation for documenting new features, rule changes, and rule/assist options in Rust code.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/biome_js_type_info/src/inferred_type.rs` around lines 10 - 47, Add
rustdoc for the public APIs in
crates/biome_js_type_info/src/inferred_type.rs:10-47, documenting each
InferredSwitchCase and StringificationMode variant, the meaning of every
ReturnTypeEvidence field, and the semantics of MisleadingReturnType results.
Also document the public API at
crates/biome_js_semantic/src/semantic_model/model.rs:347-352, including its
invalid-ID behavior and ownership of the semantic model.

Source: Coding guidelines

crates/biome_module_graph/src/db/type_inference/qualifiers.rs (1)

20-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate "walk remaining path members" loop.

The new member-walking loop in resolve_qualifier (lines 55-62) is essentially the same pattern already used in resolve_global_member_qualifier (lines 196-198): repeatedly call resolve_static_member_expression on each remaining path segment. Worth extracting into a small shared helper (e.g. resolve_member_path(&mut self, target, members: &[Text]) -> Option<InferredTypeData<'db>>) to avoid the two implementations drifting apart later.

Separately, this is genuinely intricate control flow (scope walk + qualifier-specific binding lookup + member-path walk + several structural fallbacks below). Could you confirm multi-segment qualified-name resolution (e.g. namespace member access) has dedicated spec-test coverage in spec_tests_v2.rs?

♻️ Sketch of a shared helper
+    fn resolve_member_path(
+        &mut self,
+        mut target: InferredTypeData<'db>,
+        members: impl IntoIterator<Item = impl AsRef<str>>,
+    ) -> Option<InferredTypeData<'db>> {
+        for member in members {
+            target = self.resolve_static_member_expression(target, member.as_ref())?;
+        }
+        Some(target)
+    }

Also applies to: 177-201

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/biome_module_graph/src/db/type_inference/qualifiers.rs` around lines
20 - 63, Extract the repeated member-path traversal from resolve_qualifier and
resolve_global_member_qualifier into a shared helper such as
resolve_member_path, preserving the existing resolve_static_member_expression
behavior and Unknown/None fallback semantics. Add or confirm dedicated spec-test
coverage in spec_tests_v2.rs for multi-segment qualified-name and
namespace-member resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/biome_js_analyze/src/services/typed.rs`:
- Around line 411-420: Update the call-argument type resolution around the
InferredTypeData::Interface and InferredTypeData::Object branches, plus the
related paths at the referenced locations, so it does not select the first
matching call signature, union arm, or constructor via find_map. Resolve
overloaded candidates using the call context; otherwise return None unless every
applicable candidate agrees on the argument type, eliminating declaration-order
dependence.

In `@crates/biome_js_type_info/src/inferred_type.rs`:
- Around line 598-630: Update the shared type traversals, including
all_variants_match and the helpers covering is_all_string_like,
has_number_like_variant, and could_equal_string_literal, to descend into
TypeData::Intersection constituents. Apply all semantics where every primitive
constituent must satisfy the predicate and any semantics where at least one
constituent can satisfy it, preserving existing union and generic handling so
branded intersections such as string & Brand are classified correctly.

In `@crates/biome_module_graph/src/db/type_inference/expressions.rs`:
- Around line 1096-1117: Update promise_member_type to preserve the target
Promise’s generic type arguments and model callback parameters and return types
for then, catch, and finally. Resolve the real Promise member signature when
available, or synthesize an equivalent function using target’s instance type
parameters and the callback’s inferred return type, so Promise<number>.finally()
retains number and then callbacks produce the resulting Promise type.

In `@crates/biome_module_graph/src/db/type_inference/imports.rs`:
- Around line 100-104: Update the namespace collection loop that processes
collection.stack in the import type-inference flow so exhausting
collection.remaining_steps does not return the partially collected Namespace.
Preserve the prior fail-closed Unknown result, or propagate an explicit
truncation state that prevents incomplete exports from being treated as absent.

---

Nitpick comments:
In `@crates/biome_js_type_info/src/inferred_type.rs`:
- Around line 10-47: Add rustdoc for the public APIs in
crates/biome_js_type_info/src/inferred_type.rs:10-47, documenting each
InferredSwitchCase and StringificationMode variant, the meaning of every
ReturnTypeEvidence field, and the semantics of MisleadingReturnType results.
Also document the public API at
crates/biome_js_semantic/src/semantic_model/model.rs:347-352, including its
invalid-ID behavior and ownership of the semantic model.

In `@crates/biome_module_graph/src/db/type_inference/qualifiers.rs`:
- Around line 20-63: Extract the repeated member-path traversal from
resolve_qualifier and resolve_global_member_qualifier into a shared helper such
as resolve_member_path, preserving the existing resolve_static_member_expression
behavior and Unknown/None fallback semantics. Add or confirm dedicated spec-test
coverage in spec_tests_v2.rs for multi-segment qualified-name and
namespace-member resolution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 51dd0803-ba37-45be-8200-1ec09381f73f

📥 Commits

Reviewing files that changed from the base of the PR and between 899c60d and ff456b5.

⛔ Files ignored due to path filters (5)
  • crates/biome_js_type_info/tests/snapshots/infer_resolved_type_of_async_disposable_object.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_type_info/tests/snapshots/infer_resolved_type_of_disposable_object.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_infer_module_types_merges_mixed_intersections_on_build.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_infer_module_types_preserves_floating_promise_shapes.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_infer_module_types_resolves_promise_member_chain.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (17)
  • crates/biome_js_analyze/src/services/typed.rs
  • crates/biome_js_semantic/src/semantic_model/model.rs
  • crates/biome_js_type_info/src/builders.rs
  • crates/biome_js_type_info/src/format_inferred_type_info.rs
  • crates/biome_js_type_info/src/inferred_type.rs
  • crates/biome_js_type_info/src/interned_types.rs
  • crates/biome_js_type_info/src/lib.rs
  • crates/biome_js_type_info/src/local_inference.rs
  • crates/biome_module_graph/src/db/queries.rs
  • crates/biome_module_graph/src/db/type_inference/expressions.rs
  • crates/biome_module_graph/src/db/type_inference/globals.rs
  • crates/biome_module_graph/src/db/type_inference/imports.rs
  • crates/biome_module_graph/src/db/type_inference/lookup.rs
  • crates/biome_module_graph/src/db/type_inference/qualifiers.rs
  • crates/biome_module_graph/src/db/type_inference/resolver.rs
  • crates/biome_module_graph/src/js_module_info.rs
  • crates/biome_module_graph/tests/spec_tests_v2.rs

Comment thread crates/biome_js_analyze/src/services/typed.rs
Comment on lines +598 to +630
fn all_variants_match(self, mut predicate: impl FnMut(TypeData<'db>) -> bool) -> bool {
let mut saw_variant = false;
let mut seen = FxHashSet::default();
let mut pending = vec![self.data];

for _ in 0..MAX_TYPE_VARIANT_STEPS {
let Some(data) = pending.pop() else {
return saw_variant;
};
if !seen.insert(data) {
continue;
}

match data {
TypeData::Union(union) => {
if union.types(self.db).is_empty() {
return false;
}
pending.extend(union.types(self.db).iter().copied());
}
TypeData::Generic(generic) => {
let Some(constraint) = generic.constraint(self.db) else {
return false;
};
pending.push(constraint);
}
_ if predicate(data) => saw_variant = true,
_ => return false,
}
}

false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Handle preserved branded intersections in the shared traversals.

string & Brand reaches these helpers as an Intersection; none descends into it, so predicates such as is_all_string_like, has_number_like_variant, and could_equal_string_literal incorrectly return false. Traverse primitive intersection constituents with the appropriate all/any semantics.

Also applies to: 689-729, 731-761

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/biome_js_type_info/src/inferred_type.rs` around lines 598 - 630,
Update the shared type traversals, including all_variants_match and the helpers
covering is_all_string_like, has_number_like_variant, and
could_equal_string_literal, to descend into TypeData::Intersection constituents.
Apply all semantics where every primitive constituent must satisfy the predicate
and any semantics where at least one constituent can satisfy it, preserving
existing union and generic handling so branded intersections such as string &
Brand are classified correctly.

Comment on lines +1096 to +1117
fn promise_member_type(
&self,
target: InferredTypeData<'db>,
member_name: &str,
) -> Option<(InferredTypeData<'db>, bool)> {
if !target.is_promise_class(self.db) || !matches!(member_name, "catch" | "finally" | "then")
{
return None;
}

let return_type = InferredTypeData::instance_of(self.db, target, Box::default());
Some((
InferredTypeData::Function(InferredFunction::new(
self.db,
Box::default(),
Box::default(),
InferredReturnType::Type(return_type),
false,
None,
)),
false,
))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve Promise generic semantics instead of returning a bare Promise.

The synthetic then/catch/finally function has no parameters and returns Promise without type arguments. Consequently, Promise<number>.finally() loses number, while .then(() => "value") cannot infer Promise<string>. Resolve the real member signature or synthesise it using the instance type parameters and callback return type.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/biome_module_graph/src/db/type_inference/expressions.rs` around lines
1096 - 1117, Update promise_member_type to preserve the target Promise’s generic
type arguments and model callback parameters and return types for then, catch,
and finally. Resolve the real Promise member signature when available, or
synthesize an equivalent function using target’s instance type parameters and
the callback’s inferred return type, so Promise<number>.finally() retains number
and then callbacks produce the resulting Promise type.

Comment thread crates/biome_module_graph/src/db/type_inference/imports.rs
@ematipico

Copy link
Copy Markdown
Member Author

I'll apply the fixes to the comments from CodeRabbit to the 07 PR, since that's where most of them are.

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