Thanks to visit codestin.com
Credit goes to github.com

Skip to content

refactor: use semantic model for module graph#9211

Open
ematipico wants to merge 8 commits intomainfrom
refactor/use-one-semantic-model
Open

refactor: use semantic model for module graph#9211
ematipico wants to merge 8 commits intomainfrom
refactor/use-one-semantic-model

Conversation

@ematipico
Copy link
Member

@ematipico ematipico commented Feb 23, 2026

Summary

Closes #7905

This PR refactors our module graph and type inference to use the same semantic model created in the workspace server.

This should speed things up and possibly reduce memory usage, since we don't duplicate anything. However, this is just an educated guess; I didn't test anything.

Since this is a refactor, it was a good use of AI. I made sure there hasn't been a regression in functionality. On the other hand, we have better information inside the module graph, as you can see from the updated snapshots.

Test Plan

Green CI

Docs

@changeset-bot
Copy link

changeset-bot bot commented Feb 23, 2026

🦋 Changeset detected

Latest commit: c9d2624

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-Project Area: project L-JavaScript Language: JavaScript and super languages A-Type-Inference Area: type inference labels Feb 23, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Feb 23, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 156 skipped benchmarks1


Comparing refactor/use-one-semantic-model (c9d2624) with main (1992a85)2

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.

  2. No successful run was found on main (2368aa2) during the generation of this report, so 1992a85 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 23, 2026

Parser conformance results on

js/262

Test result main count This PR count Difference
Total 52923 52923 0
Passed 51703 51703 0
Failed 1178 1178 0
Panics 42 42 0
Coverage 97.69% 97.69% 0.00%

jsx/babel

Test result main count This PR count Difference
Total 38 38 0
Passed 37 37 0
Failed 1 1 0
Panics 0 0 0
Coverage 97.37% 97.37% 0.00%

markdown/commonmark

Test result main count This PR count Difference
Total 652 652 0
Passed 652 652 0
Failed 0 0 0
Panics 0 0 0
Coverage 100.00% 100.00% 0.00%

symbols/microsoft

Test result main count This PR count Difference
Total 5464 5464 0
Passed 1915 1915 0
Failed 3549 3549 0
Panics 0 0 0
Coverage 35.05% 35.05% 0.00%

ts/babel

Test result main count This PR count Difference
Total 635 635 0
Passed 567 567 0
Failed 68 68 0
Panics 0 0 0
Coverage 89.29% 89.29% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18871 18871 0
Passed 13012 13012 0
Failed 5858 5858 0
Panics 1 1 0
Coverage 68.95% 68.95% 0.00%

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Const constructors and const accessors were added for BindingId and ScopeId (including ScopeId::GLOBAL). SemanticModel gains scope_for_range(TextRange) -> Scope and scope_from_id(ScopeId) -> Scope, and Scope exposes id() -> ScopeId. biome_js_type_info now re-exports BindingId/ScopeId from biome_js_semantic. The module-graph and related crates were rewired to thread an Arc<SemanticModel> through visitors, collectors and benches, replacing local scope/binding storage with semantic-model-backed lookups and per-binding augmentation keyed by TextRange; several APIs and enum variants now carry TextRange instead of BindingId.

Possibly related PRs

Suggested labels

A-Core

Suggested reviewers

  • arendjr
  • dyc3
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor: use semantic model for module graph' clearly and concisely summarises the main change—consolidating semantic model usage across the module graph instead of duplicating it.
Description check ✅ Passed The PR description clearly articulates the refactoring objective to use the workspace server's semantic model for module graph and type inference, avoiding duplication.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/use-one-semantic-model

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_module_graph/src/js_module_info/module_resolver.rs (1)

157-187: ⚠️ Potential issue | 🟠 Major

Use scope_for_range and handle TypeReference::Import variants in type resolution.

The _range parameter is unused while the code hardcodes global scope (ScopeId::new(0)), so shadowed bindings won't resolve correctly. Additionally, only TypeReference::Resolved is matched (lines 177–180), causing Import references to fall back to GLOBAL_UNKNOWN_ID. Use the semantic model's scope_for_range(range) to get the innermost scope and call resolve_import for import references.

