Eagerly validate search paths (#12783)

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Micha Reiser 2024-08-12 09:46:59 +02:00 committed by GitHub
parent fabf19fdc9
commit a99a45868c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 355 additions and 277 deletions

View file

@ -184,7 +184,7 @@ fn run() -> anyhow::Result<ExitStatus> {
// TODO: Use the `program_settings` to compute the key for the database's persistent // TODO: Use the `program_settings` to compute the key for the database's persistent
// cache and load the cache if it exists. // cache and load the cache if it exists.
let mut db = RootDatabase::new(workspace_metadata, program_settings, system); let mut db = RootDatabase::new(workspace_metadata, program_settings, system)?;
let (main_loop, main_loop_cancellation_token) = MainLoop::new(); let (main_loop, main_loop_cancellation_token) = MainLoop::new();

View file

@ -4,7 +4,6 @@ use std::io::Write;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use salsa::Setter;
use red_knot_python_semantic::{ use red_knot_python_semantic::{
resolve_module, ModuleName, Program, ProgramSettings, PythonVersion, SearchPathSettings, resolve_module, ModuleName, Program, ProgramSettings, PythonVersion, SearchPathSettings,
@ -26,6 +25,7 @@ struct TestCase {
/// We need to hold on to it in the test case or the temp files get deleted. /// We need to hold on to it in the test case or the temp files get deleted.
_temp_dir: tempfile::TempDir, _temp_dir: tempfile::TempDir,
root_dir: SystemPathBuf, root_dir: SystemPathBuf,
search_path_settings: SearchPathSettings,
} }
impl TestCase { impl TestCase {
@ -108,18 +108,20 @@ impl TestCase {
fn update_search_path_settings( fn update_search_path_settings(
&mut self, &mut self,
f: impl FnOnce(&SearchPathSettings) -> SearchPathSettings, f: impl FnOnce(&SearchPathSettings) -> SearchPathSettings,
) { ) -> anyhow::Result<()> {
let program = Program::get(self.db()); let program = Program::get(self.db());
let search_path_settings = program.search_paths(self.db());
let new_settings = f(search_path_settings); let new_settings = f(&self.search_path_settings);
program.set_search_paths(&mut self.db).to(new_settings); program.update_search_paths(&mut self.db, new_settings.clone())?;
self.search_path_settings = new_settings;
if let Some(watcher) = &mut self.watcher { if let Some(watcher) = &mut self.watcher {
watcher.update(&self.db); watcher.update(&self.db);
assert!(!watcher.has_errored_paths()); assert!(!watcher.has_errored_paths());
} }
Ok(())
} }
fn collect_package_files(&self, path: &SystemPath) -> Vec<File> { fn collect_package_files(&self, path: &SystemPath) -> Vec<File> {
@ -221,13 +223,13 @@ where
let system = OsSystem::new(&workspace_path); let system = OsSystem::new(&workspace_path);
let workspace = WorkspaceMetadata::from_path(&workspace_path, &system)?; let workspace = WorkspaceMetadata::from_path(&workspace_path, &system)?;
let search_paths = create_search_paths(&root_path, workspace.root()); let search_path_settings = create_search_paths(&root_path, workspace.root());
for path in search_paths for path in search_path_settings
.extra_paths .extra_paths
.iter() .iter()
.chain(search_paths.site_packages.iter()) .chain(search_path_settings.site_packages.iter())
.chain(search_paths.custom_typeshed.iter()) .chain(search_path_settings.custom_typeshed.iter())
{ {
std::fs::create_dir_all(path.as_std_path()) std::fs::create_dir_all(path.as_std_path())
.with_context(|| format!("Failed to create search path '{path}'"))?; .with_context(|| format!("Failed to create search path '{path}'"))?;
@ -235,10 +237,10 @@ where
let settings = ProgramSettings { let settings = ProgramSettings {
target_version: PythonVersion::default(), target_version: PythonVersion::default(),
search_paths, search_paths: search_path_settings.clone(),
}; };
let db = RootDatabase::new(workspace, settings, system); let db = RootDatabase::new(workspace, settings, system)?;
let (sender, receiver) = crossbeam::channel::unbounded(); let (sender, receiver) = crossbeam::channel::unbounded();
let watcher = directory_watcher(move |events| sender.send(events).unwrap()) let watcher = directory_watcher(move |events| sender.send(events).unwrap())
@ -253,6 +255,7 @@ where
watcher: Some(watcher), watcher: Some(watcher),
_temp_dir: temp_dir, _temp_dir: temp_dir,
root_dir: root_path, root_dir: root_path,
search_path_settings,
}; };
// Sometimes the file watcher reports changes for events that happened before the watcher was started. // Sometimes the file watcher reports changes for events that happened before the watcher was started.
@ -737,7 +740,8 @@ fn add_search_path() -> anyhow::Result<()> {
case.update_search_path_settings(|settings| SearchPathSettings { case.update_search_path_settings(|settings| SearchPathSettings {
site_packages: vec![site_packages.clone()], site_packages: vec![site_packages.clone()],
..settings.clone() ..settings.clone()
}); })
.expect("Search path settings to be valid");
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?; std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
@ -767,7 +771,8 @@ fn remove_search_path() -> anyhow::Result<()> {
case.update_search_path_settings(|settings| SearchPathSettings { case.update_search_path_settings(|settings| SearchPathSettings {
site_packages: vec![], site_packages: vec![],
..settings.clone() ..settings.clone()
}); })
.expect("Search path settings to be valid");
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?; std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;

View file

@ -17,6 +17,7 @@ ruff_python_ast = { workspace = true }
ruff_python_stdlib = { workspace = true } ruff_python_stdlib = { workspace = true }
ruff_text_size = { workspace = true } ruff_text_size = { workspace = true }
anyhow = { workspace = true }
bitflags = { workspace = true } bitflags = { workspace = true }
camino = { workspace = true } camino = { workspace = true }
compact_str = { workspace = true } compact_str = { workspace = true }

View file

@ -2,11 +2,13 @@ use std::iter::FusedIterator;
pub(crate) use module::Module; pub(crate) use module::Module;
pub use resolver::resolve_module; pub use resolver::resolve_module;
pub(crate) use resolver::SearchPaths;
use ruff_db::system::SystemPath; use ruff_db::system::SystemPath;
pub use typeshed::vendored_typeshed_stubs; pub use typeshed::vendored_typeshed_stubs;
use crate::module_resolver::resolver::search_paths;
use crate::Db; use crate::Db;
use resolver::{module_resolution_settings, SearchPathIterator}; use resolver::SearchPathIterator;
mod module; mod module;
mod path; mod path;
@ -20,7 +22,7 @@ mod testing;
/// Returns an iterator over all search paths pointing to a system path /// Returns an iterator over all search paths pointing to a system path
pub fn system_module_search_paths(db: &dyn Db) -> SystemModuleSearchPathsIter { pub fn system_module_search_paths(db: &dyn Db) -> SystemModuleSearchPathsIter {
SystemModuleSearchPathsIter { SystemModuleSearchPathsIter {
inner: module_resolution_settings(db).search_paths(db), inner: search_paths(db),
} }
} }

View file

@ -7,12 +7,13 @@ use ruff_db::files::{File, FilePath, FileRootKind};
use ruff_db::system::{DirectoryEntry, SystemPath, SystemPathBuf}; use ruff_db::system::{DirectoryEntry, SystemPath, SystemPathBuf};
use ruff_db::vendored::VendoredPath; use ruff_db::vendored::VendoredPath;
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::{Program, SearchPathSettings};
use super::module::{Module, ModuleKind}; use super::module::{Module, ModuleKind};
use super::path::{ModulePath, SearchPath, SearchPathValidationError}; use super::path::{ModulePath, SearchPath, SearchPathValidationError};
use super::state::ResolverState; use super::state::ResolverState;
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::{Program, PythonVersion, SearchPathSettings};
/// Resolves a module name to a module. /// Resolves a module name to a module.
pub fn resolve_module(db: &dyn Db, module_name: ModuleName) -> Option<Module> { pub fn resolve_module(db: &dyn Db, module_name: ModuleName) -> Option<Module> {
@ -84,9 +85,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
FilePath::SystemVirtual(_) => return None, FilePath::SystemVirtual(_) => return None,
}; };
let settings = module_resolution_settings(db); let mut search_paths = search_paths(db);
let mut search_paths = settings.search_paths(db);
let module_name = loop { let module_name = loop {
let candidate = search_paths.next()?; let candidate = search_paths.next()?;
@ -119,51 +118,67 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
} }
} }
pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
Program::get(db).search_paths(db).iter(db)
}
#[derive(Debug, PartialEq, Eq, Default)]
pub(crate) struct SearchPaths {
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
/// These shouldn't ever change unless the config settings themselves change.
static_paths: Vec<SearchPath>,
/// site-packages paths are not included in the above field:
/// if there are multiple site-packages paths, editable installations can appear
/// *between* the site-packages paths on `sys.path` at runtime.
/// That means we can't know where a second or third `site-packages` path should sit
/// in terms of module-resolution priority until we've discovered the editable installs
/// for the first `site-packages` path
site_packages: Vec<SearchPath>,
}
impl SearchPaths {
/// Validate and normalize the raw settings given by the user /// Validate and normalize the raw settings given by the user
/// into settings we can use for module resolution /// into settings we can use for module resolution
/// ///
/// This method also implements the typing spec's [module resolution order]. /// This method also implements the typing spec's [module resolution order].
/// ///
/// [module resolution order]: https://typing.readthedocs.io/en/latest/spec/distributing.html#import-resolution-ordering /// [module resolution order]: https://typing.readthedocs.io/en/latest/spec/distributing.html#import-resolution-ordering
fn try_resolve_module_resolution_settings( pub(crate) fn from_settings(
db: &dyn Db, db: &dyn Db,
) -> Result<ModuleResolutionSettings, SearchPathValidationError> { settings: SearchPathSettings,
let program = Program::get(db.upcast()); ) -> Result<Self, SearchPathValidationError> {
let SearchPathSettings { let SearchPathSettings {
extra_paths, extra_paths,
src_root, src_root,
custom_typeshed, custom_typeshed,
site_packages, site_packages: site_packages_paths,
} = program.search_paths(db.upcast()); } = settings;
if !extra_paths.is_empty() {
tracing::info!("Extra search paths: {extra_paths:?}");
}
if let Some(custom_typeshed) = custom_typeshed {
tracing::info!("Custom typeshed directory: {custom_typeshed}");
}
let system = db.system(); let system = db.system();
let files = db.files(); let files = db.files();
let mut static_search_paths = vec![]; let mut static_paths = vec![];
for path in extra_paths { for path in extra_paths {
let search_path = SearchPath::extra(system, path.clone())?; tracing::debug!("Adding static extra search-path '{path}'");
let search_path = SearchPath::extra(system, path)?;
files.try_add_root( files.try_add_root(
db.upcast(), db.upcast(),
search_path.as_system_path().unwrap(), search_path.as_system_path().unwrap(),
FileRootKind::LibrarySearchPath, FileRootKind::LibrarySearchPath,
); );
static_search_paths.push(search_path); static_paths.push(search_path);
} }
static_search_paths.push(SearchPath::first_party(system, src_root.clone())?); tracing::debug!("Adding static search path '{src_root}'");
static_paths.push(SearchPath::first_party(system, src_root)?);
static_search_paths.push(if let Some(custom_typeshed) = custom_typeshed.as_ref() { static_paths.push(if let Some(custom_typeshed) = custom_typeshed {
let search_path = SearchPath::custom_stdlib(db, custom_typeshed.clone())?; tracing::debug!("Adding static custom-sdtlib search-path '{custom_typeshed}'");
let search_path = SearchPath::custom_stdlib(db, custom_typeshed)?;
files.try_add_root( files.try_add_root(
db.upcast(), db.upcast(),
search_path.as_system_path().unwrap(), search_path.as_system_path().unwrap(),
@ -174,33 +189,31 @@ fn try_resolve_module_resolution_settings(
SearchPath::vendored_stdlib() SearchPath::vendored_stdlib()
}); });
let mut site_packages_paths: Vec<_> = Vec::with_capacity(site_packages.len()); let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len());
for path in site_packages { for path in site_packages_paths {
let search_path = SearchPath::site_packages(system, path.to_path_buf())?; tracing::debug!("Adding site-package path '{path}'");
let search_path = SearchPath::site_packages(system, path)?;
files.try_add_root( files.try_add_root(
db.upcast(), db.upcast(),
search_path.as_system_path().unwrap(), search_path.as_system_path().unwrap(),
FileRootKind::LibrarySearchPath, FileRootKind::LibrarySearchPath,
); );
site_packages_paths.push(search_path); site_packages.push(search_path);
} }
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step // TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
let target_version = program.target_version(db.upcast());
tracing::info!("Target version: {target_version}");
// Filter out module resolution paths that point to the same directory on disk (the same invariant maintained by [`sys.path` at runtime]). // Filter out module resolution paths that point to the same directory on disk (the same invariant maintained by [`sys.path` at runtime]).
// (Paths may, however, *overlap* -- e.g. you could have both `src/` and `src/foo` // (Paths may, however, *overlap* -- e.g. you could have both `src/` and `src/foo`
// as module resolution paths simultaneously.) // as module resolution paths simultaneously.)
// //
// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
// This code doesn't use an `IndexSet` because the key is the system path and not the search root. // This code doesn't use an `IndexSet` because the key is the system path and not the search root.
let mut seen_paths = //
FxHashSet::with_capacity_and_hasher(static_search_paths.len(), FxBuildHasher); // [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
let mut seen_paths = FxHashSet::with_capacity_and_hasher(static_paths.len(), FxBuildHasher);
static_search_paths.retain(|path| { static_paths.retain(|path| {
if let Some(path) = path.as_system_path() { if let Some(path) = path.as_system_path() {
seen_paths.insert(path.to_path_buf()) seen_paths.insert(path.to_path_buf())
} else { } else {
@ -208,17 +221,19 @@ fn try_resolve_module_resolution_settings(
} }
}); });
Ok(ModuleResolutionSettings { Ok(SearchPaths {
target_version, static_paths,
static_search_paths, site_packages,
site_packages_paths,
}) })
} }
#[salsa::tracked(return_ref)] pub(crate) fn iter<'a>(&'a self, db: &'a dyn Db) -> SearchPathIterator<'a> {
pub(crate) fn module_resolution_settings(db: &dyn Db) -> ModuleResolutionSettings { SearchPathIterator {
// TODO proper error handling if this returns an error: db,
try_resolve_module_resolution_settings(db).unwrap() static_paths: self.static_paths.iter(),
dynamic_paths: None,
}
}
} }
/// Collect all dynamic search paths. For each `site-packages` path: /// Collect all dynamic search paths. For each `site-packages` path:
@ -231,19 +246,20 @@ pub(crate) fn module_resolution_settings(db: &dyn Db) -> ModuleResolutionSetting
/// module-resolution priority. /// module-resolution priority.
#[salsa::tracked(return_ref)] #[salsa::tracked(return_ref)]
pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> { pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
let ModuleResolutionSettings { tracing::debug!("Resolving dynamic module resolution paths");
target_version: _,
static_search_paths, let SearchPaths {
site_packages_paths, static_paths,
} = module_resolution_settings(db); site_packages,
} = Program::get(db).search_paths(db);
let mut dynamic_paths = Vec::new(); let mut dynamic_paths = Vec::new();
if site_packages_paths.is_empty() { if site_packages.is_empty() {
return dynamic_paths; return dynamic_paths;
} }
let mut existing_paths: FxHashSet<_> = static_search_paths let mut existing_paths: FxHashSet<_> = static_paths
.iter() .iter()
.filter_map(|path| path.as_system_path()) .filter_map(|path| path.as_system_path())
.map(Cow::Borrowed) .map(Cow::Borrowed)
@ -252,7 +268,7 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
let files = db.files(); let files = db.files();
let system = db.system(); let system = db.system();
for site_packages_search_path in site_packages_paths { for site_packages_search_path in site_packages {
let site_packages_dir = site_packages_search_path let site_packages_dir = site_packages_search_path
.as_system_path() .as_system_path()
.expect("Expected site package path to be a system path"); .expect("Expected site package path to be a system path");
@ -302,6 +318,10 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
if existing_paths.insert(Cow::Owned(installation.clone())) { if existing_paths.insert(Cow::Owned(installation.clone())) {
match SearchPath::editable(system, installation) { match SearchPath::editable(system, installation) {
Ok(search_path) => { Ok(search_path) => {
tracing::debug!(
"Adding editable installation to module resolution path {path}",
path = search_path.as_system_path().unwrap()
);
dynamic_paths.push(search_path); dynamic_paths.push(search_path);
} }
@ -448,38 +468,6 @@ impl<'db> Iterator for PthFileIterator<'db> {
} }
} }
/// Validated and normalized module-resolution settings.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ModuleResolutionSettings {
target_version: PythonVersion,
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
/// These shouldn't ever change unless the config settings themselves change.
static_search_paths: Vec<SearchPath>,
/// site-packages paths are not included in the above field:
/// if there are multiple site-packages paths, editable installations can appear
/// *between* the site-packages paths on `sys.path` at runtime.
/// That means we can't know where a second or third `site-packages` path should sit
/// in terms of module-resolution priority until we've discovered the editable installs
/// for the first `site-packages` path
site_packages_paths: Vec<SearchPath>,
}
impl ModuleResolutionSettings {
fn target_version(&self) -> PythonVersion {
self.target_version
}
pub(crate) fn search_paths<'db>(&'db self, db: &'db dyn Db) -> SearchPathIterator<'db> {
SearchPathIterator {
db,
static_paths: self.static_search_paths.iter(),
dynamic_paths: None,
}
}
}
/// A thin wrapper around `ModuleName` to make it a Salsa ingredient. /// A thin wrapper around `ModuleName` to make it a Salsa ingredient.
/// ///
/// This is needed because Salsa requires that all query arguments are salsa ingredients. /// This is needed because Salsa requires that all query arguments are salsa ingredients.
@ -492,13 +480,13 @@ struct ModuleNameIngredient<'db> {
/// Given a module name and a list of search paths in which to lookup modules, /// Given a module name and a list of search paths in which to lookup modules,
/// attempt to resolve the module name /// attempt to resolve the module name
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, ModuleKind)> { fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, ModuleKind)> {
let resolver_settings = module_resolution_settings(db); let program = Program::get(db);
let target_version = resolver_settings.target_version(); let target_version = program.target_version(db);
let resolver_state = ResolverState::new(db, target_version); let resolver_state = ResolverState::new(db, target_version);
let is_builtin_module = let is_builtin_module =
ruff_python_stdlib::sys::is_builtin_module(target_version.minor, name.as_str()); ruff_python_stdlib::sys::is_builtin_module(target_version.minor, name.as_str());
for search_path in resolver_settings.search_paths(db) { for search_path in search_paths(db) {
// When a builtin module is imported, standard module resolution is bypassed: // When a builtin module is imported, standard module resolution is bypassed:
// the module name always resolves to the stdlib module, // the module name always resolves to the stdlib module,
// even if there's a module of the same name in the first-party root // even if there's a module of the same name in the first-party root
@ -652,6 +640,8 @@ mod tests {
use crate::module_name::ModuleName; use crate::module_name::ModuleName;
use crate::module_resolver::module::ModuleKind; use crate::module_resolver::module::ModuleKind;
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder}; use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use crate::ProgramSettings;
use crate::PythonVersion;
use super::*; use super::*;
@ -1202,14 +1192,19 @@ mod tests {
std::fs::write(foo.as_std_path(), "")?; std::fs::write(foo.as_std_path(), "")?;
std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?; std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?;
let search_paths = SearchPathSettings { Program::from_settings(
&db,
ProgramSettings {
target_version: PythonVersion::PY38,
search_paths: SearchPathSettings {
extra_paths: vec![], extra_paths: vec![],
src_root: src.clone(), src_root: src.clone(),
custom_typeshed: Some(custom_typeshed.clone()), custom_typeshed: Some(custom_typeshed.clone()),
site_packages: vec![site_packages], site_packages: vec![site_packages],
}; },
},
Program::new(&db, PythonVersion::PY38, search_paths); )
.context("Invalid program settings")?;
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap(); let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
let bar_module = resolve_module(&db, ModuleName::new_static("bar").unwrap()).unwrap(); let bar_module = resolve_module(&db, ModuleName::new_static("bar").unwrap()).unwrap();
@ -1673,8 +1668,7 @@ not_a_directory
.with_site_packages_files(&[("_foo.pth", "/src")]) .with_site_packages_files(&[("_foo.pth", "/src")])
.build(); .build();
let search_paths: Vec<&SearchPath> = let search_paths: Vec<&SearchPath> = search_paths(&db).collect();
module_resolution_settings(&db).search_paths(&db).collect();
assert!(search_paths.contains( assert!(search_paths.contains(
&&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap() &&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap()
@ -1703,16 +1697,19 @@ not_a_directory
]) ])
.unwrap(); .unwrap();
Program::new( Program::from_settings(
&db, &db,
PythonVersion::default(), ProgramSettings {
SearchPathSettings { target_version: PythonVersion::default(),
search_paths: SearchPathSettings {
extra_paths: vec![], extra_paths: vec![],
src_root: SystemPathBuf::from("/src"), src_root: SystemPathBuf::from("/src"),
custom_typeshed: None, custom_typeshed: None,
site_packages: vec![venv_site_packages, system_site_packages], site_packages: vec![venv_site_packages, system_site_packages],
}, },
); },
)
.expect("Valid program settings");
// The editable installs discovered from the `.pth` file in the first `site-packages` directory // The editable installs discovered from the `.pth` file in the first `site-packages` directory
// take precedence over the second `site-packages` directory... // take precedence over the second `site-packages` directory...

View file

@ -4,6 +4,7 @@ use ruff_db::vendored::VendoredPathBuf;
use crate::db::tests::TestDb; use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings}; use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion; use crate::python_version::PythonVersion;
use crate::ProgramSettings;
/// A test case for the module resolver. /// A test case for the module resolver.
/// ///
@ -220,16 +221,19 @@ impl TestCaseBuilder<MockedTypeshed> {
let src = Self::write_mock_directory(&mut db, "/src", first_party_files); let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
let typeshed = Self::build_typeshed_mock(&mut db, &typeshed_option); let typeshed = Self::build_typeshed_mock(&mut db, &typeshed_option);
Program::new( Program::from_settings(
&db, &db,
ProgramSettings {
target_version, target_version,
SearchPathSettings { search_paths: SearchPathSettings {
extra_paths: vec![], extra_paths: vec![],
src_root: src.clone(), src_root: src.clone(),
custom_typeshed: Some(typeshed.clone()), custom_typeshed: Some(typeshed.clone()),
site_packages: vec![site_packages.clone()], site_packages: vec![site_packages.clone()],
}, },
); },
)
.expect("Valid program settings");
TestCase { TestCase {
db, db,
@ -273,16 +277,19 @@ impl TestCaseBuilder<VendoredTypeshed> {
Self::write_mock_directory(&mut db, "/site-packages", site_packages_files); Self::write_mock_directory(&mut db, "/site-packages", site_packages_files);
let src = Self::write_mock_directory(&mut db, "/src", first_party_files); let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
Program::new( Program::from_settings(
&db, &db,
ProgramSettings {
target_version, target_version,
SearchPathSettings { search_paths: SearchPathSettings {
extra_paths: vec![], extra_paths: vec![],
src_root: src.clone(), src_root: src.clone(),
custom_typeshed: None, custom_typeshed: None,
site_packages: vec![site_packages.clone()], site_packages: vec![site_packages.clone()],
}, },
); },
)
.expect("Valid search path settings");
TestCase { TestCase {
db, db,

View file

@ -1,21 +1,53 @@
use crate::python_version::PythonVersion; use crate::python_version::PythonVersion;
use crate::Db; use anyhow::Context;
use ruff_db::system::SystemPathBuf;
use salsa::Durability; use salsa::Durability;
use salsa::Setter;
use ruff_db::system::SystemPathBuf;
use crate::module_resolver::SearchPaths;
use crate::Db;
#[salsa::input(singleton)] #[salsa::input(singleton)]
pub struct Program { pub struct Program {
pub target_version: PythonVersion, pub target_version: PythonVersion,
#[default]
#[return_ref] #[return_ref]
pub search_paths: SearchPathSettings, pub(crate) search_paths: SearchPaths,
} }
impl Program { impl Program {
pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> Self { pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> anyhow::Result<Self> {
Program::builder(settings.target_version, settings.search_paths) let ProgramSettings {
target_version,
search_paths,
} = settings;
tracing::info!("Target version: {target_version}");
let search_paths = SearchPaths::from_settings(db, search_paths)
.with_context(|| "Invalid search path settings")?;
Ok(Program::builder(settings.target_version)
.durability(Durability::HIGH) .durability(Durability::HIGH)
.new(db) .search_paths(search_paths)
.new(db))
}
pub fn update_search_paths(
&self,
db: &mut dyn Db,
search_path_settings: SearchPathSettings,
) -> anyhow::Result<()> {
let search_paths = SearchPaths::from_settings(db, search_path_settings)?;
if self.search_paths(db) != &search_paths {
tracing::debug!("Update search paths");
self.set_search_paths(db).to(search_paths);
}
Ok(())
} }
} }

View file

@ -171,29 +171,32 @@ mod tests {
use crate::program::{Program, SearchPathSettings}; use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion; use crate::python_version::PythonVersion;
use crate::types::Type; use crate::types::Type;
use crate::{HasTy, SemanticModel}; use crate::{HasTy, ProgramSettings, SemanticModel};
fn setup_db() -> TestDb { fn setup_db<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<TestDb> {
let db = TestDb::new(); let mut db = TestDb::new();
Program::new( db.write_files(files)?;
Program::from_settings(
&db, &db,
PythonVersion::default(), ProgramSettings {
SearchPathSettings { target_version: PythonVersion::default(),
search_paths: SearchPathSettings {
extra_paths: vec![], extra_paths: vec![],
src_root: SystemPathBuf::from("/src"), src_root: SystemPathBuf::from("/src"),
site_packages: vec![], site_packages: vec![],
custom_typeshed: None, custom_typeshed: None,
}, },
); },
)?;
db Ok(db)
} }
#[test] #[test]
fn function_ty() -> anyhow::Result<()> { fn function_ty() -> anyhow::Result<()> {
let mut db = setup_db(); let db = setup_db([("/src/foo.py", "def test(): pass")])?;
db.write_file("/src/foo.py", "def test(): pass")?;
let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
let ast = parsed_module(&db, foo); let ast = parsed_module(&db, foo);
@ -209,9 +212,8 @@ mod tests {
#[test] #[test]
fn class_ty() -> anyhow::Result<()> { fn class_ty() -> anyhow::Result<()> {
let mut db = setup_db(); let db = setup_db([("/src/foo.py", "class Test: pass")])?;
db.write_file("/src/foo.py", "class Test: pass")?;
let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
let ast = parsed_module(&db, foo); let ast = parsed_module(&db, foo);
@ -227,12 +229,11 @@ mod tests {
#[test] #[test]
fn alias_ty() -> anyhow::Result<()> { fn alias_ty() -> anyhow::Result<()> {
let mut db = setup_db(); let db = setup_db([
db.write_files([
("/src/foo.py", "class Test: pass"), ("/src/foo.py", "class Test: pass"),
("/src/bar.py", "from foo import Test"), ("/src/bar.py", "from foo import Test"),
])?; ])?;
let bar = system_path_to_file(&db, "/src/bar.py").unwrap(); let bar = system_path_to_file(&db, "/src/bar.py").unwrap();
let ast = parsed_module(&db, bar); let ast = parsed_module(&db, bar);

View file

@ -1494,6 +1494,7 @@ impl<'db> TypeInferenceBuilder<'db> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use anyhow::Context;
use ruff_db::files::{system_path_to_file, File}; use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module; use ruff_db::parsed::parsed_module;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@ -1508,40 +1509,58 @@ mod tests {
use crate::semantic_index::symbol::FileScopeId; use crate::semantic_index::symbol::FileScopeId;
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map}; use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
use crate::types::{global_symbol_ty_by_name, infer_definition_types, symbol_ty_by_name, Type}; use crate::types::{global_symbol_ty_by_name, infer_definition_types, symbol_ty_by_name, Type};
use crate::{HasTy, SemanticModel}; use crate::{HasTy, ProgramSettings, SemanticModel};
fn setup_db() -> TestDb { fn setup_db() -> TestDb {
let db = TestDb::new(); let db = TestDb::new();
Program::new( let src_root = SystemPathBuf::from("/src");
db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db, &db,
PythonVersion::default(), ProgramSettings {
SearchPathSettings { target_version: PythonVersion::default(),
search_paths: SearchPathSettings {
extra_paths: Vec::new(), extra_paths: Vec::new(),
src_root: SystemPathBuf::from("/src"), src_root,
site_packages: vec![], site_packages: vec![],
custom_typeshed: None, custom_typeshed: None,
}, },
); },
)
.expect("Valid search path settings");
db db
} }
fn setup_db_with_custom_typeshed(typeshed: &str) -> TestDb { fn setup_db_with_custom_typeshed<'a>(
let db = TestDb::new(); typeshed: &str,
files: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> anyhow::Result<TestDb> {
let mut db = TestDb::new();
let src_root = SystemPathBuf::from("/src");
Program::new( db.write_files(files)
.context("Failed to write test files")?;
Program::from_settings(
&db, &db,
PythonVersion::default(), ProgramSettings {
SearchPathSettings { target_version: PythonVersion::default(),
search_paths: SearchPathSettings {
extra_paths: Vec::new(), extra_paths: Vec::new(),
src_root: SystemPathBuf::from("/src"), src_root,
site_packages: vec![], site_packages: vec![],
custom_typeshed: Some(SystemPathBuf::from(typeshed)), custom_typeshed: Some(SystemPathBuf::from(typeshed)),
}, },
); },
)
.context("Failed to create Program")?;
db Ok(db)
} }
fn assert_public_ty(db: &TestDb, file_name: &str, symbol_name: &str, expected: &str) { fn assert_public_ty(db: &TestDb, file_name: &str, symbol_name: &str, expected: &str) {
@ -2131,16 +2150,17 @@ mod tests {
#[test] #[test]
fn builtin_symbol_custom_stdlib() -> anyhow::Result<()> { fn builtin_symbol_custom_stdlib() -> anyhow::Result<()> {
let mut db = setup_db_with_custom_typeshed("/typeshed"); let db = setup_db_with_custom_typeshed(
"/typeshed",
db.write_files([ [
("/src/a.py", "c = copyright"), ("/src/a.py", "c = copyright"),
( (
"/typeshed/stdlib/builtins.pyi", "/typeshed/stdlib/builtins.pyi",
"def copyright() -> None: ...", "def copyright() -> None: ...",
), ),
("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"), ("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"),
])?; ],
)?;
assert_public_ty(&db, "/src/a.py", "c", "Literal[copyright]"); assert_public_ty(&db, "/src/a.py", "c", "Literal[copyright]");
@ -2160,13 +2180,14 @@ mod tests {
#[test] #[test]
fn unknown_builtin_later_defined() -> anyhow::Result<()> { fn unknown_builtin_later_defined() -> anyhow::Result<()> {
let mut db = setup_db_with_custom_typeshed("/typeshed"); let db = setup_db_with_custom_typeshed(
"/typeshed",
db.write_files([ [
("/src/a.py", "x = foo"), ("/src/a.py", "x = foo"),
("/typeshed/stdlib/builtins.pyi", "foo = bar; bar = 1"), ("/typeshed/stdlib/builtins.pyi", "foo = bar; bar = 1"),
("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"), ("/typeshed/stdlib/VERSIONS", "builtins: 3.8-"),
])?; ],
)?;
assert_public_ty(&db, "/src/a.py", "x", "Unbound"); assert_public_ty(&db, "/src/a.py", "x", "Unbound");

View file

@ -78,7 +78,8 @@ impl Session {
custom_typeshed: None, custom_typeshed: None,
}, },
}; };
workspaces.insert(path, RootDatabase::new(metadata, program_settings, system)); // TODO(micha): Handle the case where the program settings are incorrect more gracefully.
workspaces.insert(path, RootDatabase::new(metadata, program_settings, system)?);
} }
Ok(Self { Ok(Self {

View file

@ -49,7 +49,8 @@ impl Workspace {
search_paths: SearchPathSettings::default(), search_paths: SearchPathSettings::default(),
}; };
let db = RootDatabase::new(workspace, program_settings, system.clone()); let db =
RootDatabase::new(workspace, program_settings, system.clone()).map_err(into_error)?;
Ok(Self { db, system }) Ok(Self { db, system })
} }

View file

@ -28,7 +28,11 @@ pub struct RootDatabase {
} }
impl RootDatabase { impl RootDatabase {
pub fn new<S>(workspace: WorkspaceMetadata, settings: ProgramSettings, system: S) -> Self pub fn new<S>(
workspace: WorkspaceMetadata,
settings: ProgramSettings,
system: S,
) -> anyhow::Result<Self>
where where
S: System + 'static + Send + Sync + RefUnwindSafe, S: System + 'static + Send + Sync + RefUnwindSafe,
{ {
@ -41,10 +45,10 @@ impl RootDatabase {
let workspace = Workspace::from_metadata(&db, workspace); let workspace = Workspace::from_metadata(&db, workspace);
// Initialize the `Program` singleton // Initialize the `Program` singleton
Program::from_settings(&db, settings); Program::from_settings(&db, settings)?;
db.workspace = Some(workspace); db.workspace = Some(workspace);
db Ok(db)
} }
pub fn workspace(&self) -> Workspace { pub fn workspace(&self) -> Workspace {

View file

@ -305,7 +305,7 @@ enum AnyImportRef<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use red_knot_python_semantic::{Program, PythonVersion, SearchPathSettings}; use red_knot_python_semantic::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
use ruff_db::files::system_path_to_file; use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@ -320,16 +320,23 @@ mod tests {
fn setup_db_with_root(src_root: SystemPathBuf) -> TestDb { fn setup_db_with_root(src_root: SystemPathBuf) -> TestDb {
let db = TestDb::new(); let db = TestDb::new();
Program::new( db.memory_file_system()
.create_directory_all(&src_root)
.unwrap();
Program::from_settings(
&db, &db,
PythonVersion::default(), ProgramSettings {
SearchPathSettings { target_version: PythonVersion::default(),
search_paths: SearchPathSettings {
extra_paths: Vec::new(), extra_paths: Vec::new(),
src_root, src_root,
site_packages: vec![], site_packages: vec![],
custom_typeshed: None, custom_typeshed: None,
}, },
); },
)
.expect("Valid program settings");
db db
} }

View file

@ -20,8 +20,7 @@ fn setup_db(workspace_root: SystemPathBuf) -> anyhow::Result<RootDatabase> {
target_version: PythonVersion::default(), target_version: PythonVersion::default(),
search_paths, search_paths,
}; };
let db = RootDatabase::new(workspace, settings, system); RootDatabase::new(workspace, settings, system)
Ok(db)
} }
/// Test that all snippets in testcorpus can be checked without panic /// Test that all snippets in testcorpus can be checked without panic

View file

@ -52,7 +52,7 @@ fn setup_case() -> Case {
}, },
}; };
let mut db = RootDatabase::new(metadata, settings, system); let mut db = RootDatabase::new(metadata, settings, system).unwrap();
let parser = system_path_to_file(&db, parser_path).unwrap(); let parser = system_path_to_file(&db, parser_path).unwrap();
db.workspace().open_file(&mut db, parser); db.workspace().open_file(&mut db, parser);