From aa5b04ad924393205b99db05ed59ab5241d5e8d2 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 4 Aug 2026 08:51:18 +0200 Subject: [PATCH] [ty] Support different type inference settings in a single Salsa DB (#27152) ## Summary This is the last PR that adds support for type checking multiple programs with different Python Version, search paths, and platform using a single Salsa DB. This PR changes `Program` from a singleton input to an interned struct. `ProgramFile` changes from `(File, ResolverEnvironment)` to `(File, Program)`. Changing `Program` from an input to an interned means that it now has a `'db` lifetime. This requires changing how we construct and store `Program`. That's why some of the db initialization code changes. I doubt that it will be noticeable. But this should speed up mdtests that contain tests with different Python versions (e.g. Py312, Py311, Py312, Py311), because we now reuse parsed modules, and even type inference results because the inference is now keyed by Program (instead of updating an existing Program) ## Memory regression I had codex look into the CodSpeed reported memory regression. The reason is that `Program` now being an interned and being initialized later changes the assigned `File` ids, which in turn changes in which order we iterate over the project files. And this change the max peak memory usage. It can also change the usage reported on the memory usage report (we enter fixpoint cycles from different heads, etc). The changes in the memory report are mainly from the new `ProgramFile::python_version` (needed to mitigate a larger regression, but we can reconsider), and that `Project` now stores `ProgramSettings`. --- Cargo.lock | 1 - crates/ruff_benchmark/Cargo.toml | 4 +- .../benches/module_resolution.rs | 15 +- crates/ty/tests/file_watching.rs | 3 +- crates/ty_ide/src/code_action.rs | 2 - crates/ty_ide/src/inlay_hints.rs | 2 - crates/ty_ide/src/lib.rs | 22 +-- crates/ty_ide/src/semantic_tokens.rs | 2 - crates/ty_ide/src/symbols.rs | 3 +- crates/ty_ide/src/workspace_symbols.rs | 1 - crates/ty_module_resolver/src/resolve.rs | 5 +- crates/ty_project/src/db.rs | 137 ++++++++---------- crates/ty_project/src/db/changes.rs | 35 +++-- crates/ty_project/src/lib.rs | 84 +++++------ crates/ty_project/src/metadata/options.rs | 2 +- .../ty_project/src/watch/project_watcher.rs | 3 +- crates/ty_python_core/Cargo.toml | 1 + crates/ty_python_core/src/builder.rs | 4 +- crates/ty_python_core/src/db.rs | 62 ++++++-- crates/ty_python_core/src/lib.rs | 25 ++-- crates/ty_python_core/src/platform.rs | 2 +- crates/ty_python_core/src/program.rs | 109 ++++---------- crates/ty_python_core/src/program_file.rs | 31 ++-- crates/ty_python_semantic/Cargo.toml | 2 +- crates/ty_python_semantic/src/db.rs | 45 +++--- .../ty_python_semantic/src/diagnostic/mod.rs | 9 +- crates/ty_python_semantic/src/lib.rs | 7 +- crates/ty_python_semantic/src/place.rs | 10 +- .../src/types/class/known.rs | 47 +++--- .../ty_python_semantic/src/types/context.rs | 4 +- .../src/types/diagnostic.rs | 7 +- .../src/types/infer/builder.rs | 3 +- .../src/types/infer/builder/imports.rs | 5 +- .../types/infer/builder/type_expression.rs | 3 +- .../src/types/infer/tests.rs | 83 ++++++++++- crates/ty_python_semantic/src/types/tests.rs | 23 ++- crates/ty_python_semantic/tests/corpus.rs | 44 +++--- .../server/api/requests/execute_command.rs | 3 +- crates/ty_server/src/session.rs | 4 +- crates/ty_server/tests/e2e/commands.rs | 17 ++- .../e2e__commands__debug_command.snap | 8 +- crates/ty_site_packages/src/version.rs | 6 +- crates/ty_test/src/db.rs | 29 +++- crates/ty_test/src/lib.rs | 7 +- crates/ty_wasm/src/lib.rs | 28 ++-- fuzz/Cargo.lock | 133 +++++++++-------- fuzz/Cargo.toml | 2 +- fuzz/fuzz_targets/ty_check_invalid_syntax.rs | 64 ++++---- 48 files changed, 617 insertions(+), 531 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65928cd0b5..fdc535485a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3163,7 +3163,6 @@ dependencies = [ "tracing", "ty_module_resolver", "ty_project", - "ty_python_core", ] [[package]] diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index 62cc4818a5..cf1a0fafd1 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -23,7 +23,6 @@ ruff_python_formatter = { workspace = true, optional = true } ruff_python_parser = { workspace = true, optional = true } ruff_python_trivia = { workspace = true, optional = true } ty_module_resolver = { workspace = true, optional = true } -ty_python_core = { workspace = true, optional = true } ty_project = { workspace = true, optional = true } anyhow = { workspace = true } @@ -61,7 +60,7 @@ ruff_instrumented = [ # Enables the ty instrumented benchmarks ty_instrumented = ["criterion", "ty_project", "ruff_python_trivia"] # Enables the module-resolution benchmark -module_resolution = ["divan", "ty_module_resolver", "ty_project", "ty_python_core"] +module_resolution = ["divan", "ty_module_resolver", "ty_project"] codspeed = ["codspeed-criterion-compat"] # Enables the ty_walltime benchmarks ty_walltime = ["ruff_db/os", "ty_project", "divan"] @@ -114,7 +113,6 @@ ignored = [ "ruff_python_parser", "ruff_python_trivia", "ty_module_resolver", - "ty_python_core", "mimalloc", "tikv-jemallocator" ] diff --git a/crates/ruff_benchmark/benches/module_resolution.rs b/crates/ruff_benchmark/benches/module_resolution.rs index 61bb8ba378..7f7c23f3c9 100644 --- a/crates/ruff_benchmark/benches/module_resolution.rs +++ b/crates/ruff_benchmark/benches/module_resolution.rs @@ -12,8 +12,7 @@ use ty_module_resolver::{ImportingFile, ModuleName, resolve_module}; use ty_project::metadata::options::{EnvironmentOptions, Options}; use ty_project::metadata::python_version::SupportedPythonVersion; use ty_project::metadata::value::RelativePathBuf; -use ty_project::{ProjectDatabase, ProjectMetadata}; -use ty_python_core::program::Program; +use ty_project::{Db as _, ProjectDatabase, ProjectMetadata}; const SEEDED_TARGETS: &[&str] = &["target_0", "target_1", "target_2", "target_3", "target_4"]; // Exercise stub-overlay discovery followed by normal fallback. @@ -73,8 +72,10 @@ fn setup_case(n: usize) -> Case { }); let db = ProjectDatabase::fallible(metadata, system).unwrap(); - // Intern the resolver environment before timing so its initial allocation is not benchmarked. - let _ = Program::get(&db).resolver_environment(&db); + + // Keep lazy program and resolver initialization out of the measured module-resolution queries. + let _ = db.project().program(&db).resolver_environment(&db); + let importing_file = system_path_to_file(&db, &importing_path).unwrap(); let resolves = SEEDED_TARGETS @@ -96,7 +97,11 @@ fn ty_module_resolver(bencher: Bencher) { bencher .with_inputs(|| setup_case(PATHS)) .bench_local_refs(|case| { - let environment = Program::get(&case.db).resolver_environment(&case.db); + let environment = case + .db + .project() + .program(&case.db) + .resolver_environment(&case.db); for name in &case.resolves { black_box(resolve_module( &case.db, diff --git a/crates/ty/tests/file_watching.rs b/crates/ty/tests/file_watching.rs index 7cc6b86024..943cab4bb5 100644 --- a/crates/ty/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -19,7 +19,6 @@ use ty_project::metadata::value::{RelativeGlobPattern, RelativePathBuf}; use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher}; use ty_project::{ChangeResult, Db, ProjectDatabase, ProjectMetadata}; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::Program; use ty_static::EnvVars; struct TestCase { @@ -39,7 +38,7 @@ fn resolve_module_confident<'db>( ) -> Option> { ty_module_resolver::resolve_module_confident( db, - Program::get(db).resolver_environment(db), + db.project().program(db).resolver_environment(db), module_name, ) } diff --git a/crates/ty_ide/src/code_action.rs b/crates/ty_ide/src/code_action.rs index fcd8648c66..67fd68257c 100644 --- a/crates/ty_ide/src/code_action.rs +++ b/crates/ty_ide/src/code_action.rs @@ -892,8 +892,6 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program().unwrap(); - let mut cleansed = dedent(source).to_string(); let start = cleansed diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 4669252242..265f9a6531 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -819,8 +819,6 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program().unwrap(); - let source = dedent(source); let start = source.find(START); diff --git a/crates/ty_ide/src/lib.rs b/crates/ty_ide/src/lib.rs index 34ec0997a9..f5a9276d3c 100644 --- a/crates/ty_ide/src/lib.rs +++ b/crates/ty_ide/src/lib.rs @@ -409,7 +409,7 @@ mod tests { use ruff_db::files::{File, FileRootKind, system_path_to_file}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; use ruff_db::source::{SourceText, source_text}; - use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem, SystemPath, SystemPathBuf}; + use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf}; use ruff_python_ast::PythonVersion; use ruff_python_codegen::Stylist; use ruff_python_trivia::textwrap::dedent; @@ -418,7 +418,7 @@ mod tests { use ty_project::{Db as _, ProjectMetadata, SemanticDb as _}; use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_python_semantic::PythonVersionWithSource; /// A way to create a simple single-file (named `main.py`) cursor test. @@ -525,10 +525,9 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program_with_python_version( - self.python_version.unwrap_or_else(PythonVersion::latest_ty), - ) - .unwrap(); + if let Some(python_version) = self.python_version { + db.set_python_version(python_version); + } let mut cursor: Option = None; for &Source { @@ -656,7 +655,7 @@ mod tests { let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", project_root.clone())); - // Write site-packages files first (before init) + // Write site-packages files first. for Source { path, contents, @@ -668,11 +667,6 @@ mod tests { .expect("write to memory file system to be successful"); } - // Create /src directory for first-party code - db.memory_file_system() - .create_directory_all(&project_root) - .expect("create /src directory"); - // Configure search paths with site-packages let search_paths = SearchPathSettings { src_roots: vec![project_root.clone()], @@ -682,8 +676,8 @@ mod tests { .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) .expect("valid search paths"); - Program::from_settings( - &db, + db.project().update_program( + &mut db, ProgramSettings { python_version: PythonVersionWithSource::default(), python_platform: PythonPlatform::default(), diff --git a/crates/ty_ide/src/semantic_tokens.rs b/crates/ty_ide/src/semantic_tokens.rs index 0432ab3df7..86a78648ce 100644 --- a/crates/ty_ide/src/semantic_tokens.rs +++ b/crates/ty_ide/src/semantic_tokens.rs @@ -4708,8 +4708,6 @@ from pathlib import Missing as Alias let mut db = ty_project::TestDb::new(ProjectMetadata::new("test", SystemPathBuf::from("/"))); - db.init_program().unwrap(); - let path = SystemPath::new("src/main.py"); db.write_file(path, ruff_python_trivia::textwrap::dedent(source)) .expect("Write to memory file system to always succeed"); diff --git a/crates/ty_ide/src/symbols.rs b/crates/ty_ide/src/symbols.rs index 2ccdaec2ec..2020ccb099 100644 --- a/crates/ty_ide/src/symbols.rs +++ b/crates/ty_ide/src/symbols.rs @@ -3229,8 +3229,7 @@ class C: ... let metadata = ProjectMetadata::new("test", SystemPathBuf::from("/")); let mut db = TestDb::new(metadata); - db.init_program_with_python_version(self.python_version.unwrap_or_default()) - .unwrap(); + db.set_python_version(self.python_version.unwrap_or_default()); for Source { path, contents } in &self.sources { db.write_file(path, contents) diff --git a/crates/ty_ide/src/workspace_symbols.rs b/crates/ty_ide/src/workspace_symbols.rs index f882fc6adf..da1b0a3d17 100644 --- a/crates/ty_ide/src/workspace_symbols.rs +++ b/crates/ty_ide/src/workspace_symbols.rs @@ -15,7 +15,6 @@ pub fn workspace_symbols(db: &dyn Db, query: &str) -> Vec { let _span = workspace_symbols_span.enter(); let project = db.project(); - let query = QueryPattern::fuzzy(query); let files = project.files(db); let files: Vec<_> = files.iter().copied().collect(); diff --git a/crates/ty_module_resolver/src/resolve.rs b/crates/ty_module_resolver/src/resolve.rs index fef29c17f5..0a100649e9 100644 --- a/crates/ty_module_resolver/src/resolve.rs +++ b/crates/ty_module_resolver/src/resolve.rs @@ -836,9 +836,8 @@ impl SearchPaths { /// Returns a new `SearchPaths` with no search paths configured. /// - /// This is primarily useful for testing. - #[cfg(test)] - pub(crate) fn empty(vendored: &VendoredFileSystem) -> Self { + /// The vendored standard library remains available. + pub fn empty(vendored: &VendoredFileSystem) -> Self { Self { static_paths: vec![], stdlib_path: Some(SearchPath::vendored_stdlib()), diff --git a/crates/ty_project/src/db.rs b/crates/ty_project/src/db.rs index a9cff91b5b..a276873110 100644 --- a/crates/ty_project/src/db.rs +++ b/crates/ty_project/src/db.rs @@ -13,12 +13,9 @@ use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{File, Files}; use ruff_db::system::System; use ruff_db::vendored::VendoredFileSystem; -use ruff_python_ast::PythonVersion; use salsa::{Database, Event, Setter}; use ty_python_core::ProgramFile; -use ty_python_core::program::{ - FallibleStrategy, MisconfigurationStrategy, Program, UseDefaultStrategy, -}; +use ty_python_core::program::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{AnalysisSettings, Db as SemanticDb}; @@ -26,9 +23,6 @@ mod changes; #[salsa::db] pub trait Db: SemanticDb { - /// Returns the Python version for files in the primary environment. - fn python_version(&self) -> PythonVersion; - fn project(&self) -> Project; fn dyn_clone(&self) -> Box; @@ -87,16 +81,12 @@ impl ProjectDatabase { /// Permanently freezes the most heavily read inputs that are immutable during a one-shot check. /// - /// This is intentionally not exhaustive. It includes every [`Program`] input, the most heavily + /// This is intentionally not exhaustive. It includes the program, the most heavily /// read immutable [`Project`] inputs, and every field on files created after this call. Existing /// files retain their durability. This must not be used by incremental consumers or checks that /// apply fixes. pub fn freeze(&mut self) { - let program = Program::try_get(self).expect("the program should be initialized"); - let project = self.project(); - - program.freeze(self); - project.freeze(self); + self.project().freeze(self); self.files.freeze(); } @@ -142,28 +132,30 @@ impl ProjectDatabase { let merged_options = project_metadata.to_merged_options(); - // Initialize the `Program` singleton let (program_settings, program_settings_diagnostics) = strategy .to_anyhow(merged_options.to_program_settings(db.system(), db.vendored(), strategy))?; - // This must be called before `from_settings`, or the `SearchPath` root + // This must be called before `from_metadata`, or the `SearchPath` root // will take precedence over the `Project` root, resulting in // all project files having HIGH durability. project_metadata.try_add_project_root(&db); - Program::from_settings(&db, program_settings); - - let (settings, settings_diagnostics) = strategy + let (settings, mut settings_diagnostics) = strategy .map_err(merged_options.to_settings(&db, strategy), |error| { anyhow::anyhow!("{}", error.pretty(&db)) })?; + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(&db)), + ); db.project = Some(Project::from_metadata( &db, project_metadata, settings, + program_settings, settings_diagnostics, - program_settings_diagnostics, )); Ok(db) @@ -547,7 +539,7 @@ impl SemanticDb for ProjectDatabase { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.project().program(self).program_file(self, file) } fn rule_selection(&self, file: File) -> &RuleSelection { @@ -610,10 +602,6 @@ impl salsa::Database for ProjectDatabase {} #[salsa::db] impl Db for ProjectDatabase { - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } - fn project(&self) -> Project { self.project.unwrap() } @@ -625,15 +613,17 @@ impl Db for ProjectDatabase { #[cfg(feature = "format")] mod format { - use crate::{Db as _, ProjectDatabase}; + use crate::ProjectDatabase; use ruff_db::files::File; use ruff_python_formatter::{Db as FormatDb, PyFormatOptions}; + use ty_python_semantic::Db as _; #[salsa::db] impl FormatDb for ProjectDatabase { fn format_options(&self, file: File) -> PyFormatOptions { let source_ty = file.source_type(self); - PyFormatOptions::from_source_type(source_ty).with_target_version(self.python_version()) + PyFormatOptions::from_source_type(source_ty) + .with_target_version(self.program_file(file).python_version(self)) } } } @@ -648,13 +638,16 @@ pub(crate) mod testing { use ruff_db::files::{File, FileRootKind, Files}; use ruff_db::system::{DbWithTestSystem, System, TestSystem}; use ruff_db::vendored::VendoredFileSystem; + #[cfg(feature = "testing")] use ruff_python_ast::PythonVersion; use ty_module_resolver::SearchPathSettings; use ty_python_core::ProgramFile; use ty_python_core::platform::PythonPlatform; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; + #[cfg(feature = "testing")] + use ty_python_semantic::ProgramEnvironment; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; - use ty_python_semantic::{AnalysisSettings, ProgramEnvironment, PythonVersionWithSource}; + use ty_python_semantic::{AnalysisSettings, PythonVersionWithSource}; use crate::db::Db; use crate::{Project, ProjectMetadata}; @@ -694,51 +687,52 @@ pub(crate) mod testing { .to_merged_options() .to_settings(&db, &FallibleStrategy) .unwrap(); - let project = - Project::from_metadata(&db, project, settings, settings_diagnostics, Vec::new()); + let root = project.root().to_path_buf(); + db.system + .memory_file_system() + .create_directory_all(&root) + .expect("create project root"); + let search_paths = SearchPathSettings::new(vec![root.clone()]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .expect("Valid search path settings"); + + db.files().try_add_root(&db, &root, FileRootKind::Project); + + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths, + }; + let project = Project::from_metadata( + &db, + project, + settings, + program_settings, + settings_diagnostics, + ); db.project = Some(project); db } - pub fn init_program(&mut self) -> anyhow::Result<()> { - self.init_program_with_python_version(PythonVersion::latest_ty()) - } - - pub fn init_program_with_python_version( - &mut self, - python_version: PythonVersion, - ) -> anyhow::Result<()> { - let root = self.project().root(self); - - let search_paths = SearchPathSettings::new(vec![root.to_path_buf()]) - .to_search_paths(self.system(), self.vendored(), &FallibleStrategy) - .expect("Valid search path settings"); - - self.files().try_add_root(self, root, FileRootKind::Project); - - Program::from_settings( - self, - ProgramSettings { - python_version: PythonVersionWithSource { - source: ty_python_semantic::PythonVersionSource::Default, - version: python_version, - }, - python_platform: PythonPlatform::default(), - search_paths, + #[cfg(feature = "testing")] + pub fn set_python_version(&mut self, python_version: PythonVersion) { + let program = self.project().program(self); + let settings = ProgramSettings { + python_version: PythonVersionWithSource { + source: ty_python_semantic::PythonVersionSource::Default, + version: python_version, }, - ); - - Ok(()) + python_platform: program.python_platform(self).clone(), + search_paths: program.search_paths(self).clone(), + }; + self.project().update_program(self, settings); } } impl TestDb { - pub fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } - + #[cfg(feature = "testing")] pub fn program_environment(&self) -> ProgramEnvironment<'_> { - ProgramEnvironment::from_program(Program::get(self).resolver_environment(self)) + ProgramEnvironment::from_program(self.project().program(self)) } /// Takes the salsa events. @@ -786,15 +780,15 @@ pub(crate) mod testing { #[salsa::db] impl ty_python_semantic::Db for TestDb { + fn program_file(&self, file: File) -> ProgramFile<'_> { + self.project().program(self).program_file(self, file) + } + #[inline] fn check_file(&self, file: File) -> Vec { crate::check_file(self, file) } - fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) - } - fn rule_selection(&self, _file: ruff_db::files::File) -> &RuleSelection { self.project().rules(self) } @@ -822,10 +816,6 @@ pub(crate) mod testing { #[salsa::db] impl Db for TestDb { - fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) - } - fn project(&self) -> Project { self.project.unwrap() } @@ -845,9 +835,8 @@ mod tests { use ruff_db::files::FileRootKind; use ruff_db::system::{SystemPathBuf, TestSystem}; use ty_module_resolver::list_modules; - use ty_python_core::program::Program; - use crate::{ProjectDatabase, ProjectMetadata}; + use crate::{Db as _, ProjectDatabase, ProjectMetadata}; #[test] fn frozen_inputs_support_a_one_shot_check() -> anyhow::Result<()> { @@ -891,7 +880,7 @@ mod tests { let metadata = ProjectMetadata::discover(&project, &system)?; let db = ProjectDatabase::fallible(metadata, system)?; - let modules = list_modules(&db, Program::get(&db).resolver_environment(&db)); + let modules = list_modules(&db, db.project().program(&db).resolver_environment(&db)); assert!( modules .iter() diff --git a/crates/ty_project/src/db/changes.rs b/crates/ty_project/src/db/changes.rs index c27045e445..4530a9d189 100644 --- a/crates/ty_project/src/db/changes.rs +++ b/crates/ty_project/src/db/changes.rs @@ -8,7 +8,7 @@ use ruff_db::Db as _; use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{SystemPath, SystemPathBuf}; use rustc_hash::FxHashSet; -use ty_python_core::program::{FallibleStrategy, Program}; +use ty_python_core::program::FallibleStrategy; /// Represents the result of applying changes to the project database. pub struct ChangeResult { @@ -34,7 +34,7 @@ impl ProjectDatabase { let project = self.project(); let project_root = project.root(self).to_path_buf(); let configuration_paths = ConfigurationPaths::from_metadata(project.metadata(self)); - let program = Program::get(self); + let program = self.project().program(self); let custom_stdlib_versions_path = program .custom_stdlib_search_path(self) .map(|path| path.join("VERSIONS")); @@ -253,8 +253,7 @@ impl ProjectDatabase { &FallibleStrategy, ) { Ok((program_settings, diagnostics)) => { - let program = Program::get(self); - program.update_from_settings(self, program_settings); + project.update_program(self, program_settings); diagnostics } Err(error) => { @@ -265,7 +264,7 @@ impl ProjectDatabase { } }; - let (settings, settings_diagnostics) = match merged_options + let (settings, mut settings_diagnostics) = match merged_options .to_settings(self, &FallibleStrategy) { Ok((settings, diagnostics)) => (Some(settings), diagnostics), @@ -276,15 +275,14 @@ impl ProjectDatabase { (None, vec![error.into_diagnostic()]) } }; + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(self)), + ); tracing::debug!("Reloading project after structural change"); - match project.reload( - self, - metadata, - settings, - settings_diagnostics, - program_settings_diagnostics, - ) { + match project.reload(self, metadata, settings, settings_diagnostics) { ProjectReloadResult::Unchanged => {} ProjectReloadResult::Changed { files_changed } => { result.project_changed = true; @@ -325,17 +323,18 @@ impl ProjectDatabase { &FallibleStrategy, ) { Ok((program_settings, program_settings_diagnostics)) => { - let settings_diagnostics = + let mut settings_diagnostics = match merged_options.to_settings(self, &FallibleStrategy) { Ok((_, diagnostics)) => diagnostics, Err(error) => vec![error.into_diagnostic()], }; - program.update_from_settings(self, program_settings); - project.update_settings_diagnostics( - self, - settings_diagnostics, - program_settings_diagnostics, + project.update_program(self, program_settings); + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(self)), ); + project.update_settings_diagnostics(self, settings_diagnostics); } Err(error) => { tracing::error!("Failed to resolve program settings: {error}"); diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index b461f64078..45c84dd18a 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -3,7 +3,7 @@ reason = "Prefer System trait methods over std methods in ty crates" )] use crate::glob::{GlobFilterCheckMode, IncludeResult}; -use crate::metadata::options::{OptionDiagnostic, ProgramSettingsDiagnostic}; +use crate::metadata::options::OptionDiagnostic; use crate::parallel::ParallelIteratorExt; use crate::walk::{ProjectFilesFilter, ProjectFilesWalker}; #[cfg(feature = "testing")] @@ -28,6 +28,7 @@ use std::iter::FusedIterator; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::sync::Arc; use ty_python_core::ProgramFile; +use ty_python_core::program::{Program, ProgramSettings}; pub use ty_python_semantic::Db as SemanticDb; use ty_python_semantic::lint::RuleSelection; @@ -44,7 +45,7 @@ pub mod watch; /// ## How is a project different from a program? /// There are two (related) motivations: /// -/// 1. Program is defined in `ruff_db` and it can't reference the settings types for the linter and formatter +/// 1. Program is defined in `ty_python_core` and it can't reference the settings types for the linter and formatter /// without introducing a cyclic dependency. The project is defined in a higher level crate /// where it can reference these setting types. /// 2. Running `ruff check` with different target versions results in different programs (settings) but @@ -79,6 +80,10 @@ pub struct Project { #[returns(deref)] pub settings: Box, + /// The settings used to construct the Python program for this project. + #[returns(ref)] + pub program_settings: ProgramSettings, + /// The paths that should be included when checking this project. /// /// The default (when this list is empty) is to include all files in the project root @@ -175,34 +180,33 @@ impl ProgressReporter for CollectReporter { #[salsa::tracked] impl Project { /// Create a project from resolved metadata and settings. - /// - /// Program-settings diagnostics are accepted separately so callers do not need to know how to - /// convert and merge them into the stored project settings diagnostics. fn from_metadata( db: &dyn Db, metadata: ProjectMetadata, settings: Settings, + program_settings: ProgramSettings, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) -> Self { - let diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); + program_settings.search_paths.try_register_static_roots(db); - 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::builder( + Box::new(metadata), + Box::new(settings), + program_settings, + settings_diagnostics, + ) + .durability(Durability::MEDIUM) + .open_fileset_durability(Durability::LOW) + .file_set_durability(Durability::LOW) + .new(db) } - /// Permanently freezes the most heavily read immutable project inputs. + /// Permanently freezes the most heavily read immutable project and program inputs. /// /// This is intentionally not exhaustive. fn freeze(self, db: &mut dyn Db) { let durability = Durability::NEVER_CHANGE; + let program_settings = self.program_settings(db).clone(); let metadata = Box::new(self.metadata(db).clone()); let settings = Box::new(self.settings(db).clone()); let included_paths = self.included_paths_list(db).to_vec(); @@ -216,6 +220,9 @@ impl Project { self.set_settings(db) .with_durability(durability) .to(settings); + self.set_program_settings(db) + .with_durability(durability) + .to(program_settings); self.set_included_paths_list(db) .with_durability(durability) .to(included_paths); @@ -232,6 +239,18 @@ impl Project { IndexedFiles::freeze(db, self); } + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + pub fn program(self, db: &dyn Db) -> Program<'_> { + Program::from_settings(db, self.program_settings(db).clone()) + } + + pub fn update_program(self, db: &mut dyn Db, settings: ProgramSettings) { + if self.program_settings(db) != &settings { + settings.search_paths.try_register_static_roots(db); + self.set_program_settings(db).to(settings); + } + } + pub fn root(self, db: &dyn Db) -> &SystemPath { self.metadata(db).root() } @@ -270,25 +289,15 @@ impl Project { } /// Reload the project after its metadata or settings have changed. - /// - /// Program-settings diagnostics are converted and merged here to keep reload behavior - /// consistent with initial project creation. pub fn reload( self, db: &mut dyn Db, metadata: ProjectMetadata, settings: Option, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) -> ProjectReloadResult { tracing::debug!("Reloading project"); let metadata_changed = &metadata != self.metadata(db); - let settings_diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); - let root_changed = metadata.root() != self.root(db); let (settings_changed, files_changed) = if let Some(settings) = settings && self.settings(db) != &settings @@ -331,32 +340,12 @@ impl Project { self, db: &mut dyn Db, settings_diagnostics: Vec, - program_settings_diagnostics: Vec, ) { - let settings_diagnostics = Self::settings_diagnostics_with_program_diagnostics( - db, - settings_diagnostics, - program_settings_diagnostics, - ); - if self.settings_diagnostics(db) != settings_diagnostics { self.set_settings_diagnostics(db).to(settings_diagnostics); } } - fn settings_diagnostics_with_program_diagnostics( - db: &dyn Db, - mut settings_diagnostics: Vec, - program_settings_diagnostics: Vec, - ) -> Vec { - settings_diagnostics.extend( - program_settings_diagnostics - .into_iter() - .map(|diagnostic| diagnostic.into_diagnostic(db)), - ); - settings_diagnostics - } - /// Checks the project and its dependencies according to the project's check mode. fn check(self, db: &ProjectDatabase, reporter: &mut dyn ProgressReporter) { let project_span = tracing::debug_span!("Project::check"); @@ -906,7 +895,6 @@ mod tests { fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { let project = ProjectMetadata::new("test", SystemPathBuf::from("/")); let mut db = TestDb::new(project); - db.init_program().unwrap(); let path = SystemPath::new("test.py"); db.write_file(path, "x = 10")?; diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 000dcfb460..a07a8ce03a 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -613,7 +613,7 @@ pub enum ProgramSettingsDiagnostic { impl ProgramSettingsDiagnostic { /// Convert this program-settings diagnostic into a diagnostic that can be stored on a project. - pub(crate) fn into_diagnostic(self, db: &dyn Db) -> OptionDiagnostic { + pub fn into_diagnostic(self, db: &dyn Db) -> OptionDiagnostic { match self { Self::UnsupportedInferredPythonVersion(python_version) => { unsupported_inferred_python_version_diagnostic(db, &python_version) diff --git a/crates/ty_project/src/watch/project_watcher.rs b/crates/ty_project/src/watch/project_watcher.rs index c4829c4727..4b5f8afef2 100644 --- a/crates/ty_project/src/watch/project_watcher.rs +++ b/crates/ty_project/src/watch/project_watcher.rs @@ -6,7 +6,6 @@ use tracing::info; use ruff_cache::{CacheKey, CacheKeyHasher}; use ruff_db::system::{SystemPath, SystemPathBuf}; use ty_module_resolver::system_module_search_paths; -use ty_python_core::program::Program; use crate::db::{Db, ProjectDatabase}; use crate::watch::Watcher; @@ -41,7 +40,7 @@ impl ProjectWatcher { } pub fn update(&mut self, db: &ProjectDatabase) { - let environment = Program::get(db).resolver_environment(db); + let environment = db.project().program(db).resolver_environment(db); let search_paths: Vec<_> = system_module_search_paths(db, environment).collect(); let project_path = db.project().root(db); diff --git a/crates/ty_python_core/Cargo.toml b/crates/ty_python_core/Cargo.toml index fad05d5d0e..35238e3ea3 100644 --- a/crates/ty_python_core/Cargo.toml +++ b/crates/ty_python_core/Cargo.toml @@ -48,6 +48,7 @@ anyhow = { workspace = true } [features] serde = ["dep:serde", "dep:ruff_macros"] schemars = ["dep:schemars", "dep:serde_json"] +testing = [] [lints] workspace = true diff --git a/crates/ty_python_core/src/builder.rs b/crates/ty_python_core/src/builder.rs index 42501ef7ed..d0b9227631 100644 --- a/crates/ty_python_core/src/builder.rs +++ b/crates/ty_python_core/src/builder.rs @@ -254,6 +254,7 @@ pub(super) struct SemanticIndexBuilder<'db, 'ast> { // Used for checking semantic syntax errors resolver_environment: ResolverEnvironment<'db>, + python_version: PythonVersion, source_text: OnceCell, semantic_checker: SemanticSyntaxChecker, in_try: bool, @@ -345,6 +346,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { enclosing_snapshots: FxHashMap::default(), resolver_environment: file.resolver_environment(db), + python_version: file.python_version(db), source_text: OnceCell::new(), semantic_checker: SemanticSyntaxChecker::default(), in_try: false, @@ -5015,7 +5017,7 @@ impl SemanticSyntaxContext for SemanticIndexBuilder<'_, '_> { } fn python_version(&self) -> PythonVersion { - self.resolver_environment.python_version(self.db) + self.python_version } fn source(&self) -> &str { diff --git a/crates/ty_python_core/src/db.rs b/crates/ty_python_core/src/db.rs index 831360fc94..98bd1ae087 100644 --- a/crates/ty_python_core/src/db.rs +++ b/crates/ty_python_core/src/db.rs @@ -1,6 +1,9 @@ use ruff_db::files::File; use ty_module_resolver::Db as ModuleResolverDb; +#[cfg(any(test, feature = "testing"))] +use crate::program::{Program, ProgramSettings}; + /// Database giving access to semantic information about a Python program. #[salsa::db] pub trait Db: ModuleResolverDb { @@ -8,6 +11,25 @@ pub trait Db: ModuleResolverDb { fn should_check_file(&self, file: File) -> bool; } +#[cfg(any(test, feature = "testing"))] +#[salsa::db] +pub trait TestProgramDb: Db { + fn program_settings(&self) -> &ProgramSettings; + + // Salsa-cached because interning a program requires hashing all search paths. + fn program(&self) -> Program<'_> + where + Self: Sized, + { + #[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)] + fn program_inner(db: &dyn TestProgramDb) -> Program<'_> { + Program::from_settings(db, db.program_settings().clone()) + } + + program_inner(self) + } +} + #[cfg(test)] pub(crate) mod tests { use std::sync::{Arc, Mutex}; @@ -25,9 +47,9 @@ pub(crate) mod tests { use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use crate::platform::PythonPlatform; - use crate::program::{Program, ProgramSettings}; + use crate::program::ProgramSettings; - use super::Db; + use super::{Db, TestProgramDb}; type Events = Arc>>; @@ -38,11 +60,14 @@ pub(crate) mod tests { files: Files, system: TestSystem, vendored: VendoredFileSystem, + program_settings: ProgramSettings, } impl TestDb { fn new() -> Self { let events = Events::default(); + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); Self { storage: salsa::Storage::new(Some(Box::new({ move |event| { @@ -52,8 +77,9 @@ pub(crate) mod tests { } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), + program_settings, } } } @@ -93,6 +119,13 @@ pub(crate) mod tests { #[salsa::db] impl ModuleResolverDb for TestDb {} + #[salsa::db] + impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } + } + #[salsa::db] impl salsa::Database for TestDb {} @@ -132,19 +165,18 @@ pub(crate) mod tests { db.write_files(self.files) .context("Failed to write test files")?; - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource { - version: self.python_version, - source: PythonVersionSource::default(), - }, - python_platform: self.python_platform, - search_paths: SearchPathSettings::new(vec![src_root]) - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .context("Invalid search path settings")?, + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource { + version: self.python_version, + source: PythonVersionSource::default(), }, - ); + python_platform: self.python_platform, + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .context("Invalid search path settings")?, + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; Ok(db) } diff --git a/crates/ty_python_core/src/lib.rs b/crates/ty_python_core/src/lib.rs index 0d86cf8001..84ed3749a6 100644 --- a/crates/ty_python_core/src/lib.rs +++ b/crates/ty_python_core/src/lib.rs @@ -15,11 +15,7 @@ use ruff_text_size::TextRange; use rustc_hash::{FxHashMap, FxHashSet}; use salsa::plumbing::AsId; use smallvec::SmallVec; -use ty_module_resolver::{ModuleName, ResolverEnvironment}; - -// FIXME: Replace this temporary alias once semantic query keys can use the environment-bearing -// `Program` Salsa ingredient directly. -pub type Program<'db> = ResolverEnvironment<'db>; +use ty_module_resolver::ModuleName; use crate::frozen::{FrozenMap, FrozenSet}; use crate::place::ScopedPlaceId; @@ -64,8 +60,11 @@ pub mod symbol; pub mod unpack; mod use_def; pub use db::Db; +#[cfg(any(test, feature = "testing"))] +pub use db::TestProgramDb; pub mod program; pub mod program_file; +pub use program::Program; pub use program_file::ProgramFile; /// Returns the semantic index for `file`. @@ -1126,7 +1125,7 @@ mod tests { } fn program_file(db: &TestDb, file: File) -> ProgramFile<'_> { - Program::get(db).program_file(db, file) + db.program().program_file(db, file) } fn names(table: &PlaceTable) -> Vec { @@ -1861,13 +1860,14 @@ class C[T]: scopes: impl Iterator, db: &'db dyn Db, file: File, + program: Program<'db>, module: &'a ParsedModuleRef, ) -> Vec<&'a str> { scopes .into_iter() .map(|(scope_id, _)| { scope_id - .to_scope_id(db, Program::get(db).program_file(db, file)) + .to_scope_id(db, program.program_file(db, file)) .name(db, module) }) .collect() @@ -1891,17 +1891,20 @@ def x(): let descendants = index.descendent_scopes(FileScopeId::global()); assert_eq!( - scope_names(descendants, &db, file, &module), + scope_names(descendants, &db, file, db.program(), &module), vec!["Test", "foo", "bar", "baz", "x"] ); let children = index.child_scopes(FileScopeId::global()); - assert_eq!(scope_names(children, &db, file, &module), vec!["Test", "x"]); + assert_eq!( + scope_names(children, &db, file, db.program(), &module), + vec!["Test", "x"] + ); let test_class = index.child_scopes(FileScopeId::global()).next().unwrap().0; let test_child_scopes = index.child_scopes(test_class); assert_eq!( - scope_names(test_child_scopes, &db, file, &module), + scope_names(test_child_scopes, &db, file, db.program(), &module), vec!["foo", "baz"] ); @@ -1913,7 +1916,7 @@ def x(): let ancestors = index.ancestor_scopes(bar_scope); assert_eq!( - scope_names(ancestors, &db, file, &module), + scope_names(ancestors, &db, file, db.program(), &module), vec!["bar", "foo", "Test", ""] ); } diff --git a/crates/ty_python_core/src/platform.rs b/crates/ty_python_core/src/platform.rs index a575baba47..8da3b45817 100644 --- a/crates/ty_python_core/src/platform.rs +++ b/crates/ty_python_core/src/platform.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter}; use ty_combine::Combine; /// The target platform to assume when resolving types. -#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize, ruff_macros::RustDoc), diff --git a/crates/ty_python_core/src/program.rs b/crates/ty_python_core/src/program.rs index 6934f23896..59f5971b0d 100644 --- a/crates/ty_python_core/src/program.rs +++ b/crates/ty_python_core/src/program.rs @@ -2,9 +2,8 @@ use crate::{Db, platform::PythonPlatform}; use ruff_db::files::File; use ruff_db::system::SystemPath; +use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; -use salsa::Durability; -use salsa::Setter; use ty_module_resolver::{ResolverEnvironment, SearchPaths}; use ty_site_packages::PythonVersionWithSource; @@ -13,110 +12,66 @@ use crate::ProgramFile; // Re-export the misconfiguration strategy types from ty_module_resolver. pub use ty_module_resolver::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy}; -#[salsa::input(singleton, heap_size=ruff_memory_usage::heap_size)] -pub struct Program { +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct Program<'db> { + // FIXME: Move the source out of `Program`. Different source locations prevent otherwise + // equivalent programs from being reused across scripts. #[returns(ref)] pub python_version_with_source: PythonVersionWithSource, #[returns(ref)] pub python_platform: PythonPlatform, - #[returns(ref)] - pub search_paths: SearchPaths, + #[returns(copy)] + pub resolver_environment: ResolverEnvironment<'db>, } -#[salsa::tracked] -impl Program { - pub fn init_or_update(db: &mut dyn Db, settings: ProgramSettings) -> Self { - match Self::try_get(db) { - Some(program) => { - program.update_from_settings(db, settings); - program - } - None => Self::from_settings(db, settings), - } - } +impl get_size2::GetSize for Program<'_> {} - pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> Self { +impl<'db> Program<'db> { + /// Creates a program from settings whose search roots have already been registered. + pub fn from_settings(db: &'db dyn Db, settings: ProgramSettings) -> Self { let ProgramSettings { python_version, python_platform, search_paths, } = settings; - search_paths.try_register_static_roots(db); - - Program::builder(python_version, python_platform, search_paths) - .durability(Durability::HIGH) - .new(db) + let resolver_environment = + ResolverEnvironment::new(db, python_version.version, &search_paths); + Program::new(db, python_version, python_platform, resolver_environment) } - pub fn python_version(self, db: &dyn Db) -> PythonVersion { - self.python_version_with_source(db).version + pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { + self.resolver_environment(db).python_version(db) } - /// Returns the module-resolution environment for this program. - pub fn resolver_environment(self, db: &dyn Db) -> ResolverEnvironment<'_> { - ResolverEnvironment::new(db, self.python_version(db), self.search_paths(db)) + pub fn search_paths(self, db: &'db dyn Db) -> &'db SearchPaths { + self.resolver_environment(db).search_paths(db) } - pub fn program_file(self, db: &dyn Db, file: File) -> ProgramFile<'_> { - ProgramFile::new(db, file, self.resolver_environment(db)) + pub fn program_file(self, db: &'db dyn Db, file: File) -> ProgramFile<'db> { + ProgramFile::new(db, file, self) } - pub fn update_from_settings(self, db: &mut dyn Db, settings: ProgramSettings) { - let ProgramSettings { - python_version, - python_platform, - search_paths, - } = settings; - - if self.search_paths(db) != &search_paths { - tracing::debug!("Updating search paths"); - search_paths.try_register_static_roots(db); - self.set_search_paths(db).to(search_paths); - } - - if &python_platform != self.python_platform(db) { - tracing::debug!("Updating python platform: `{python_platform:?}`"); - self.set_python_platform(db).to(python_platform); - } - - if &python_version != self.python_version_with_source(db) { - tracing::debug!( - "Updating python version: Python {version}", - version = python_version.version - ); - self.set_python_version_with_source(db).to(python_version); - } - } - - /// Permanently freezes all program inputs. - pub fn freeze(self, db: &mut dyn Db) { - let durability = Durability::NEVER_CHANGE; - let python_version = self.python_version_with_source(db).clone(); - let python_platform = self.python_platform(db).clone(); - let search_paths = self.search_paths(db).clone(); - - self.set_python_version_with_source(db) - .with_durability(durability) - .to(python_version); - self.set_python_platform(db) - .with_durability(durability) - .to(python_platform); - self.set_search_paths(db) - .with_durability(durability) - .to(search_paths); - } - - pub fn custom_stdlib_search_path(self, db: &dyn Db) -> Option<&SystemPath> { + pub fn custom_stdlib_search_path(self, db: &'db dyn Db) -> Option<&'db SystemPath> { self.search_paths(db).custom_stdlib() } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)] pub struct ProgramSettings { pub python_version: PythonVersionWithSource, pub python_platform: PythonPlatform, pub search_paths: SearchPaths, } + +impl ProgramSettings { + pub fn empty(vendored: &VendoredFileSystem) -> Self { + Self { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPaths::empty(vendored), + } + } +} diff --git a/crates/ty_python_core/src/program_file.rs b/crates/ty_python_core/src/program_file.rs index a232e82cf1..bc6548a6d8 100644 --- a/crates/ty_python_core/src/program_file.rs +++ b/crates/ty_python_core/src/program_file.rs @@ -3,7 +3,7 @@ use ruff_db::files::File; use ruff_python_ast::PythonVersion; use ty_module_resolver::{ResolverEnvironment, ResolverFile}; -use crate::{Db, Program}; +use crate::{Db, program::Program}; /// A file interpreted within a particular Python program. /// @@ -54,24 +54,21 @@ use crate::{Db, Program}; heap_size = ruff_memory_usage::heap_size )] pub struct ProgramFile<'db> { - /// The cached parser key for `file` and the environment's Python version. + /// Cache the parser key even though its Python version is redundant with `program`: + /// program files are created infrequently, but their parser keys are looked up extensively. #[returns(copy)] pub python_file: PythonFile<'db>, #[returns(copy)] - pub resolver_environment: ResolverEnvironment<'db>, + pub program: Program<'db>, } impl get_size2::GetSize for ProgramFile<'_> {} impl<'db> ProgramFile<'db> { - pub fn new( - db: &'db dyn Db, - file: File, - resolver_environment: ResolverEnvironment<'db>, - ) -> Self { - let python_file = PythonFile::new(db, file, resolver_environment.python_version(db)); - Self::new_internal(db, python_file, resolver_environment) + pub fn new(db: &'db dyn Db, file: File, program: Program<'db>) -> Self { + let python_file = PythonFile::new(db, file, program.python_version(db)); + Self::new_internal(db, python_file, program) } /// Returns the physical file represented by this program file. @@ -79,18 +76,18 @@ impl<'db> ProgramFile<'db> { self.python_file(db).file(db) } + /// Returns the module-resolution environment for this program file. + pub fn resolver_environment(self, db: &'db dyn Db) -> ResolverEnvironment<'db> { + self.program(db).resolver_environment(db) + } + /// Returns the resolver key for this file. pub fn resolver_file(self, db: &'db dyn Db) -> ResolverFile<'db> { ResolverFile::new(db, self.file(db), self.resolver_environment(db)) } - /// Returns the program associated with this file. - pub fn program(self, db: &'db dyn Db) -> Program<'db> { - self.resolver_environment(db) - } - - /// Returns the Python version associated with this file's resolver environment. + /// Returns the Python version associated with this file's program. pub fn python_version(self, db: &'db dyn Db) -> PythonVersion { - self.resolver_environment(db).python_version(db) + self.program(db).python_version(db) } } diff --git a/crates/ty_python_semantic/Cargo.toml b/crates/ty_python_semantic/Cargo.toml index 500d017ca5..ebdbfb5af8 100644 --- a/crates/ty_python_semantic/Cargo.toml +++ b/crates/ty_python_semantic/Cargo.toml @@ -73,7 +73,7 @@ serde = [ "ruff_python_ast/serde", "ty_python_core/serde", ] -testing = [] +testing = ["ty_python_core/testing"] [[test]] name = "mdtest" diff --git a/crates/ty_python_semantic/src/db.rs b/crates/ty_python_semantic/src/db.rs index 1d3044b0b4..305e7323d1 100644 --- a/crates/ty_python_semantic/src/db.rs +++ b/crates/ty_python_semantic/src/db.rs @@ -48,7 +48,8 @@ pub(crate) mod tests { use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::PythonVersion; use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings}; - use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; + use ty_python_core::TestProgramDb; + use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; type Events = Arc>>; @@ -64,11 +65,14 @@ pub(crate) mod tests { rule_selection: Arc, analysis_settings: Arc, open_files: rustc_hash::FxHashSet, + program_settings: ProgramSettings, } impl TestDb { fn new() -> Self { let events = Events::default(); + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); Self { storage: salsa::Storage::new(Some(Box::new({ let events = events.clone(); @@ -79,21 +83,22 @@ pub(crate) mod tests { } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, events, files: Files::default(), rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())), analysis_settings: AnalysisSettings::default().into(), open_files: rustc_hash::FxHashSet::default(), + program_settings, } } pub(crate) fn python_version(&self) -> PythonVersion { - Program::get(self).python_version(self) + self.program().python_version(self) } pub(crate) fn program_environment(&self) -> ProgramEnvironment<'_> { - ProgramEnvironment::from_program(Program::get(self).resolver_environment(self)) + ProgramEnvironment::from_program(self.program()) } /// Marks `file` as open in the editor. @@ -151,6 +156,13 @@ pub(crate) mod tests { } } + #[salsa::db] + impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } + } + #[salsa::db] impl Db for TestDb { fn check_file(&self, file: File) -> Vec { @@ -162,7 +174,7 @@ pub(crate) mod tests { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.program().program_file(self, file) } fn rule_selection(&self, _file: File) -> &RuleSelection { @@ -242,19 +254,18 @@ pub(crate) mod tests { db.write_files(self.files) .context("Failed to write test files")?; - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource { - version: self.python_version, - source: PythonVersionSource::default(), - }, - python_platform: self.python_platform, - search_paths: SearchPathSettings::new(vec![src_root]) - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .context("Invalid search path settings")?, + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource { + version: self.python_version, + source: PythonVersionSource::default(), }, - ); + python_platform: self.python_platform, + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .context("Invalid search path settings")?, + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; Ok(db) } diff --git a/crates/ty_python_semantic/src/diagnostic/mod.rs b/crates/ty_python_semantic/src/diagnostic/mod.rs index 0fbb0422c9..8ead4596ee 100644 --- a/crates/ty_python_semantic/src/diagnostic/mod.rs +++ b/crates/ty_python_semantic/src/diagnostic/mod.rs @@ -1,6 +1,6 @@ use crate::{ - Db, PythonVersionSource, PythonVersionWithSource, lint::lint_documentation_url, - types::TypeCheckDiagnostics, + Db, ProgramEnvironment, PythonVersionSource, PythonVersionWithSource, + lint::lint_documentation_url, types::TypeCheckDiagnostics, }; use levenshtein::{HideUnderscoredSuggestions, find_best_suggestion}; use ruff_db::{ @@ -46,11 +46,12 @@ pub fn inferred_python_version_source_annotation( /// configuration files, or defaults. pub(crate) fn add_inferred_python_version_hint_to_diagnostic( db: &dyn Db, + env: &ProgramEnvironment, diagnostic: &mut Diagnostic, action: &str, ) { - let program = ty_python_core::program::Program::get(db); - let PythonVersionWithSource { version, source } = program.python_version_with_source(db); + let PythonVersionWithSource { version, source } = + env.program(db).python_version_with_source(db); match source { crate::PythonVersionSource::Cli => { diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 417b52521e..773990d89f 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -204,7 +204,12 @@ pub fn check_file(db: &dyn Db, file: ProgramFile<'_>) -> Result( visibility: BuiltinVisibility, ) -> Option<(ScopeId<'db>, PlaceAndQualifiers<'db>)> { let program = env.program(db); + let resolver_environment = program.resolver_environment(db); let resolver = |module: Module<'db>| { let file = ProgramFile::new(db, module.file(db)?, program); let scope = global_scope(db, file); @@ -695,12 +696,13 @@ fn builtins_symbol_impl<'db>( // If this symbol is not present in project-level builtins, search in the default ones. resolve_module_confident( db, - program, + resolver_environment, &ModuleName::new_static("__builtins__").unwrap(), ) .and_then(&resolver) .or_else(|| { - resolve_module_confident(db, program, &KnownModule::Builtins.name()).and_then(resolver) + resolve_module_confident(db, resolver_environment, &KnownModule::Builtins.name()) + .and_then(resolver) }) } @@ -1424,7 +1426,7 @@ fn symbol_impl<'db>( "version_info" => { return Place::bound(Type::sys_version_info()).into(); } - "platform" => match ty_python_core::program::Program::get(db).python_platform(db) { + "platform" => match scope.program(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { return Place::bound(Type::string_literal(db, platform.as_str())).into(); } @@ -1437,7 +1439,7 @@ fn symbol_impl<'db>( } if name == "name" && is_known_module(KnownModule::Os) { - match ty_python_core::program::Program::get(db).python_platform(db) { + match scope.program(db).python_platform(db) { crate::PythonPlatform::Identifier(platform) => { // In CPython, `os.name` is `"nt"` on Windows and `"posix"` otherwise. let os_name = if platform == "win32" { "nt" } else { "posix" }; diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs index 3c408bb588..b9ec5226a6 100644 --- a/crates/ty_python_semantic/src/types/class/known.rs +++ b/crates/ty_python_semantic/src/types/class/known.rs @@ -2058,21 +2058,19 @@ impl<'db> KnownClassLookupError<'db> { #[cfg(test)] mod tests { use super::*; - use crate::db::tests::setup_db; + use crate::db::tests::{TestDbBuilder, setup_db}; use crate::{PythonVersionSource, PythonVersionWithSource}; - use salsa::Setter; use strum::IntoEnumIterator; use ty_module_resolver::resolve_module_confident; + use ty_python_core::TestProgramDb as _; + use ty_python_core::program::{Program, ProgramSettings}; #[test] fn known_class_roundtrip_from_str() { - let mut db = setup_db(); - ty_python_core::program::Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_preview(), - source: PythonVersionSource::default(), - }); + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::latest_preview()) + .build() + .expect("valid TestDb setup"); let python_version = db.python_version(); let resolver_environment = db.program_environment().resolver_environment(&db); for class in KnownClass::iter() { @@ -2101,14 +2099,10 @@ mod tests { #[test] fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_latest_version() { - let mut db = setup_db(); - - ty_python_core::program::Program::get(&db) - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { - version: PythonVersion::latest_ty(), - source: PythonVersionSource::default(), - }); + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::latest_ty()) + .build() + .expect("valid TestDb setup"); let python_version = db.python_version(); let env = db.program_environment(); @@ -2134,7 +2128,7 @@ mod tests { #[test] fn known_class_doesnt_fallback_to_unknown_unexpectedly_on_low_python_version() { - let mut db = setup_db(); + let db = setup_db(); // First, collect the `KnownClass` variants // and sort them according to the version they were added in. @@ -2164,22 +2158,27 @@ mod tests { classes.sort_unstable_by_key(|(_, version)| *version); - let program = ty_python_core::program::Program::get(&db); + let mut program = db.program(); let mut current_version = program.python_version(&db); + let python_platform = program.python_platform(&db).clone(); + let search_paths = program.search_paths(&db).clone(); for (class, version_added) in classes { if version_added != current_version { - program - .set_python_version_with_source(&mut db) - .to(PythonVersionWithSource { + let settings = ProgramSettings { + python_version: PythonVersionWithSource { version: version_added, source: PythonVersionSource::default(), - }); + }, + python_platform: python_platform.clone(), + search_paths: search_paths.clone(), + }; + program = Program::from_settings(&db, settings); current_version = version_added; } // Check the class can be looked up successfully - let env = db.program_environment(); + let env = ProgramEnvironment::from_program(program); class.try_to_class_literal(&db, &env).unwrap(); // We can't call `KnownClass::Tuple.to_instance()`; diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 8170157912..c47d4297f6 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs @@ -74,7 +74,7 @@ impl<'db> ProgramEnvironment<'db> { #[inline] pub fn program(&self, db: &'db dyn Db) -> Program<'db> { let program = match self.environment.get() { - ProgramSource::Program(id) => return ResolverEnvironment::from_id(id), + ProgramSource::Program(id) => return Program::from_id(id), ProgramSource::File(file) => { cold_path(); // The source handle and database share `'db`; re-wrapping the stored ingredient @@ -109,7 +109,7 @@ impl<'db> ProgramEnvironment<'db> { /// Returns the resolver environment used by this operation. #[inline] pub fn resolver_environment(&self, db: &'db dyn Db) -> ResolverEnvironment<'db> { - self.program(db) + self.program(db).resolver_environment(db) } } diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index ad80014d2a..a9929e669e 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -4689,6 +4689,7 @@ pub(super) fn report_invalid_total_ordering_call( /// The function returns `true` if a hint was added, `false` otherwise. pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( db: &dyn Db, + env: &ProgramEnvironment<'_>, diagnostic: &mut Diagnostic, full_submodule_name: &ModuleName, parent_module: Module, @@ -4701,7 +4702,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( return false; } - let program = ty_python_core::program::Program::get(db); + let program = env.program(db); let typeshed_versions = program.search_paths(db).typeshed_versions(); let Some(version_range) = typeshed_versions.exact(full_submodule_name) else { @@ -4721,7 +4722,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( version_range = version_range.diagnostic_display(), )); - add_inferred_python_version_hint_to_diagnostic(db, diagnostic, "resolving modules"); + add_inferred_python_version_hint_to_diagnostic(db, env, diagnostic, "resolving modules"); true } @@ -4778,7 +4779,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( // TODO: determine what version they need to be on // TODO: also mention the platform we're assuming // TODO: determine what platform they need to be on - add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, action); + add_inferred_python_version_hint_to_diagnostic(db, env, &mut diagnostic, action); } pub(super) fn report_invalid_concatenate_last_arg<'db>( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b27e76a486..acad198bc3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -10162,7 +10162,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "`{id}` was added as a builtin in Python 3.{version_added_to_builtins}" )); add_inferred_python_version_hint_to_diagnostic( - self.db(), + db, + self.program_environment(), &mut diagnostic, "resolving types", ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/imports.rs b/crates/ty_python_semantic/src/types/infer/builder/imports.rs index de63f9295f..78dab4f12e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/imports.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/imports.rs @@ -86,6 +86,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); add_inferred_python_version_hint_to_diagnostic( db, + self.program_environment(), &mut diagnostic, "resolving modules", ); @@ -514,6 +515,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(full_submodule_name) = full_submodule_name { submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions( db, + self.program_environment(), &mut diagnostic, &full_submodule_name, module, @@ -635,7 +637,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); hint_if_stdlib_submodule_exists_on_other_versions( - db, + self.db(), + self.program_environment(), &mut diagnostic, &full_submodule_name, module, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 778b7cf259..0ed17b16e5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -355,7 +355,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.type_expression_context() )); add_inferred_python_version_hint_to_diagnostic( - self.db(), + db, + self.program_environment(), &mut diagnostic, "inferring types", ); diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index cbea2b2190..0317728238 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -8,11 +8,14 @@ use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::DbWithWritableSystem as _; use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run}; use ruff_python_ast::PythonVersion; -use ty_module_resolver::ResolverEnvironment; +use salsa::plumbing::AsId; use ty_python_core::definition::Definition; -use ty_python_core::program::Program as ProjectProgram; +use ty_python_core::program::{Program, ProgramSettings}; use ty_python_core::scope::FileScopeId; -use ty_python_core::{ProgramFile, global_scope, place_table, semantic_index, use_def_map}; +use ty_python_core::{ + ProgramFile, TestProgramDb as _, global_scope, place_table, semantic_index, use_def_map, +}; +use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use super::*; @@ -111,16 +114,38 @@ fn same_file_at_different_python_versions() -> anyhow::Result<()> { db.write_dedented("src/py312_dependency.py", "value: int = 312")?; let file = system_path_to_file(&db, "src/main.py").expect("file to exist"); - let search_paths = ProjectProgram::get(&db).search_paths(&db); + let default_program = db.program(); + let search_paths = default_program.search_paths(&db).clone(); + let python_platform = default_program.python_platform(&db).clone(); let py311 = ProgramFile::new( &db, file, - ResolverEnvironment::new(&db, PythonVersion::PY311, search_paths), + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY311, + source: PythonVersionSource::Default, + }, + python_platform: python_platform.clone(), + search_paths: search_paths.clone(), + }, + ), ); let py312 = ProgramFile::new( &db, file, - ResolverEnvironment::new(&db, PythonVersion::PY312, search_paths), + Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY312, + source: PythonVersionSource::Default, + }, + python_platform, + search_paths, + }, + ), ); let check = |file, expected_type, expect_invalid_syntax, expect_unresolved_import| { @@ -161,6 +186,52 @@ fn same_file_at_different_python_versions() -> anyhow::Result<()> { Ok(()) } +#[test] +fn program_file_changes_with_python_version() -> anyhow::Result<()> { + let db = TestDbBuilder::new() + .with_python_version(PythonVersion::PY311) + .with_file("src/main.py", "type Alias = int") + .build()?; + let file = system_path_to_file(&db, "src/main.py").expect("file to exist"); + let program = db.program(); + let (program_file_id, py311) = { + let program_file = program.program_file(&db, file); + (program_file.as_id(), program_file.python_file(&db).as_id()) + }; + + let equivalent_program = Program::from_settings( + &db, + ProgramSettings { + python_version: program.python_version_with_source(&db).clone(), + python_platform: program.python_platform(&db).clone(), + search_paths: program.search_paths(&db).clone(), + }, + ); + assert_eq!(program, equivalent_program); + assert_eq!( + program_file_id, + equivalent_program.program_file(&db, file).as_id() + ); + + let py312_program = Program::from_settings( + &db, + ProgramSettings { + python_version: PythonVersionWithSource { + version: PythonVersion::PY312, + source: PythonVersionSource::Default, + }, + python_platform: program.python_platform(&db).clone(), + search_paths: program.search_paths(&db).clone(), + }, + ); + + let program_file = py312_program.program_file(&db, file); + assert_ne!(program_file_id, program_file.as_id()); + assert_eq!(program_file.python_version(&db), PythonVersion::PY312); + assert_ne!(py311, program_file.python_file(&db).as_id()); + Ok(()) +} + #[test] fn expected_types_are_collected_only_for_open_files() -> anyhow::Result<()> { let has_expected_type = |open_file: bool| -> anyhow::Result { diff --git a/crates/ty_python_semantic/src/types/tests.rs b/crates/ty_python_semantic/src/types/tests.rs index 8fafadcf43..df9c2a5e84 100644 --- a/crates/ty_python_semantic/src/types/tests.rs +++ b/crates/ty_python_semantic/src/types/tests.rs @@ -1,14 +1,14 @@ use super::*; -use crate::Db; -use crate::ProgramEnvironment; use crate::db::tests::{TestDbBuilder, setup_db}; use crate::place::{typing_extensions_symbol, typing_symbol}; use crate::types::type_alias::PEP695TypeAliasType; +use crate::{Db, ProgramEnvironment}; use ruff_db::system::DbWithWritableSystem as _; use ruff_python_ast as ast; use ruff_python_ast::PythonVersion; use test_case::test_case; -use ty_python_core::ProgramFile; +use ty_python_core::program::Program; +use ty_python_core::{ProgramFile, TestProgramDb as _}; /// Explicitly test for Python version <3.13 and >=3.13, to ensure that /// the fallback to `typing_extensions` is working correctly. @@ -73,23 +73,20 @@ fn oscillating_generic_alias_cycle_recover<'db>( cycle: &salsa::Cycle, previous: &Type<'db>, current: Type<'db>, + program: Program<'db>, ) -> Type<'db> { - let env = ProgramEnvironment::from_program( - ty_python_core::program::Program::get(db).resolver_environment(db), - ); + let env = ProgramEnvironment::from_program(program); current.cycle_normalized(db, &env, *previous, cycle) } #[salsa::tracked( returns(copy), - cycle_initial=|_, id| Type::divergent(id), + cycle_initial=|_, id, _| Type::divergent(id), cycle_fn=oscillating_generic_alias_cycle_recover, )] -fn oscillating_generic_alias(db: &dyn Db) -> Type<'_> { - let env = ProgramEnvironment::from_program( - ty_python_core::program::Program::get(db).resolver_environment(db), - ); - let previous = oscillating_generic_alias(db); +fn oscillating_generic_alias<'db>(db: &'db dyn Db, program: Program<'db>) -> Type<'db> { + let env = ProgramEnvironment::from_program(program); + let previous = oscillating_generic_alias(db, program); let argument = if let Type::GenericAlias(alias) = previous && alias.specialization(db).types(db) == [Type::unknown()] { @@ -104,7 +101,7 @@ fn oscillating_generic_alias(db: &dyn Db) -> Type<'_> { #[test] fn generic_alias_cycle_recovery_normalizes_same_origin_unknown_oscillation() { let db = setup_db(); - let Type::GenericAlias(alias) = oscillating_generic_alias(&db) else { + let Type::GenericAlias(alias) = oscillating_generic_alias(&db, db.program()) else { panic!("cycle recovery should preserve the generic alias"); }; diff --git a/crates/ty_python_semantic/tests/corpus.rs b/crates/ty_python_semantic/tests/corpus.rs index 3c3575c61a..1c7db3c6b6 100644 --- a/crates/ty_python_semantic/tests/corpus.rs +++ b/crates/ty_python_semantic/tests/corpus.rs @@ -1,23 +1,18 @@ use std::sync::Arc; use anyhow::{Context, anyhow}; -use ruff_db::Db; use ruff_db::files::{File, Files, system_path_to_file}; use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem}; use ruff_db::vendored::VendoredFileSystem; -use ruff_python_ast::PythonVersion; -use ty_module_resolver::SearchPathSettings; -use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::program::ProgramSettings; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::pull_types::pull_types; use ty_python_semantic::{AnalysisSettings, Db as _, check_file_unwrap, default_lint_registry}; -use ty_site_packages::{PythonVersionSource, PythonVersionWithSource}; use ruff_db::diagnostic::Diagnostic; use test_case::test_case; -use ty_python_core::{Db as _, ProgramFile}; +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; fn get_cargo_workspace_root() -> anyhow::Result<&'static SystemPath> { SystemPath::new(env!("CARGO_MANIFEST_DIR")) @@ -149,35 +144,23 @@ pub struct CorpusDb { system: TestSystem, vendored: VendoredFileSystem, analysis_settings: Arc, + program_settings: ProgramSettings, } impl CorpusDb { #[expect(clippy::new_without_default)] pub fn new() -> Self { - let db = Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); + Self { storage: salsa::Storage::new(None), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, rule_selection: RuleSelection::from_registry(default_lint_registry()), files: Files::default(), analysis_settings: Arc::new(AnalysisSettings::default()), - }; - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource { - version: PythonVersion::latest_ty(), - source: PythonVersionSource::default(), - }, - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings::new(vec![]) - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .unwrap(), - }, - ); - - db + program_settings, + } } } @@ -216,6 +199,13 @@ impl ty_python_core::Db for CorpusDb { } } +#[salsa::db] +impl TestProgramDb for CorpusDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } +} + #[salsa::db] impl ty_python_semantic::Db for CorpusDb { fn check_file(&self, file: File) -> Vec { @@ -227,7 +217,7 @@ impl ty_python_semantic::Db for CorpusDb { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.program().program_file(self, file) } fn rule_selection(&self, _file: File) -> &RuleSelection { diff --git a/crates/ty_server/src/server/api/requests/execute_command.rs b/crates/ty_server/src/server/api/requests/execute_command.rs index 18f4f96f1f..ed0c2234c6 100644 --- a/crates/ty_server/src/server/api/requests/execute_command.rs +++ b/crates/ty_server/src/server/api/requests/execute_command.rs @@ -12,7 +12,6 @@ use std::fmt::{self, Write}; use std::str::FromStr; use ty_module_resolver::ModuleResolveMode; use ty_project::Db as _; -use ty_python_core::program::Program; pub(crate) struct ExecuteCommand; @@ -67,7 +66,7 @@ fn debug_information(session: &Session) -> crate::Result { for db in session.project_dbs() { writeln!(buffer, "Project at {}", db.project().root(db))?; - let program = Program::get(db); + let program = db.project().program(db); writeln!(buffer, "Program:")?; writeln!( buffer, diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 017b0a3edf..808e9ecc0d 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -27,7 +27,7 @@ use ty_project::watch::{ChangeEvent, CreatedKind}; use ty_project::{ChangeResult, Db as _, ProjectDatabase, ProjectMetadata}; use index::DocumentError; -use ty_python_core::program::{Program, UseDefaultStrategy}; +use ty_python_core::program::UseDefaultStrategy; pub(crate) use self::options::InitializationOptions; pub use self::options::{ClientOptions, DiagnosticMode, GlobalOptions, WorkspaceOptions}; @@ -1089,7 +1089,7 @@ impl Session { .flat_map(|db| { ty_module_resolver::system_module_search_paths( db, - Program::get(db).resolver_environment(db), + db.project().program(db).resolver_environment(db), ) .map(move |path| (db, path)) }) diff --git a/crates/ty_server/tests/e2e/commands.rs b/crates/ty_server/tests/e2e/commands.rs index 5725f6be6b..4f641562f3 100644 --- a/crates/ty_server/tests/e2e/commands.rs +++ b/crates/ty_server/tests/e2e/commands.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use lsp_types::{ExecuteCommandParams, ExecuteCommandRequest, WorkDoneProgressParams}; use ruff_db::system::SystemPath; @@ -49,6 +49,21 @@ python-platform = \"linux\" .as_str() .expect("debug command to return a string response"); + let (before_structs, salsa_structs) = response + .split_once("=======SALSA STRUCTS=======\n") + .context("debug response missing Salsa structs section")?; + let (salsa_structs, after_structs) = salsa_structs + .split_once("=======SALSA QUERIES=======\n") + .context("debug response missing Salsa queries section")?; + + // The production report orders structs by memory usage, which varies between platforms. + let mut salsa_structs = salsa_structs.lines().collect::>(); + salsa_structs.sort_unstable(); + let response = format!( + "{before_structs}=======SALSA STRUCTS=======\n{}\n=======SALSA QUERIES=======\n{after_structs}", + salsa_structs.join("\n") + ); + let mut settings = insta::Settings::clone_current(); settings.add_filter(r"\b[0-9]+.[0-9]+MB\b", "[X.XXMB]"); settings.add_filter(r"Workspace .+\)", "Workspace XXX"); diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index b93f2b699c..45c48b2f97 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -180,14 +180,16 @@ Settings: Settings { Memory report: =======SALSA STRUCTS======= -`Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 -`ResolverEnvironment` metadata=[X.XXMB] fields=[X.XXMB] count=1 -`Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 `FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=1 `ModuleResolveModeIngredient` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`ResolverEnvironment` metadata=[X.XXMB] fields=[X.XXMB] count=1 =======SALSA QUERIES======= `dynamic_resolution_paths -> alloc::vec::Vec` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Project::program_ -> ty_python_core::program::Program<'_>` + metadata=[X.XXMB] fields=[X.XXMB] count=1 =======SALSA SUMMARY======= TOTAL MEMORY USAGE: [X.XXMB] struct metadata = [X.XXMB] diff --git a/crates/ty_site_packages/src/version.rs b/crates/ty_site_packages/src/version.rs index 8a643a3e82..6832650350 100644 --- a/crates/ty_site_packages/src/version.rs +++ b/crates/ty_site_packages/src/version.rs @@ -10,7 +10,7 @@ use ruff_python_ast::PythonVersion; use ruff_text_size::TextRange; /// The source of the Python version. -#[derive(Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Default, get_size2::GetSize)] pub enum PythonVersionSource { /// Value loaded from a project's configuration file. ConfigFile(PythonVersionFileSource), @@ -49,7 +49,7 @@ pub enum PythonVersionSource { /// Information regarding the file and [`TextRange`] of the configuration /// from which we inferred the Python version. -#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, get_size2::GetSize)] pub struct PythonVersionFileSource { path: Arc, range: Option, @@ -72,7 +72,7 @@ impl PythonVersionFileSource { } /// A Python version with its source. -#[derive(Eq, PartialEq, Debug, Clone, get_size2::GetSize)] +#[derive(Eq, PartialEq, Hash, Debug, Clone, get_size2::GetSize)] pub struct PythonVersionWithSource { pub version: PythonVersion, pub source: PythonVersionSource, diff --git a/crates/ty_test/src/db.rs b/crates/ty_test/src/db.rs index af2cf8255b..51995c81f2 100644 --- a/crates/ty_test/src/db.rs +++ b/crates/ty_test/src/db.rs @@ -15,8 +15,8 @@ use std::borrow::Cow; use std::sync::Arc; use tempfile::TempDir; use ty_module_resolver::ModuleGlobSetBuilder; -use ty_python_core::program::Program; -use ty_python_core::{Db as _, ProgramFile}; +use ty_python_core::program::ProgramSettings; +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; use ty_python_semantic::lint::{LintRegistry, RuleSelection}; use ty_python_semantic::{ AnalysisSettings, Db as SemanticDb, check_file_unwrap, default_lint_registry, @@ -34,6 +34,8 @@ pub(crate) struct Db { impl Db { pub(crate) fn setup() -> Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); let mut db = Self { system: MdtestSystem::in_memory(), storage: salsa::Storage::new(Some(Box::new({ @@ -41,12 +43,12 @@ impl Db { tracing::trace!("event: {:?}", event); } }))), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), settings: None, }; - db.settings = Some(Settings::new(&db)); + db.settings = Some(Settings::new(&db, program_settings)); db } @@ -54,6 +56,14 @@ impl Db { self.settings.unwrap() } + pub(crate) fn update_program(&mut self, settings: ProgramSettings) { + let db_settings = self.settings(); + if db_settings.program(self) != &settings { + settings.search_paths.try_register_static_roots(self); + db_settings.set_program(self).to(settings); + } + } + pub(crate) fn set_verbosity(&mut self, verbose: bool) { self.settings().set_verbose(self).to(verbose); } @@ -133,7 +143,7 @@ impl SemanticDb for Db { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.program().program_file(self, file) } fn rule_selection(&self, file: File) -> &RuleSelection { @@ -161,6 +171,13 @@ impl SemanticDb for Db { } } +#[salsa::db] +impl TestProgramDb for Db { + fn program_settings(&self) -> &ProgramSettings { + self.settings().program(self) + } +} + #[salsa::db] impl salsa::Database for Db {} @@ -214,6 +231,8 @@ impl FileSettings { #[salsa::input(debug)] struct Settings { + #[returns(ref)] + program: ProgramSettings, #[default] #[returns(ref)] analysis: AnalysisSettings, diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 65a6875dc4..7e9c793191 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -20,8 +20,9 @@ use std::fmt::Write; use ty_module_resolver::{ Module, SearchPath, SearchPathSettings, list_modules, resolve_module_confident, }; +use ty_python_core::TestProgramDb as _; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; +use ty_python_core::program::{FallibleStrategy, ProgramSettings}; use ty_python_semantic::pull_types::pull_types; use ty_python_semantic::types::UNDEFINED_REVEAL; use ty_python_semantic::{ @@ -304,7 +305,7 @@ fn run_test( .expect("Failed to resolve search path settings"), }; - Program::init_or_update(db, settings); + db.update_program(settings); db.update_analysis_options(configuration.analysis.as_ref()); db.update_mdtest_rule_selection(configuration.rules.as_ref(), options.default_error_rule); db.set_verbosity(test.configuration().verbose()); @@ -493,7 +494,7 @@ struct ModuleInconsistency<'db> { /// `list_module`. fn run_module_resolution_consistency_test(db: &db::Db) -> Result<(), Vec>> { let mut errs = vec![]; - let environment = Program::get(db).resolver_environment(db); + let environment = db.program().resolver_environment(db); for from_list in list_modules(db, environment).iter().copied() { // TODO: For now list_modules does not partake in desperate module resolution so // only compare against confident module resolution. diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index baeb5ea85f..19d61f6f7c 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -2,7 +2,6 @@ use std::any::Any; use js_sys::{Error, JsString}; use ruff_db::Db as _; -use ruff_db::PythonFile; use ruff_db::diagnostic::{self, DisplayDiagnosticConfig}; use ruff_db::files::{File, FilePath, FileRange, system_path_to_file, vendored_path_to_file}; use ruff_db::source::{SourceText, line_index, source_text}; @@ -28,7 +27,7 @@ use ty_project::metadata::options::Options; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use ty_project::{CheckMode, ProjectMetadata}; use ty_project::{Db, ProjectDatabase, SemanticDb as _}; -use ty_python_core::program::{FallibleStrategy, Program}; +use ty_python_core::program::FallibleStrategy; use ty_python_semantic::ProgramEnvironment; use wasm_bindgen::prelude::*; @@ -171,20 +170,23 @@ impl Workspace { let (program_settings, program_settings_diagnostics) = merged_options .to_program_settings(&self.system, self.db.vendored(), &FallibleStrategy) .map_err(into_error)?; - Program::get(&self.db).update_from_settings(&mut self.db, program_settings); + self.db + .project() + .update_program(&mut self.db, program_settings); - let (settings, settings_diagnostics) = merged_options + let (settings, mut settings_diagnostics) = merged_options .to_settings(&self.db, &FallibleStrategy) .map_err(into_error)?; - - self.db.project().reload( - &mut self.db, - project, - Some(settings), - settings_diagnostics, - program_settings_diagnostics, + settings_diagnostics.extend( + program_settings_diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_diagnostic(&self.db)), ); + self.db + .project() + .reload(&mut self.db, project, Some(settings), settings_diagnostics); + Ok(()) } @@ -294,7 +296,7 @@ impl Workspace { pub fn parsed(&self, file_id: &FileHandle) -> Result { let parsed = ruff_db::parsed::parsed_module( &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), + self.db.program_file(file_id.file).python_file(&self.db), ) .load(&self.db); @@ -309,7 +311,7 @@ impl Workspace { pub fn tokens(&self, file_id: &FileHandle) -> Result { let parsed = ruff_db::parsed::parsed_module( &self.db, - PythonFile::new(&self.db, file_id.file, self.db.python_version()), + self.db.program_file(file_id.file).python_file(&self.db), ) .load(&self.db); diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index a7e92f5efc..9767b80f2d 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -115,7 +115,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -131,7 +131,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn", + "syn 2.0.119", ] [[package]] @@ -287,7 +287,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -422,7 +422,7 @@ dependencies = [ "defmt-parser", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -442,7 +442,7 @@ checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -453,7 +453,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -558,7 +558,7 @@ checksum = "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d" dependencies = [ "attribute-derive", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -816,7 +816,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -873,7 +873,7 @@ checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -940,7 +940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0903173ea316c34a44d0497161e04d9210af44f5f5e89bf2f55d9a254c9a0e8d" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -982,7 +982,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1093,21 +1093,18 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-absolutize" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +checksum = "f808742975794703469f67a28dd14b1d1009a1743c18b0353b4b951dbb0068ad" dependencies = [ "path-dedot", ] [[package]] name = "path-dedot" -version = "3.1.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" -dependencies = [ - "once_cell", -] +checksum = "03351d0f1c066c114015408dc6a3e101f080fc03ef9d8d799aa58ec760cac5a6" [[package]] name = "path-slash" @@ -1371,7 +1368,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1513,7 +1510,7 @@ dependencies = [ [[package]] name = "ruff_annotate_snippets" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anstyle", "memchr", @@ -1522,7 +1519,7 @@ dependencies = [ [[package]] name = "ruff_cache" -version = "0.0.5" +version = "0.0.7" dependencies = [ "char_str", "filetime", @@ -1535,7 +1532,7 @@ dependencies = [ [[package]] name = "ruff_db" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anstyle", "arc-swap", @@ -1573,7 +1570,7 @@ dependencies = [ [[package]] name = "ruff_diagnostics" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", "is-macro", @@ -1583,7 +1580,7 @@ dependencies = [ [[package]] name = "ruff_formatter" -version = "0.0.5" +version = "0.0.7" dependencies = [ "drop_bomb", "ruff_cache", @@ -1598,7 +1595,7 @@ dependencies = [ [[package]] name = "ruff_index" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", "ruff_macros", @@ -1607,7 +1604,7 @@ dependencies = [ [[package]] name = "ruff_linter" -version = "0.15.22" +version = "0.16.1" dependencies = [ "aho-corasick", "anyhow", @@ -1665,7 +1662,7 @@ dependencies = [ [[package]] name = "ruff_macros" -version = "0.0.5" +version = "0.0.7" dependencies = [ "heck", "itertools 0.15.0", @@ -1673,19 +1670,19 @@ dependencies = [ "quote", "regex", "ruff_python_trivia", - "syn", + "syn 3.0.3", ] [[package]] name = "ruff_memory_usage" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", ] [[package]] name = "ruff_notebook" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anyhow", "rand 0.10.2", @@ -1700,7 +1697,7 @@ dependencies = [ [[package]] name = "ruff_python_ast" -version = "0.0.5" +version = "0.0.7" dependencies = [ "aho-corasick", "arrayvec", @@ -1724,7 +1721,7 @@ dependencies = [ [[package]] name = "ruff_python_codegen" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ruff_python_ast", "ruff_python_literal", @@ -1735,7 +1732,7 @@ dependencies = [ [[package]] name = "ruff_python_formatter" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anyhow", "clap", @@ -1763,7 +1760,7 @@ dependencies = [ [[package]] name = "ruff_python_importer" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anyhow", "ruff_diagnostics", @@ -1776,7 +1773,7 @@ dependencies = [ [[package]] name = "ruff_python_index" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ruff_python_ast", "ruff_python_trivia", @@ -1786,7 +1783,7 @@ dependencies = [ [[package]] name = "ruff_python_literal" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "icu_properties", @@ -1796,7 +1793,7 @@ dependencies = [ [[package]] name = "ruff_python_parser" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "bstr", @@ -1816,7 +1813,7 @@ dependencies = [ [[package]] name = "ruff_python_semantic" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "is-macro", @@ -1833,7 +1830,7 @@ dependencies = [ [[package]] name = "ruff_python_stdlib" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "unicode-ident", @@ -1841,7 +1838,7 @@ dependencies = [ [[package]] name = "ruff_python_trivia" -version = "0.0.5" +version = "0.0.7" dependencies = [ "itertools 0.15.0", "ruff_source_file", @@ -1852,7 +1849,7 @@ dependencies = [ [[package]] name = "ruff_ranged_value" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ruff_db", "ruff_text_size", @@ -1862,7 +1859,7 @@ dependencies = [ [[package]] name = "ruff_source_file" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", "memchr", @@ -1872,7 +1869,7 @@ dependencies = [ [[package]] name = "ruff_text_size" -version = "0.0.5" +version = "0.0.7" dependencies = [ "get-size2", "serde", @@ -1947,7 +1944,7 @@ checksum = "ec5c48c5a4a53a6e2be9762f56566f1325d4394c011c00b3ea86ba2d13411e71" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1998,7 +1995,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2101,7 +2098,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2121,6 +2118,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2129,7 +2137,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2173,7 +2181,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2184,7 +2192,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2294,7 +2302,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2308,7 +2316,7 @@ dependencies = [ [[package]] name = "ty_combine" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ordermap", "ruff_db", @@ -2318,12 +2326,13 @@ dependencies = [ [[package]] name = "ty_module_resolver" -version = "0.0.5" +version = "0.0.7" dependencies = [ "anyhow", "camino", "compact_str", "get-size2", + "ordermap", "regex", "regex-syntax", "ruff_db", @@ -2340,7 +2349,7 @@ dependencies = [ [[package]] name = "ty_python_core" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "bitvec", @@ -2368,7 +2377,7 @@ dependencies = [ [[package]] name = "ty_python_semantic" -version = "0.0.5" +version = "0.0.7" dependencies = [ "bitflags 2.13.0", "char_str", @@ -2408,7 +2417,7 @@ dependencies = [ [[package]] name = "ty_site_packages" -version = "0.0.5" +version = "0.0.7" dependencies = [ "camino", "colored", @@ -2428,14 +2437,14 @@ dependencies = [ [[package]] name = "ty_static" -version = "0.0.5" +version = "0.0.7" dependencies = [ "ruff_macros", ] [[package]] name = "ty_vendored" -version = "0.0.5" +version = "0.0.7" dependencies = [ "path-slash", "ruff_db", @@ -2618,7 +2627,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -2711,7 +2720,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -2732,7 +2741,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2752,7 +2761,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -2786,7 +2795,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 14cef7c28c..3a41d55495 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -26,7 +26,7 @@ ruff_python_formatter = { path = "../crates/ruff_python_formatter" } ruff_text_size = { path = "../crates/ruff_text_size" } ty_module_resolver = { path = "../crates/ty_module_resolver" } -ty_python_semantic = { path = "../crates/ty_python_semantic" } +ty_python_semantic = { path = "../crates/ty_python_semantic", features = ["testing"] } ty_vendored = { path = "../crates/ty_vendored" } ty_python_core = { path = "../crates/ty_python_core" } diff --git a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs index cbe62a7a19..6141d7c74a 100644 --- a/fuzz/fuzz_targets/ty_check_invalid_syntax.rs +++ b/fuzz/fuzz_targets/ty_check_invalid_syntax.rs @@ -17,8 +17,8 @@ use ruff_db::vendored::VendoredFileSystem; use ruff_python_parser::{Mode, ParseOptions, parse_unchecked}; use ty_module_resolver::{Db as ModuleResolverDb, SearchPathSettings}; use ty_python_core::platform::PythonPlatform; -use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings}; -use ty_python_core::{Db as _, ProgramFile}; +use ty_python_core::program::{FallibleStrategy, ProgramSettings}; +use ty_python_core::{Db as _, ProgramFile, TestProgramDb}; use ty_python_semantic::lint::LintRegistry; use ty_python_semantic::types::check_types; use ty_python_semantic::{ @@ -38,22 +38,43 @@ struct TestDb { vendored: VendoredFileSystem, rule_selection: Arc, analysis_settings: Arc, + program_settings: ProgramSettings, } impl TestDb { fn new() -> Self { - Self { + let vendored = ty_vendored::file_system().clone(); + let program_settings = ProgramSettings::empty(&vendored); + let mut db = Self { storage: salsa::Storage::new(Some(Box::new({ move |event| { tracing::trace!("event: {:?}", event); } }))), system: TestSystem::default(), - vendored: ty_vendored::file_system().clone(), + vendored, files: Files::default(), rule_selection: RuleSelection::from_registry(default_lint_registry()).into(), analysis_settings: AnalysisSettings::default().into(), - } + program_settings, + }; + + let src_root = SystemPathBuf::from("/src"); + db.memory_file_system() + .create_directory_all(&src_root) + .unwrap(); + + let program_settings = ProgramSettings { + python_version: PythonVersionWithSource::default(), + python_platform: PythonPlatform::default(), + search_paths: SearchPathSettings::new(vec![src_root]) + .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) + .expect("Valid search path settings"), + }; + program_settings.search_paths.try_register_static_roots(&db); + db.program_settings = program_settings; + + db } } @@ -103,7 +124,7 @@ impl SemanticDb for TestDb { } fn program_file(&self, file: File) -> ProgramFile<'_> { - Program::get(self).program_file(self, file) + self.program().program_file(self, file) } fn rule_selection(&self, _file: File) -> &RuleSelection { @@ -132,30 +153,15 @@ impl SemanticDb for TestDb { } #[salsa::db] -impl salsa::Database for TestDb {} - -fn setup_db() -> TestDb { - let db = TestDb::new(); - - let src_root = SystemPathBuf::from("/src"); - db.memory_file_system() - .create_directory_all(&src_root) - .unwrap(); - - Program::from_settings( - &db, - ProgramSettings { - python_version: PythonVersionWithSource::default(), - python_platform: PythonPlatform::default(), - search_paths: SearchPathSettings::new(vec![src_root]) - .to_search_paths(db.system(), db.vendored(), &FallibleStrategy) - .expect("Valid search path settings"), - }, - ); - - db +impl TestProgramDb for TestDb { + fn program_settings(&self) -> &ProgramSettings { + &self.program_settings + } } +#[salsa::db] +impl salsa::Database for TestDb {} + static TEST_DB: OnceLock> = OnceLock::new(); fn do_fuzz(case: &[u8]) -> Corpus { @@ -169,7 +175,7 @@ fn do_fuzz(case: &[u8]) -> Corpus { } let mut db = TEST_DB - .get_or_init(|| Mutex::new(setup_db())) + .get_or_init(|| Mutex::new(TestDb::new())) .lock() .unwrap();