diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index 129e4e0539a333..4cce465829f3fe 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -33,6 +33,15 @@ pub trait Db: SemanticDb { #[salsa::db] #[derive(Clone)] pub struct ProjectDatabase { + // This handle must remain stable for the lifetime of the database. + // + // Many tracked queries branch on the untracked `db.project()` read before + // consulting tracked `Project` fields. Replacing the handle during reload + // therefore changes query behavior outside salsa's dependency graph and can + // trigger stale results. + // + // Structural reloads must update the existing `Project` in place via salsa + // setters instead of swapping in a freshly constructed handle. project: Option, files: Files, diff --git a/crates/ty_project/src/db/changes.rs b/crates/ty_project/src/db/changes.rs index 3b1a3907255477..10c9fd2bbeb311 100644 --- a/crates/ty_project/src/db/changes.rs +++ b/crates/ty_project/src/db/changes.rs @@ -1,7 +1,7 @@ +use crate::ProjectMetadata; use crate::db::{Db, ProjectDatabase}; use crate::metadata::options::ProjectOptionsOverrides; use crate::watch::{ChangeEvent, CreatedKind, DeletedKind}; -use crate::{Project, ProjectMetadata}; use std::collections::BTreeSet; use crate::walk::ProjectFilesWalker; @@ -38,7 +38,7 @@ impl ProjectDatabase { changes: Vec, project_options_overrides: Option<&ProjectOptionsOverrides>, ) -> ChangeResult { - let mut project = self.project(); + let project = self.project(); let project_root = project.root(self).to_path_buf(); let config_file_override = project_options_overrides.and_then(|options| options.config_file_override.clone()); @@ -59,11 +59,12 @@ impl ProjectDatabase { let mut sync_recursively = BTreeSet::default(); for change in changes { - tracing::trace!("Handle change: {:?}", change); + tracing::debug!("Handling file watcher change event: {:?}", change); if let Some(path) = change.system_path() { if let Some(config_file) = &config_file_override { if config_file.as_path() == path { + File::sync_path(self, path); result.project_changed = true; continue; @@ -74,6 +75,7 @@ impl ProjectDatabase { path.file_name(), Some(".gitignore" | ".ignore" | "ty.toml" | "pyproject.toml") ) { + File::sync_path(self, path); // Changes to ignore files or settings can change the project structure or add/remove files. result.project_changed = true; @@ -279,28 +281,8 @@ impl ProjectDatabase { } } - if metadata.root() == project.root(self) { - tracing::debug!("Reloading project after structural change"); - project.reload(self, metadata); - } else { - match Project::from_metadata(self, metadata, &FallibleStrategy) { - Ok(new_project) => { - tracing::debug!("Replace project after structural change"); - project = new_project; - } - Err(error) => { - tracing::error!( - "Keeping old project configuration because loading the new settings failed with: {error}" - ); - - project - .set_settings_diagnostics(self) - .to(vec![error.into_diagnostic()]); - } - } - - self.project = Some(project); - } + tracing::debug!("Reloading project after structural change"); + project.reload(self, metadata); } Err(error) => { tracing::error!( diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index 25bfaf8f44bd8d..d62af21b6c569f 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -184,22 +184,26 @@ impl Project { .options() .to_settings(db, metadata.root(), strategy)?; - // This adds a file root for the project itself. This enables - // tracking of when changes are made to the files in a project - // at the directory level. At time of writing (2025-07-17), - // this is used for caching completions for submodules. - db.files() - .try_add_root(db, metadata.root(), FileRootKind::Project); - let project = Project::builder(Box::new(metadata), Box::new(settings), diagnostics) .durability(Durability::MEDIUM) .open_fileset_durability(Durability::LOW) .file_set_durability(Durability::LOW) .new(db); + project.try_add_file_root(db); + Ok(project) } + fn try_add_file_root(self, db: &dyn Db) { + // This adds a file root for the project itself. This enables + // tracking of when changes are made to the files in a project + // at the directory level. At time of writing (2025-07-17), + // this is used for caching completions for submodules. + db.files() + .try_add_root(db, self.root(db), FileRootKind::Project); + } + pub fn root(self, db: &dyn Db) -> &SystemPath { self.metadata(db).root() } @@ -242,32 +246,37 @@ impl Project { pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) { tracing::debug!("Reloading project"); - assert_eq!(self.root(db), metadata.root()); - if &metadata != self.metadata(db) { - match metadata - .options() - .to_settings(db, metadata.root(), &FallibleStrategy) - { - Ok((settings, settings_diagnostics)) => { - if self.settings(db) != &settings { - self.set_settings(db).to(Box::new(settings)); - } + self.reload_files(db); - if self.settings_diagnostics(db) != settings_diagnostics { - self.set_settings_diagnostics(db).to(settings_diagnostics); - } + if &metadata == self.metadata(db) { + return; + } + + match metadata + .options() + .to_settings(db, metadata.root(), &FallibleStrategy) + { + Ok((settings, settings_diagnostics)) => { + if self.settings(db) != &settings { + self.set_settings(db).to(Box::new(settings)); } - Err(error) => { - self.set_settings_diagnostics(db) - .to(vec![error.into_diagnostic()]); + + if self.settings_diagnostics(db) != settings_diagnostics { + self.set_settings_diagnostics(db).to(settings_diagnostics); } } - - self.set_metadata(db).to(Box::new(metadata)); + Err(error) => { + tracing::warn!( + "Keeping old project configuration because loading the new settings failed with: {error}" + ); + self.set_settings_diagnostics(db) + .to(vec![error.into_diagnostic()]); + } } - self.reload_files(db); + self.set_metadata(db).to(Box::new(metadata)); + self.try_add_file_root(db); } /// Checks the project and its dependencies according to the project's check mode.