Skip to content

refactor(core): semantic model in collector#9905

Merged
ematipico merged 5 commits intomainfrom
refactor/better-semantic-model
Apr 13, 2026
Merged

refactor(core): semantic model in collector#9905
ematipico merged 5 commits intomainfrom
refactor/better-semantic-model

Conversation

@ematipico
Copy link
Copy Markdown
Member

Summary

This PR finally finishes the refactor of using the semantic model inside our module graph.

There were still some parts that were ad hoc, and with this PR, now we use the semantic model for everything.

  • JSDoc have been moved to the semantic model
  • Export ranges have been moved to the semantic model
  • Some snapshots were updated, but that's expected because we moved data.

Test Plan

Updated the snapshots, updated unit tests.
Existing tests should still pass (module graph and JS rules)

Docs

N/A
Changeset not needed, it's an internal refactor.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 10, 2026

⚠️ No Changeset found

Latest commit: fc66eb5

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

@ematipico ematipico requested review from a team April 10, 2026 15:57
@github-actions github-actions Bot added A-Project Area: project L-JavaScript Language: JavaScript and super languages labels Apr 10, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 10, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 196 skipped benchmarks1


Comparing refactor/better-semantic-model (fc66eb5) with main (27dd7b1)

Open in CodSpeed

Footnotes

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

@github-actions
Copy link
Copy Markdown
Contributor

Parser conformance results on

js/262

Test result main count This PR count Difference
Total 53178 53178 0
Passed 51958 51958 0
Failed 1178 1178 0
Panics 42 42 0
Coverage 97.71% 97.71% 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 5467 5467 0
Passed 1915 1915 0
Failed 3552 3552 0
Panics 0 0 0
Coverage 35.03% 35.03% 0.00%

ts/babel

Test result main count This PR count Difference
Total 640 640 0
Passed 569 569 0
Failed 71 71 0
Panics 0 0 0
Coverage 88.91% 88.91% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18876 18876 0
Passed 13014 13014 0
Failed 5861 5861 0
Panics 1 1 0
Coverage 68.94% 68.94% 0.00%

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 966596eb-51a7-4b46-b538-9470c63d94df

📥 Commits

Reviewing files that changed from the base of the PR and between 07eb428 and fc66eb5.

⛔ Files ignored due to path filters (1)
  • crates/biome_service/src/snapshots/biome_service__workspace__tests__debug_semantic_model.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (1)
  • crates/biome_js_semantic/src/semantic_model/binding.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_semantic/src/semantic_model/binding.rs

Walkthrough

Export bookkeeping was changed from start positions to full TextRange values and JSDoc comments were integrated into the semantic layer. Bindings now store export ranges and optional JsdocComment; SemanticModelBuilder records export-node JSDoc by range and SemanticModel exposes range-based binding lookups and export_jsdoc. The module-graph and its collector were simplified to consume an Arc (removing traversal/event plumbing) and moved export/JSDoc responsibilities into the semantic model. JsdocComment representation and its formatter were refactored; formatting/tests adjusted to the new APIs.

Suggested reviewers

  • dyc3
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarises the main change: refactoring the semantic model integration into the collector module.
Description check ✅ Passed The description relates to the changeset, explaining the refactor's goals and noting that JSDoc and export ranges have been moved to the semantic model.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/better-semantic-model

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

Copy link
Copy Markdown
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: 3

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/visitor.rs (1)

301-337: ⚠️ Potential issue | 🟠 Major

Carry the export range for named re-exports.

Line 336 hard-codes None, so /** ... */ export { Foo as Bar } from "./mod" loses its statement JSDoc after this refactor. Downstream there is nothing left for the semantic-model lookup to resolve.

💡 Suggested patch
 fn visit_export_named_from_clause(
     &self,
     node: JsExportNamedFromClause,
     collector: &mut JsModuleInfoCollector,
 ) -> Option<()> {
+    let export_range = node.syntax().parent().map(|parent| parent.text_trimmed_range());
     let module_source = node.source().ok()?;
     let import_specifier = module_source
         .as_js_module_source()?
         .inner_string_text()
         .ok()?;
@@
             collector.register_export(JsCollectedExport::Reexport {
                 export_name,
                 reexport: JsReexport {
                     import: JsImport {
                         specifier: import_specifier.clone().into(),
                         resolved_path: resolved_path.clone(),
                         symbol: ImportSymbol::Named(imported_name),
                     },
-                    export_range: None,
+                    export_range,
                 },
             });
         }