🧹 Nitpick comments (1)
crates/biome_module_graph/src/js_module_info/collector.rs (1)

577-599: TODO comment flags incomplete migration — consider tracking this.

The comment on lines 578-582 explains that type inference still uses the collector's temporary scopes rather than the semantic model directly. This is a reasonable incremental approach, but the TODO should be tracked to avoid becoming stale.

Would you like me to open an issue to track refactoring type inference to use semantic_model's scopes directly?

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

In `@crates/biome_module_graph/src/js_module_info/collector.rs` around lines 577 -
599, The TODO in infer_all_types indicates a needed refactor to use
semantic_model scopes but isn’t tracked; create a formal issue describing
migrating infer_all_types to use semantic_model scopes (mention
scope_range_by_start, scope_by_range, scope_id_for_range, binding_node_by_start,
infer_type) and then update the TODO comment to reference that issue ID/URL (or
add a TODO/FIXME with the issue number) so the work is discoverable and won't go
stale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_module_graph/src/js_module_info/module_resolver.rs`:
- Around line 421-454: The current logic checks
module.static_imports.contains_key(identifier.text()) regardless of whether
find_binding_in_scope returned a local binding, causing shadowed locals to be
treated as imports; update the branch to only treat the name as an import when
the binding returned by find_binding_in_scope actually corresponds to an import
declaration (e.g., inspect the returned binding or its declaration kind from
binding_range to ensure it's an import) or compare the binding_range to the
import's declaration range before calling resolve_import (otherwise fall back to
binding_type_data + resolve_reference); reference symbols:
find_binding_in_scope, binding_range, static_imports.contains_key,
static_import_paths, resolve_import, binding_type_data, resolve_reference,
semantic_scope_id.

In `@crates/biome_service/src/workspace/server.rs`:
- Around line 1016-1033: The update currently calls
self.module_graph.update_graph_for_js_paths with
services.semantic_model.expect(...), which can panic because
services.as_js_services() may not have a semantic_model when lint/assist are
disabled; change the call site (inside the
SendNode::into_language_root::<AnyJsRoot> / services.as_js_services() branch) to
handle the Option from services.semantic_model safely: either (a) construct or
obtain a lightweight fallback SemanticModel when semantic_model.is_none() and
pass that into update_graph_for_js_paths, or (b) skip/early-return the JS
module-graph update and log a debug message when semantic_model is missing;
ensure you reference services.semantic_model and update_graph_for_js_paths to
locate the code and avoid using expect() so no panic occurs.

---

Nitpick comments:
In `@crates/biome_module_graph/src/js_module_info/collector.rs`:
- Around line 577-599: The TODO in infer_all_types indicates a needed refactor
to use semantic_model scopes but isn’t tracked; create a formal issue describing
migrating infer_all_types to use semantic_model scopes (mention
scope_range_by_start, scope_by_range, scope_id_for_range, binding_node_by_start,
infer_type) and then update the TODO comment to reference that issue ID/URL (or
add a TODO/FIXME with the issue number) so the work is discoverable and won't go
stale.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0385eb2 and ecb9e17.

⛔ Files ignored due to path filters (18)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_module_graph/tests/snapshots/test_export_default_function_declaration.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_export_referenced_function.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_export_type_referencing_imported_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_export_types.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value_with_multiple_modules.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_import_as_namespace.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_merged_types.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_multiple_reexports.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_nested_function_call_with_namespace_in_return_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_promise_export.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_imported_promise_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_reexported_promise_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_single_reexport.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_type_of_this_in_class_export.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (20)
  • crates/biome_js_semantic/src/semantic_model/model.rs
  • crates/biome_js_type_info/Cargo.toml
  • crates/biome_js_type_info/src/type_data.rs
  • crates/biome_js_type_info/tests/local_inference.rs
  • crates/biome_js_type_info/tests/resolver.rs
  • crates/biome_module_graph/benches/module_graph.rs
  • crates/biome_module_graph/src/format_module_graph.rs
  • crates/biome_module_graph/src/js_module_info.rs
  • crates/biome_module_graph/src/js_module_info/binding.rs
  • crates/biome_module_graph/src/js_module_info/collector.rs
  • crates/biome_module_graph/src/js_module_info/module_resolver.rs
  • crates/biome_module_graph/src/js_module_info/scope.rs
  • crates/biome_module_graph/src/js_module_info/visitor.rs
  • crates/biome_module_graph/src/lib.rs
  • crates/biome_module_graph/src/module_graph.rs
  • crates/biome_module_graph/tests/snap/mod.rs
  • crates/biome_module_graph/tests/spec_tests.rs
  • crates/biome_service/src/workspace/server.rs
  • crates/biome_test_utils/Cargo.toml
  • crates/biome_test_utils/src/lib.rs

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 (1)
crates/biome_module_graph/src/js_module_info/module_resolver.rs (1)

165-168: Consider using the global scope's ID directly.

The _global_scope variable is fetched but never used. If global_scope() returns something with an accessible ID, you could use that instead of hardcoding ScopeId::new(0).

🔧 Potential simplification

If the semantic model's Scope type exposes its ID, consider:

-        let _global_scope = module.0.semantic_model.global_scope();
-        // We need the internal ScopeId to pass to find_binding_in_scope
-        // SAFETY: The global scope is always at index 0
-        let scope_id = biome_js_semantic::ScopeId::new(0);
+        let scope_id = module.0.semantic_model.global_scope().id();

Otherwise, if this is intentional due to API limitations, removing the unused _global_scope assignment would tidy things up.

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

In `@crates/biome_module_graph/src/js_module_info/module_resolver.rs` around lines
165 - 168, The code fetches module.0.semantic_model.global_scope() into
_global_scope but then hardcodes biome_js_semantic::ScopeId::new(0) for
scope_id; either remove the unused _global_scope binding or, if the Scope object
exposes its id, derive scope_id from that global scope (use the global scope's
id instead of ScopeId::new(0)) so find_binding_in_scope gets the actual global
scope id; update the code around module.0.semantic_model.global_scope(),
scope_id, and the call to find_binding_in_scope accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_module_graph/src/js_module_info.rs`:
- Around line 104-111: The method scope_for_range should guard against empty
TextRange values before calling self.0.semantic_model.scope_for_range because
that method debug‑asserts on empty ranges; update scope_for_range to
early‑return JsScope { info: self.0.clone(), scope: self.0.global_scope() } (or
call an existing global_scope() helper) when range.is_empty() (or equivalent) is
true, otherwise delegate to semantic_model.scope_for_range(range) as before;
reference the scope_for_range function and semantic_model.scope_for_range in
your change.

