refactor: use salsa#10315
Conversation
|
Merging this PR will degrade performance by 10.29%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR replaces the eager in-memory ModuleGraph with a Salsa-tracked ProjectDatabase (ModuleDb). It adds a new biome_db crate and workspace salsa dependency, implements ProjectDatabase and ModuleDb queries (symbols, JSDoc, CSS class traversal, import-tree builders), introduces PathInfoCache and resolver functions (resolve_js/css/html, ModuleInfoKind), and rewires analyzers, service wiring, go-to handlers, workspace server, test utilities and many tests to use module_db/ProjectDatabase. Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/biome_js_analyze/src/lint/correctness/no_unresolved_imports.rs (1)
137-145:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGate the unresolved-export check to JS modules only
find_js_exported_symbol()returnsNoneunless the target isModuleInfoKind::Js. The lint now passes anyModuleInfo(viactx.module_info_for_path(resolved_path)) intohas_exported_symbol(), so HTML/CSS-backed imports can get flagged as “has no export named …” / “no default export” even when they should be skipped.Add a
ModuleInfoKind::Jsgate before callingfind_js_exported_symbol(forcrates/biome_js_analyze/src/lint/correctness/no_unresolved_imports.rs, incl. thetarget_infowiring around 137-145 and thehas_exported_symbolpath around 239-275).🤖 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_analyze/src/lint/correctness/no_unresolved_imports.rs` around lines 137 - 145, The lint is calling has_exported_symbol/find_js_exported_symbol for any ModuleInfo; add a guard to only run export checks when target_info.kind is ModuleInfoKind::Js: after obtaining target_info via ctx.module_info_for_path(resolved_path) (and before constructing GetUnresolvedImportsOptions and before the has_exported_symbol path), check target_info.kind == ModuleInfoKind::Js and skip export-resolution logic (return Vec::new() or bypass has_exported_symbol) for non-JS kinds; ensure both the wiring around target_info (where GetUnresolvedImportsOptions is created) and the branch that calls has_exported_symbol/find_js_exported_symbol (the code between the existing lines ~239–275) are updated to short-circuit for non-JS ModuleInfoKind.
🤖 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_module_graph/README.md`:
- Line 3: Fix the grammar in the README sentence that currently reads "A module
a is a known file that imports and exports data, which is used" by removing the
extra "a" so it reads "A module is a known file that imports and exports data,
which is used"; update the sentence in the README.md (the line containing "A
module a is...") to the corrected wording.
In `@crates/biome_module_graph/src/css_module_info/traverse.rs`:
- Around line 105-143: The loop over importers currently breaks after the first
CSS-producing parent, which prevents pushing later sibling file_path entries
onto self.stack; change the logic in traverse.rs so you do not exit the loop
early: instead of break, continue iterating so all sibling importers get pushed,
and only set self.current_css_iter if it is not already set (guard with
self.current_css_iter.is_none()) to avoid overwriting a previously chosen
iterator; keep the existing pushes to self.visited and self.stack and the
css_steps creation but replace the unconditional break with a continue and a
conditional assignment for current_css_iter.
In `@crates/biome_module_graph/src/db/queries.rs`:
- Around line 392-397: The default-reexport branch (ImportSymbol::Default) must
be guarded by seen_paths just like the Named branch to avoid infinite loops:
when you resolve reexport.import.resolved_path.as_deref() and call
db.js_module_info_for_path(path) before doing stack.push((module, symbol_name)),
check whether seen_paths already contains that path and only push (and/or insert
into seen_paths) if it is not present; apply this exact same guard in both
functions that handle exported-symbol lookup and JSDoc lookup (the other
occurrence around the second ImportSymbol::Default handling) so default
re-export cycles are short-circuited.
- Around line 180-204: The code iterates html_info.static_import_paths twice
causing duplicate CssClassStep entries; update the second loop to only iterate
dynamic imports (or otherwise exclude static_import_paths) so each import is
processed once: modify the loop that currently chains
html_info.static_import_paths.values().chain(html_info.dynamic_import_paths.values())
to only iterate html_info.dynamic_import_paths.values() and keep the same
css_module_info_for_path lookup and linked_steps.push(CssClassStep { css_path:
..., css_classes: ... }) logic to avoid duplicated CSS steps.
In `@crates/biome_module_graph/src/path_info_cache.rs`:
- Around line 50-53: The loop in while let Some(dir) = parent currently calls
self.get_or_insert(dir, fs) and breaks if that returns Some, which treats
newly-inserted entries the same as pre-existing ones and stops prepopulating
ancestors; change the logic so you first check existence with self.get(dir) (or
an existence check) and only break when the directory already exists, otherwise
call self.get_or_insert(dir, fs) to insert and continue up the ancestor chain;
update get_or_insert only if needed (or have it return a flag) so the loop can
distinguish newly-inserted vs pre-existing entries.
In `@crates/biome_service/src/workspace/server.rs`:
- Around line 1435-1473: These methods silently ignore a poisoned DB mutex by
returning on lock error; instead panic immediately so callers don't operate on
stale state. Replace the pattern "let Ok(...)= self.lock_db() else { return; }"
in db_set_module_info, db_remove_module, and db_unload_path with a clear
unwrap/expect from lock_db() (e.g. let mut db =
self.lock_db().expect("WorkspaceServer db lock poisoned") for the mutable case)
so the code fails fast on poison rather than silently no-op.
---
Outside diff comments:
In `@crates/biome_js_analyze/src/lint/correctness/no_unresolved_imports.rs`:
- Around line 137-145: The lint is calling
has_exported_symbol/find_js_exported_symbol for any ModuleInfo; add a guard to
only run export checks when target_info.kind is ModuleInfoKind::Js: after
obtaining target_info via ctx.module_info_for_path(resolved_path) (and before
constructing GetUnresolvedImportsOptions and before the has_exported_symbol
path), check target_info.kind == ModuleInfoKind::Js and skip export-resolution
logic (return Vec::new() or bypass has_exported_symbol) for non-JS kinds; ensure
both the wiring around target_info (where GetUnresolvedImportsOptions is
created) and the branch that calls has_exported_symbol/find_js_exported_symbol
(the code between the existing lines ~239–275) are updated to short-circuit for
non-JS ModuleInfoKind.
🪄 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: e2ff9908-42b9-40e0-a735-ea16aa1703d3
⛔ Files ignored due to path filters (3)
Cargo.lockis excluded by!**/*.lockand included by**crates/biome_diagnostics_categories/src/categories.rsis excluded by!**/categories.rsand included by**packages/@biomejs/backend-jsonrpc/src/workspace.tsis excluded by!**/backend-jsonrpc/src/workspace.tsand included by**
📒 Files selected for processing (64)
Cargo.tomlcrates/biome_css_analyze/src/lib.rscrates/biome_css_analyze/src/lint/nursery/no_unused_classes.rscrates/biome_css_analyze/src/services/module_graph.rscrates/biome_css_analyze/tests/spec_tests.rscrates/biome_db/Cargo.tomlcrates/biome_db/src/lib.rscrates/biome_html_analyze/src/lib.rscrates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rscrates/biome_html_analyze/src/services/module_graph.rscrates/biome_html_analyze/tests/spec_tests.rscrates/biome_js_analyze/src/lib.rscrates/biome_js_analyze/src/lint/correctness/no_private_imports.rscrates/biome_js_analyze/src/lint/correctness/no_unresolved_imports.rscrates/biome_js_analyze/src/lint/correctness/use_import_extensions.rscrates/biome_js_analyze/src/lint/correctness/use_json_import_attributes.rscrates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rscrates/biome_js_analyze/src/lint/suspicious/no_deprecated_imports.rscrates/biome_js_analyze/src/lint/suspicious/no_import_cycles.rscrates/biome_js_analyze/src/services/database.rscrates/biome_js_analyze/src/services/mod.rscrates/biome_js_analyze/src/suppressions.tests.rscrates/biome_js_analyze/tests/spec_tests.rscrates/biome_js_type_info/src/type.rscrates/biome_lsp/src/session.rscrates/biome_module_graph/Cargo.tomlcrates/biome_module_graph/README.mdcrates/biome_module_graph/benches/module_graph.rscrates/biome_module_graph/src/css_module_info/traverse.rscrates/biome_module_graph/src/db/mod.rscrates/biome_module_graph/src/db/project_database.rscrates/biome_module_graph/src/db/queries.rscrates/biome_module_graph/src/format_module_graph.rscrates/biome_module_graph/src/js_module_info.rscrates/biome_module_graph/src/js_module_info/module_resolver.rscrates/biome_module_graph/src/lib.rscrates/biome_module_graph/src/module_graph.rscrates/biome_module_graph/src/module_graph/fs_proxy.rscrates/biome_module_graph/src/path_info_cache.rscrates/biome_module_graph/tests/snap/mod.rscrates/biome_module_graph/tests/spec_tests.rscrates/biome_resolver/Cargo.tomlcrates/biome_resolver/src/db/inputs.rscrates/biome_resolver/src/db/mod.rscrates/biome_resolver/src/lib.rscrates/biome_ruledoc_utils/Cargo.tomlcrates/biome_ruledoc_utils/src/lib.rscrates/biome_service/Cargo.tomlcrates/biome_service/src/diagnostics.rscrates/biome_service/src/file_handlers/css.rscrates/biome_service/src/file_handlers/css/go_to.rscrates/biome_service/src/file_handlers/graphql.rscrates/biome_service/src/file_handlers/html.rscrates/biome_service/src/file_handlers/html/go_to.rscrates/biome_service/src/file_handlers/javascript.rscrates/biome_service/src/file_handlers/javascript/go_to.rscrates/biome_service/src/file_handlers/json.rscrates/biome_service/src/file_handlers/mod.rscrates/biome_service/src/workspace.rscrates/biome_service/src/workspace/db.rscrates/biome_service/src/workspace/server.rscrates/biome_service/src/workspace/server.tests.rscrates/biome_test_utils/Cargo.tomlcrates/biome_test_utils/src/lib.rs
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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_css_analyze/src/lint/nursery/no_unused_classes.rs`:
- Around line 78-85: The new DB-backed lookup path using ctx.db(),
module_for_path(...), is_class_referenced_by_importers(...) and SymbolName built
from class_name.token_text_trimmed().text() needs corresponding spec fixtures:
add explicit valid and invalid test cases for the noUnusedClasses rule that
exercise importer-resolution (a class referenced only via an importer should be
marked valid) and the :global(...) exclusion (classes inside :global(...) should
be treated as used/valid). Create fixtures that construct SymbolName via the
same token text shape and include a negative case where a class is truly unused
to assert the rule flags it.
🪄 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: 6200b86d-d673-486d-a02a-623878d7d672
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lockand included by**
📒 Files selected for processing (5)
Cargo.tomlcrates/biome_css_analyze/src/lib.rscrates/biome_css_analyze/src/lint/nursery/no_unused_classes.rscrates/biome_css_analyze/src/services/module_graph.rscrates/biome_css_analyze/tests/spec_tests.rs
I don't know enough about how Biome works today, but wrapping a Db in a The only thing that's worth keeping in mind is that Salsa cancels all pending queries running on another thread when mutating an input, by panicking. You can use |
| /// | ||
| /// Tracked: depends on `js_module_info(db, module)` and on the CSS modules it | ||
| /// imports. If any of those change, this recomputes. | ||
| #[salsa::tracked(no_eq)] |
There was a problem hiding this comment.
You probably want to use returns(ref) or returns(deref) here or salsa will clone the Vec every time this function is called (over returning a slice to the cached data).
| module: ModuleInfo, | ||
| symbol_name: SymbolName<'db>, |
There was a problem hiding this comment.
If ModuleInfo and SymbolName is a common combination, it might make sense to intern the data separately.
Technically, salsa only supports function with two arguments: db + one salsa struct. Functions with more than those two arguments are "lowered" into the two argument form by creating an ad-hoc interned struct that contains all argument values. Now, if you have 5 functions that take the same argument pairs, then Salsa interns those values 5 times. If you create one shared interned struct, then you only intern the value once
There was a problem hiding this comment.
Do you mean I should create a salsa::interned that contains both? ModuleInfo and the symbol?
There was a problem hiding this comment.
Yes, but only if it's common that you call different queries with the same ModuleInfo and SymbolName.
(What salsa does internally is that it creates a salsa::interned fore each query that takes both ModuleInfo and SymbolName, but it's a different interned struct for each query, and, therefore, salsa ends up interning the same ModuleInfo/SymbolName multiple times, once for each query that's called with the same pair. Using your own struct prevents this
Thank you for the suggestions @MichaReiser ! They are really appreciated. I am still finding the correct architecture and also learning how salsa works. The reason why the database is under a At the moment, the CLI sends the |

Summary
This is the first PR of a refactor we want to do in core. The objective is to introduce salsa.
Salsa is a framework (not a library), so we will see more and more of the workspace architecture to better fit how Salsa works.
The intention to bring Salsa into Biome is old, but years ago the framework was in a weird status, so we couldn't adopt it. A few months ago, Astral took it under their wing, and they use Salsa to power up their type-check system for Python. Salsa is also used by rust-analyzer (it was, at least). The new Salsa is battle-tested and maintained by a group of folks with an amazing reputation.
Unfortunately, the Salsa book (their docs) isn't up to date, but many concepts stayed the same. Micha, one of the current maintainers, shared this guide with me: https://hackmd.io/5ddaZLKYSVC_YTTD3SzRDw?view
The PR is semi-vibe-coded. I used a coding agent to grab as much information as possible, and I iterated the plan 4 or 5 times. Then I changed almost all of the code that it created.
How salsa works today, in Biome
I don't have all the knowledge, but the core concepts are relatively simple
We have Salsa inputs that are tagged with
#[salsa::input]. These structs change outside the database. In this PR,ModuleInfois a type that changes outside the database, and the workspace is responsible for storing it.In our design, the Workspace is responsible for storing and evicting data. When we need to index a module, we eventually create the
ModuleInfotype using salsa setters. The setter methods are automatically created via#[salsa::input]. We need to abide by them.Salsa doesn't enforce what to store in the database, but it enforces that we store data that has
HashandPartialEq. This isn't a problem for us.The database inside the Workspace uses a
Mutex. We need to lock it when we update it i.e. it's locked when we create aModuleInfoinstance, or we update an existing one.How the database is defined
The lowest piece of the database is
biome_db. It defines one singleDbtrait that will be used throughout the application by those data structures that need a database to query.The intermediate pieces are defined by those crates that need to query the database. In this PR, the module graph. We define a new trait. This is necessary because of the Rust orphan rule.
In this PR, we have the
ModuleDb, which defines some methods that must be implemented by the final implementors.The highest piece, and the final implementer of the traits, is the
WorkspaceServer.If in the future we want to add semantic data (most likely) to the database, we will implement a
SemanticDbtrait, and create a physical database in charge of updating the data.One database VS Multiple databases
I don't have the answer, and we will figure it out as we go.
As for now, we have a single database for all projects. The likelihood that a project queries a path that belongs to another project is basically null, because we have the
ProjectLayout, but we can evaluate the existence of a database per project.Changes to the module graph
The module graph is now a thin layer over Salsa. Whenever we want to query the database, we use a query, which is tagged with
#[salsa::tracked]. Queries must accept a database reference and a Salsa type.All existing functions have been moved into
queries.rs, and they are tracked. I had to make some changes because many of these functions were using paths as inputs.Pros and Cons
In addition to fitting our architecture to the framework's requirements, salsa comes with its own runtime. This runtime has a performance penalty, especially for "cold" executions such as the CLI, because all data is created once and read multiple times.
However, for the Language Server (editors), the story is completely different. Nowadays, every change to a document (keystroke), triggers:
We're lucky that we use a language that is fast; however, this already has some repercussions in terms of performance and memory usage.
Test Plan
Green CI.
I manually tested the build in the editor to make sure everything still works.
This is also an internal refactor, so existing things should stay as is, and the changeset isn't needed.
Docs
Not needed.