🤖 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/visitor.rs` around lines 301 -
337, The re-export registration drops the export's source range by passing
export_range: None in visit_export_named_from_clause, which loses statement
JSDoc for named re-exports; fix it by computing the export range and passing it
into JsCollectedExport::Reexport. Locate visit_export_named_from_clause and
where collector.register_export(...) builds JsReexport, derive an export_range
(prefer the specifier's export_as() range if present, otherwise fall back to the
node's overall range/export range) and set export_range: Some(<that range>)
instead of None so downstream semantic-model lookups can resolve the JSDoc.
🤖 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_js_semantic/src/semantic_model/builder.rs`:
- Around line 405-415: The current find_jsdoc walks all ancestors and can pick
up an outer declaration/export JSDoc for an inner binding; change the traversal
so it stops at the binding’s own declaration boundary: when iterating ancestors
in find_jsdoc, if you encounter an AnyJsDeclaration that is an ancestor of the
node, attempt to return its JsdocComment
(JsdocComment::try_from(decl.syntax()).ok()) and then stop (do not continue to
outer ancestors); keep the existing JsExport handling but ensure you only return
export comments if encountered before hitting that first declaration boundary.
This ensures inner bindings don’t inherit JSDoc from outer declarations while
still allowing direct-declaration or preceding-export comments to be returned.

In `@crates/biome_module_graph/src/format_module_graph.rs`:
- Around line 291-294: The JsReexport.export_range metadata is currently ignored
(left behind the TODO) so dumps lose that info; update the formatter in
format_module_graph.rs (the Display/fmt implementation that currently comments
out jsdoc_comment) to include export_range when formatting JsReexport nodes (or
alternatively add an explicit assertion that export_range is present elsewhere),
referencing the JsReexport type and the export_range field so the range is
either printed in the module-graph output or pinned by an assertion to prevent
it from silently disappearing.

In `@crates/biome_module_graph/src/module_graph.rs`:
- Around line 255-265: The JsOwnExport::Binding branch currently only uses
module.semantic_model.as_binding_by_range(...).and_then(|binding|
binding.jsdoc()), which misses export-site comments and docs for re-exported or
imported bindings; change it to first check binding.export_ranges() and for each
export range call module.semantic_model.export_jsdoc(range).cloned() (returning
the first Some), then if none found and the binding is an import follow the
import through the module graph to the source module and attempt to read its
export-site JSDoc there, and finally fall back to binding.jsdoc(); update the
logic in the JsOwnExport::Binding arm (and reuse
module.semantic_model.as_binding_by_range, binding.export_ranges(), and
module.semantic_model.export_jsdoc(...) helpers) so re-exports and imported
default bindings surface their export comments.

---