---

Nitpick comments:
In `@crates/biome_module_graph/src/js_module_info/module_resolver.rs`:
- Around line 165-168: The code fetches module.0.semantic_model.global_scope()
into _global_scope but then hardcodes biome_js_semantic::ScopeId::new(0) for
scope_id; either remove the unused _global_scope binding or, if the Scope object
exposes its id, derive scope_id from that global scope (use the global scope's
id instead of ScopeId::new(0)) so find_binding_in_scope gets the actual global
scope id; update the code around module.0.semantic_model.global_scope(),
scope_id, and the call to find_binding_in_scope accordingly.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecb9e17 and 6c87805.

📒 Files selected for processing (3)
  • crates/biome_module_graph/src/js_module_info.rs
  • crates/biome_module_graph/src/js_module_info/module_resolver.rs
  • crates/biome_service/src/workspace/server.rs

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.

🧹 Nitpick comments (1)
crates/biome_module_graph/src/js_module_info.rs (1)

304-324: Consider extracting the scope-walk helper.
The parent-scope traversal is duplicated in is_binding_imported; a shared helper would reduce drift.

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

In `@crates/biome_module_graph/src/js_module_info.rs` around lines 304 - 324, The
scope-parent traversal logic in find_binding_in_scope is duplicated in
is_binding_imported; extract a shared helper (e.g., iterate_scope_chain or
find_in_scope_chain) that accepts a starting ScopeId and a closure/predicate to
run against each Scope, then refactor find_binding_in_scope to call that helper
and refactor is_binding_imported to use the same helper to check parent scopes;
reference the existing symbols find_binding_in_scope and is_binding_imported and
ensure the helper returns either the predicate result (Option<TextRange> for the
binding case or bool for imported check) or a common enum/Result that both
callers can use.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_module_graph/src/js_module_info.rs`:
- Around line 304-324: The scope-parent traversal logic in find_binding_in_scope
is duplicated in is_binding_imported; extract a shared helper (e.g.,
iterate_scope_chain or find_in_scope_chain) that accepts a starting ScopeId and
a closure/predicate to run against each Scope, then refactor
find_binding_in_scope to call that helper and refactor is_binding_imported to
use the same helper to check parent scopes; reference the existing symbols
find_binding_in_scope and is_binding_imported and ensure the helper returns
either the predicate result (Option<TextRange> for the binding case or bool for
imported check) or a common enum/Result that both callers can use.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c87805 and bd88aaf.

📒 Files selected for processing (1)
  • crates/biome_module_graph/src/js_module_info.rs

@ematipico ematipico force-pushed the refactor/use-one-semantic-model branch from 5609292 to 0f3e0a5 Compare February 25, 2026 22:18
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: 2

🧹 Nitpick comments (5)
crates/biome_service/src/workspace/server.rs (1)

1030-1033: Add a debug breadcrumb when JS graph updates are skipped.

Line 1031 silently returns defaults; a small debug log here would save future “why didn’t it index?” archaeology.

🔎 Tiny observability tweak
                     } else {
-                        // No semantic model available - return empty result
+                        tracing::debug!(
+                            path = %path,
+                            "Skipping JS module graph update: semantic model unavailable"
+                        );
                         Default::default()
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_service/src/workspace/server.rs` around lines 1030 - 1033, Add a
debug breadcrumb before the silent return in the else branch that currently
returns Default::default() so skipped JS graph updates are logged; locate the
branch in workspace/server.rs where the code returns Default::default() for "No
semantic model available" (the JS graph update path) and call the project's
logger (e.g., tracing::debug! or log::debug!) with a short message like
"Skipping JS graph update: no semantic model" and any contextual identifiers
(workspace id, file path, or request id) available in scope to aid future
debugging.
crates/biome_module_graph/src/js_module_info/collector.rs (2)

1135-1158: _semantic_model is a forward-declared parameter with no current use.

The method builds BindingTypeData purely from self.bindings and never touches _semantic_model. The _ prefix suppresses the warning, which is fine for now — but consider dropping the parameter until it's actually needed, to avoid misleading callers about what drives the output.

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

In `@crates/biome_module_graph/src/js_module_info/collector.rs` around lines 1135
- 1158, The build_binding_type_data method on JsModuleInfoCollector currently
takes an unused parameter _semantic_model; remove the unused parameter from the
function signature (change fn build_binding_type_data(&self, _semantic_model:
&biome_js_semantic::SemanticModel) -> ... to fn build_binding_type_data(&self)
-> ...) and update all callers to call build_binding_type_data() without the
extra argument; keep the body that maps self.bindings into BindingTypeData (ty,
jsdoc, export_ranges) unchanged so behavior is preserved.

577-599: Acknowledged interim duplication — TODO is clear, just ensure it's tracked.

infer_all_types rebuilds a Lapper from scope_range_by_start on every call while a SemanticModel (which already holds this data) is passed but ignored. The TODO comment is clear, but this means scope_range_by_start (and all its memory) is still being kept alive for each module even though the semantic model holds equivalent scope data.

Worth filing a follow-up issue to track the migration so it doesn't linger.

Want me to open a tracking issue for the TODO: Refactor type inference to use semantic_model's scopes directly?

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

In `@crates/biome_module_graph/src/js_module_info/collector.rs` around lines 577 -
599, infer_all_types currently rebuilds a Lapper from the collector's
scope_range_by_start each call even though a SemanticModel is passed in and
contains equivalent scope data; file a follow-up issue tracking the TODO and
update infer_all_types to use the scopes provided by the SemanticModel (use
semantic_model's scope data instead of scope_range_by_start), remove/stop
retaining collector-level scope_range_by_start where redundant, and update calls
that rely on scope_id_for_range to accept the semantic_model-backed Lapper or
lookup so the type inference (infer_type) uses the semantic model's scopes
directly.
crates/biome_module_graph/src/js_module_info/scope.rs (1)

14-27: pub range annotated dead — consider pub(crate) or removing the field for now.

A pub field with #[expect(dead_code)] is a contradiction: public to the world, but nothing reads it. If it's genuinely unused externally too, pub(crate) is a better fit until the "future scope analysis" materialises.

♻️ Suggested change
 pub struct JsScopeData {
-    #[expect(dead_code, reason = "May be used in future for scope analysis")]
-    pub range: TextRange,
+    pub(crate) range: TextRange,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/js_module_info/scope.rs` around lines 14 - 27,
The field JsScopeData::range is publicly exported but annotated
#[expect(dead_code)] — change its visibility to pub(crate) (or remove the field
if you prefer) to avoid exposing an unused public API; locate the struct
JsScopeData and update the range field's declaration (currently typed TextRange
and carrying the expect attribute) to use pub(crate) instead of pub, and remove
the expect(dead_code) attribute if you keep it as pub(crate).
crates/biome_module_graph/src/js_module_info.rs (1)

306-348: Duplicated scope-walking loop — extract a shared private helper.

find_binding_in_scope and is_binding_imported share the exact same loop structure: start at scope_from_id, call get_binding(name), fall back to parent(). The only difference is what they do with the Binding once found.

♻️ Suggested refactor
+    /// Looks up a binding by name starting from `scope_id` and walking up the scope chain.
+    fn lookup_binding_in_scope(
+        &self,
+        name: &str,
+        scope_id: ScopeId,
+    ) -> Option<biome_js_semantic::Binding> {
+        let mut scope = self.semantic_model.scope_from_id(scope_id);
+        loop {
+            if let Some(binding) = scope.get_binding(name) {
+                return Some(binding);
+            }
+            match scope.parent() {
+                Some(parent) => scope = parent,
+                None => return None,
+            }
+        }
+    }
+
     fn find_binding_in_scope(&self, name: &str, scope_id: ScopeId) -> Option<TextRange> {
-        let mut scope = self.semantic_model.scope_from_id(scope_id);
-        loop {
-            if let Some(binding) = scope.get_binding(name) {
-                return Some(binding.syntax().text_trimmed_range());
-            }
-            match scope.parent() {
-                Some(parent) => scope = parent,
-                None => break,
-            }
-        }
-        None
+        self.lookup_binding_in_scope(name, scope_id)
+            .map(|b| b.syntax().text_trimmed_range())
     }

     fn is_binding_imported(&self, name: &str, scope_id: ScopeId) -> bool {
-        let mut scope = self.semantic_model.scope_from_id(scope_id);
-        loop {
-            if let Some(binding) = scope.get_binding(name) {
-                return binding.is_imported();
-            }
-            match scope.parent() {
-                Some(parent) => scope = parent,
-                None => break,
-            }
-        }
-        false
+        self.lookup_binding_in_scope(name, scope_id)
+            .is_some_and(|b| b.is_imported())
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/js_module_info.rs` around lines 306 - 348, Both
find_binding_in_scope and is_binding_imported repeat the same scope-walking
loop; extract a private helper (e.g., walk_binding_in_scope or find_binding)
that takes (&self, name: &str, scope_id: ScopeId) and returns Option<Binding>
(or a reference/handle to the Binding) by starting from
self.semantic_model.scope_from_id(scope_id), calling get_binding(name) and
following parent() until None. Replace the loop in find_binding_in_scope to call
the helper and then return binding.syntax().text_trimmed_range() mapped into
Option<TextRange>, and replace the loop in is_binding_imported to call the
helper and return binding.is_imported() or false if None. Ensure the helper and
callers use the same Binding type and visibility (private) so behavior is
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_module_graph/benches/module_graph.rs`:
- Line 3: The benchmark constructs an incomplete model by calling
SemanticModelBuilder::new(root.clone()).build(); replace that direct builder
usage with the library helper that runs preorder traversal and event
extraction—call semantic_model(root.clone()) (importing semantic_model from
biome_js_semantic if needed) so the benchmark measures the fully constructed
semantic model instead of the incomplete build from
SemanticModelBuilder::new(...).build().

In `@crates/biome_module_graph/src/js_module_info/module_resolver.rs`:
- Around line 157-181: resolved_type_of_named_value currently ignores the
provided range and always uses module.0.semantic_model.global_scope() and a
hard-coded ScopeId(0); change it to lookup the scope at the given range (use the
semantic model's range-based scope lookup or find the node at range and derive
the scope id) instead of the global scope before calling find_binding_in_scope,
and when you retrieve binding_type_data use self.resolve_reference(&data.ty)
rather than only matching TypeReference::Resolved so import-based references are
correctly resolved; update references to find_binding_in_scope,
binding_type_data, and resolve_reference in resolved_type_of_named_value
accordingly.

---

Nitpick comments:
In `@crates/biome_module_graph/src/js_module_info.rs`:
- Around line 306-348: Both find_binding_in_scope and is_binding_imported repeat
the same scope-walking loop; extract a private helper (e.g.,
walk_binding_in_scope or find_binding) that takes (&self, name: &str, scope_id:
ScopeId) and returns Option<Binding> (or a reference/handle to the Binding) by
starting from self.semantic_model.scope_from_id(scope_id), calling
get_binding(name) and following parent() until None. Replace the loop in
find_binding_in_scope to call the helper and then return
binding.syntax().text_trimmed_range() mapped into Option<TextRange>, and replace
the loop in is_binding_imported to call the helper and return
binding.is_imported() or false if None. Ensure the helper and callers use the
same Binding type and visibility (private) so behavior is unchanged.

In `@crates/biome_module_graph/src/js_module_info/collector.rs`:
- Around line 1135-1158: The build_binding_type_data method on
JsModuleInfoCollector currently takes an unused parameter _semantic_model;
remove the unused parameter from the function signature (change fn
build_binding_type_data(&self, _semantic_model:
&biome_js_semantic::SemanticModel) -> ... to fn build_binding_type_data(&self)
-> ...) and update all callers to call build_binding_type_data() without the
extra argument; keep the body that maps self.bindings into BindingTypeData (ty,
jsdoc, export_ranges) unchanged so behavior is preserved.
- Around line 577-599: infer_all_types currently rebuilds a Lapper from the
collector's scope_range_by_start each call even though a SemanticModel is passed
in and contains equivalent scope data; file a follow-up issue tracking the TODO
and update infer_all_types to use the scopes provided by the SemanticModel (use
semantic_model's scope data instead of scope_range_by_start), remove/stop
retaining collector-level scope_range_by_start where redundant, and update calls
that rely on scope_id_for_range to accept the semantic_model-backed Lapper or
lookup so the type inference (infer_type) uses the semantic model's scopes
directly.

In `@crates/biome_module_graph/src/js_module_info/scope.rs`:
- Around line 14-27: The field JsScopeData::range is publicly exported but
annotated #[expect(dead_code)] — change its visibility to pub(crate) (or remove
the field if you prefer) to avoid exposing an unused public API; locate the
struct JsScopeData and update the range field's declaration (currently typed
TextRange and carrying the expect attribute) to use pub(crate) instead of pub,
and remove the expect(dead_code) attribute if you keep it as pub(crate).

In `@crates/biome_service/src/workspace/server.rs`:
- Around line 1030-1033: Add a debug breadcrumb before the silent return in the
else branch that currently returns Default::default() so skipped JS graph
updates are logged; locate the branch in workspace/server.rs where the code
returns Default::default() for "No semantic model available" (the JS graph
update path) and call the project's logger (e.g., tracing::debug! or
log::debug!) with a short message like "Skipping JS graph update: no semantic
model" and any contextual identifiers (workspace id, file path, or request id)
available in scope to aid future debugging.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd88aaf and 0f3e0a5.

⛔ Files ignored due to path filters (18)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_module_graph/tests/snapshots/test_export_default_function_declaration.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_export_referenced_function.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_export_type_referencing_imported_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_export_types.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_generic_return_value_with_multiple_modules.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_import_as_namespace.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_merged_types.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_multiple_reexports.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_nested_function_call_with_namespace_in_return_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_promise_export.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_imported_promise_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_promise_from_imported_function_returning_reexported_promise_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_single_reexport.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_type_of_this_in_class_export.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (20)
  • crates/biome_js_semantic/src/semantic_model/model.rs
  • crates/biome_js_type_info/Cargo.toml
  • crates/biome_js_type_info/src/type_data.rs
  • crates/biome_js_type_info/tests/local_inference.rs
  • crates/biome_js_type_info/tests/resolver.rs
  • crates/biome_module_graph/benches/module_graph.rs
  • crates/biome_module_graph/src/format_module_graph.rs
  • crates/biome_module_graph/src/js_module_info.rs
  • crates/biome_module_graph/src/js_module_info/binding.rs
  • crates/biome_module_graph/src/js_module_info/collector.rs
  • crates/biome_module_graph/src/js_module_info/module_resolver.rs
  • crates/biome_module_graph/src/js_module_info/scope.rs
  • crates/biome_module_graph/src/js_module_info/visitor.rs
  • crates/biome_module_graph/src/lib.rs
  • crates/biome_module_graph/src/module_graph.rs
  • crates/biome_module_graph/tests/snap/mod.rs
  • crates/biome_module_graph/tests/spec_tests.rs
  • crates/biome_service/src/workspace/server.rs
  • crates/biome_test_utils/Cargo.toml
  • crates/biome_test_utils/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • crates/biome_js_type_info/tests/resolver.rs
  • crates/biome_js_type_info/Cargo.toml
  • crates/biome_module_graph/tests/snap/mod.rs
  • crates/biome_module_graph/src/lib.rs
  • crates/biome_module_graph/src/js_module_info/visitor.rs
  • crates/biome_module_graph/tests/spec_tests.rs
  • crates/biome_js_semantic/src/semantic_model/model.rs

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_module_graph/src/js_module_info/module_resolver.rs`:
- Around line 139-144: The export default path drops import-backed bindings
because resolved_type_for_reference(&data.ty) only handles
TypeReference::Resolved and returns Type::default() for TypeReference::Import;
update the binding handling (JsOwnExport::Binding) to match on data.ty and
explicitly handle TypeReference::Import by resolving the import reference (e.g.,
call the import-resolution routine or a new helper like
resolved_type_for_import_reference) instead of falling back to default, or fix
resolved_type_for_reference to recognize and resolve TypeReference::Import into
the correct Type (instead of returning default); ensure you still fall back to
resolved_type_for_id(GLOBAL_UNKNOWN_ID) only when resolution truly fails.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f3e0a5 and c025e90.

📒 Files selected for processing (3)
  • crates/biome_js_semantic/src/semantic_model/scope.rs
  • crates/biome_module_graph/benches/module_graph.rs
  • crates/biome_module_graph/src/js_module_info/module_resolver.rs

@github-actions github-actions bot added the A-Linter Area: linter label Feb 26, 2026
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.

💅 noFloatingPromises doesn't report floating promises from imported functions

1 participant