Outside diff comments:
In `@crates/biome_module_graph/src/js_module_info/visitor.rs`:
- Around line 301-337: The re-export registration drops the export's source
range by passing export_range: None in visit_export_named_from_clause, which
loses statement JSDoc for named re-exports; fix it by computing the export range
and passing it into JsCollectedExport::Reexport. Locate
visit_export_named_from_clause and where collector.register_export(...) builds
JsReexport, derive an export_range (prefer the specifier's export_as() range if
present, otherwise fall back to the node's overall range/export range) and set
export_range: Some(<that range>) instead of None so downstream semantic-model
lookups can resolve the JSDoc.
🪄 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: 86bece2b-13ee-4de2-be1a-1d12925c2755

📥 Commits

Reviewing files that changed from the base of the PR and between 1c8e1ef and f8e895b.

⛔ Files ignored due to path filters (20)
  • 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_default_imported_binding.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_namespace_reexport_type_inference.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 (14)
  • crates/biome_js_semantic/Cargo.toml
  • crates/biome_js_semantic/src/semantic_model/binding.rs
  • crates/biome_js_semantic/src/semantic_model/builder.rs
  • crates/biome_js_semantic/src/semantic_model/model.rs
  • crates/biome_jsdoc_comment/Cargo.toml
  • crates/biome_jsdoc_comment/src/format_jsdoc_comment.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/scope.rs
  • crates/biome_module_graph/src/js_module_info/visitor.rs
  • crates/biome_module_graph/src/module_graph.rs
  • crates/biome_module_graph/tests/spec_tests.rs

Comment thread crates/biome_js_semantic/src/semantic_model/builder.rs
Comment thread crates/biome_module_graph/src/format_module_graph.rs Outdated
Comment thread crates/biome_module_graph/src/module_graph.rs
Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

Kind of a rubber stamp, I'm not entirely up to speed in this area of the codebase.

Comment on lines 40 to 109
@@ -106,7 +105,6 @@ Imports {
```
PromisedResult => BindingTypeData {
Types Module(0) TypeId(4),
Exported Ranges: (12..26)
}
```
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.

regressions?

Copy link
Copy Markdown
Member Author

@ematipico ematipico Apr 11, 2026

Choose a reason for hiding this comment

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

No, it's correct. I mentioned in the description

Export ranges have been moved to the semantic model

I updated the printing of the semantic model and added a few snapshots.

Comment thread crates/biome_module_graph/tests/snapshots/test_resolve_exports.snap
@ematipico ematipico force-pushed the refactor/better-semantic-model branch from 6f553e0 to 07eb428 Compare April 11, 2026 17:30
Copy link
Copy Markdown
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_jsdoc_comment/src/jsdoc_comment.rs (1)

123-134: Please add one focused test for range preservation.

You’re now persisting trivia range, but tests only assert comment detection/collection text. A small assertion that TryFrom<JsSyntaxToken> preserves the expected TextRange would lock this down.

Suggested test shape
 #[cfg(test)]
 mod tests {
     use super::*;
     use biome_js_parser::{JsParserOptions, parse};
     use biome_js_syntax::JsFileSource;
+    use biome_rowan::AstNode;

+    #[test]
+    fn test_try_from_token_preserves_jsdoc_range() {
+        let source = "/** docs */\nconst a = 1;";
+        let root = parse(source, JsFileSource::tsx(), JsParserOptions::default()).syntax();
+        let token = root.first_token().expect("expected first token");
+        let jsdoc = JsdocComment::try_from(token).expect("expected jsdoc");
+        assert_eq!(jsdoc.range, TextRange::new(0.into(), 11.into()));
+    }

As per coding guidelines, "All code changes MUST include appropriate tests: ... and bug fixes require tests that reproduce and validate the fix."

Also applies to: 141-233

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

In `@crates/biome_jsdoc_comment/src/jsdoc_comment.rs` around lines 123 - 134, Add
a focused unit test that constructs a JsSyntaxToken containing a JSDoc trivia
and asserts that TryFrom<JsSyntaxToken> for JsdocComment returns a JsdocComment
whose stored TextRange equals the trivia's original TextRange; specifically,
create a token with known leading_trivia (using a single/multi-line comment
text), call JsdocComment::try_from(token) (or TryFrom::try_from), and assert the
returned JsdocComment.range() (or the field persisted by Self::new) matches the
trivia.text_range() to lock down range preservation for text_is_jsdoc_comment
and JsdocComment::new.
🤖 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_js_semantic/src/semantic_model/binding.rs`:
- Around line 602-605: The current exports() iterator panics because it indexes
self.data.binding_node_by_start with positions from binding.export_ranges that
may be absent; change the logic to use
self.data.binding_node_by_start.get(&export_start.start()) and a filter_map to
skip missing entries, e.g. iterate
binding.export_ranges.iter().filter_map(|export_start|
self.data.binding_node_by_start.get(&export_start.start()).map(|node|
node.to_node(self.data.to_root().syntax()))), so missing declaration positions
are handled defensively without panicking.

---

Nitpick comments:
In `@crates/biome_jsdoc_comment/src/jsdoc_comment.rs`:
- Around line 123-134: Add a focused unit test that constructs a JsSyntaxToken
containing a JSDoc trivia and asserts that TryFrom<JsSyntaxToken> for
JsdocComment returns a JsdocComment whose stored TextRange equals the trivia's
original TextRange; specifically, create a token with known leading_trivia
(using a single/multi-line comment text), call JsdocComment::try_from(token) (or
TryFrom::try_from), and assert the returned JsdocComment.range() (or the field
persisted by Self::new) matches the trivia.text_range() to lock down range
preservation for text_is_jsdoc_comment and JsdocComment::new.
🪄 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: 9f68f81f-d4c8-42df-9af1-25220d228a12

📥 Commits

Reviewing files that changed from the base of the PR and between f8e895b and 6f553e0.

⛔ Files ignored due to path filters (5)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_js_semantic/src/snapshots/format_js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_semantic/src/snapshots/format_ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_namespace_reexport_type_inference.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 **
📒 Files selected for processing (8)
  • crates/biome_js_analyze/src/lint/correctness/no_unused_imports.rs
  • crates/biome_js_semantic/Cargo.toml
  • crates/biome_js_semantic/src/format_semantic_model.rs
  • crates/biome_js_semantic/src/semantic_model/binding.rs
  • crates/biome_js_semantic/src/tests/format.rs
  • crates/biome_js_semantic/src/tests/mod.rs
  • crates/biome_jsdoc_comment/src/jsdoc_comment.rs
  • crates/biome_module_graph/src/format_module_graph.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/biome_js_semantic/src/tests/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/biome_js_semantic/Cargo.toml
  • crates/biome_module_graph/src/format_module_graph.rs

Comment thread crates/biome_js_semantic/src/semantic_model/binding.rs Outdated
@ematipico ematipico merged commit bcd6508 into main Apr 13, 2026
36 checks passed
@ematipico ematipico deleted the refactor/better-semantic-model branch April 13, 2026 11:28
@arendjr
Copy link
Copy Markdown
Contributor

arendjr commented Apr 15, 2026

Thanks for this! Looks pretty sweet :)

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 L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants