diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index 62931b3e64..4a10bafef6 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -1,7 +1,9 @@ use configuration_file::{ConfigurationFile, ConfigurationFileError}; +use ruff_db::system::walk_directory::WalkState; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::vendored::VendoredFileSystem; use ruff_python_ast::name::Name; +use std::collections::BTreeMap; use std::sync::Arc; use thiserror::Error; use ty_combine::Combine; @@ -141,90 +143,25 @@ impl ProjectMetadata { let mut closest_project: Option = None; for project_root in path.ancestors() { - let pyproject_path = project_root.join("pyproject.toml"); - - let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) { - match PyProject::from_toml_str( - &pyproject_str, - ValueSource::File(Arc::new(pyproject_path.clone())), - ) { - Ok(pyproject) => Some(pyproject), - Err(error) => { - return Err(ProjectMetadataError::InvalidPyProject { - path: pyproject_path, - source: Box::new(error), - }); - } - } - } else { - None + let Some(discovered) = Self::discover_in(project_root, system)? else { + continue; }; - // A `ty.toml` takes precedence over a `pyproject.toml`. - let ty_toml_path = project_root.join("ty.toml"); - if let Ok(ty_str) = system.read_to_string(&ty_toml_path) { - let options = match Options::from_toml_str( - &ty_str, - ValueSource::File(Arc::new(ty_toml_path.clone())), - ) { - Ok(options) => options, - Err(error) => { - return Err(ProjectMetadataError::InvalidTyToml { - path: ty_toml_path, - source: Box::new(error), - }); - } - }; - - if pyproject - .as_ref() - .is_some_and(|project| project.ty().is_some()) - { - // TODO: Consider using a diagnostic here - tracing::warn!( - "Ignoring the `tool.ty` section in `{pyproject_path}` because `{ty_toml_path}` takes precedence." - ); + match discovered { + DiscoveredProject::PyProject { + has_ty_section: true, + metadata, } - - tracing::debug!("Found project at '{}'", project_root); - - let metadata = ProjectMetadata::from_options( - options, - project_root.to_path_buf(), - pyproject - .as_ref() - .and_then(|pyproject| pyproject.project.as_ref()), - ) - .map_err(|err| { - ProjectMetadataError::InvalidRequiresPythonConstraint { - source: err, - path: pyproject_path, - } - })?; - - return Ok(metadata); - } - - if let Some(pyproject) = pyproject { - let has_ty_section = pyproject.ty().is_some(); - let metadata = - ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf()) - .map_err( - |err| ProjectMetadataError::InvalidRequiresPythonConstraint { - source: err, - path: pyproject_path, - }, - )?; - - if has_ty_section { + | DiscoveredProject::Ty { metadata } => { tracing::debug!("Found project at '{}'", project_root); return Ok(metadata); } - - // Not a project itself, keep looking for an enclosing project. - if closest_project.is_none() { - closest_project = Some(metadata); + DiscoveredProject::PyProject { metadata, .. } => { + // Not a project itself, keep looking for an enclosing project. + if closest_project.is_none() { + closest_project = Some(metadata); + } } } } @@ -252,6 +189,101 @@ impl ProjectMetadata { Ok(metadata) } + fn discover_in( + project_root: &SystemPath, + system: &dyn System, + ) -> Result, ProjectMetadataError> { + tracing::debug!("Searching for a project in '{project_root}'"); + + if !system.is_directory(project_root) { + return Err(ProjectMetadataError::NotADirectory( + project_root.to_path_buf(), + )); + } + + let pyproject_path = project_root.join("pyproject.toml"); + + let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) { + match PyProject::from_toml_str( + &pyproject_str, + ValueSource::File(Arc::new(pyproject_path.clone())), + ) { + Ok(pyproject) => Some(pyproject), + Err(error) => { + return Err(ProjectMetadataError::InvalidPyProject { + path: pyproject_path, + source: Box::new(error), + }); + } + } + } else { + None + }; + + // A `ty.toml` takes precedence over a `pyproject.toml`. + let ty_toml_path = project_root.join("ty.toml"); + if let Ok(ty_str) = system.read_to_string(&ty_toml_path) { + let options = match Options::from_toml_str( + &ty_str, + ValueSource::File(Arc::new(ty_toml_path.clone())), + ) { + Ok(options) => options, + Err(error) => { + return Err(ProjectMetadataError::InvalidTyToml { + path: ty_toml_path, + source: Box::new(error), + }); + } + }; + + if pyproject + .as_ref() + .is_some_and(|project| project.ty().is_some()) + { + // TODO: Consider using a diagnostic here + tracing::warn!( + "Ignoring the `tool.ty` section in `{pyproject_path}` because `{ty_toml_path}` takes precedence." + ); + } + + tracing::debug!("Found project at '{}'", project_root); + + let metadata = ProjectMetadata::from_options( + options, + project_root.to_path_buf(), + pyproject + .as_ref() + .and_then(|pyproject| pyproject.project.as_ref()), + ) + .map_err( + |err| ProjectMetadataError::InvalidRequiresPythonConstraint { + source: err, + path: pyproject_path, + }, + )?; + + return Ok(Some(DiscoveredProject::Ty { metadata })); + } + + if let Some(pyproject) = pyproject { + let has_ty_section = pyproject.ty().is_some(); + let metadata = ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf()) + .map_err(|err| { + ProjectMetadataError::InvalidRequiresPythonConstraint { + source: err, + path: pyproject_path, + } + })?; + + return Ok(Some(DiscoveredProject::PyProject { + has_ty_section, + metadata, + })); + } + + Ok(None) + } + pub fn root(&self) -> &SystemPath { &self.root } @@ -344,6 +376,36 @@ pub enum ProjectMetadataError { }, } +#[derive(Debug)] +enum DiscoveredProject { + PyProject { + has_ty_section: bool, + metadata: ProjectMetadata, + }, + Ty { + metadata: ProjectMetadata, + }, +} + +impl DiscoveredProject { + fn is_ty_or_project_with_ty_section(&self) -> bool { + match self { + DiscoveredProject::PyProject { + has_ty_section: tool_ty, + .. + } => *tool_ty, + DiscoveredProject::Ty { .. } => true, + } + } + + fn into_metadata(self) -> ProjectMetadata { + match self { + DiscoveredProject::PyProject { metadata, .. } => metadata, + DiscoveredProject::Ty { metadata } => metadata, + } + } +} + #[cfg(test)] mod tests { //! Integration tests for project discovery diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 992f02f929..ab68d17964 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -499,6 +499,9 @@ impl Session { continue; }; + // TODO: Discover all projects within this workspace, starting from root. + // + // For now, create one project database per workspace. // In the future, index the workspace directories to find all projects // and create a project database for each. @@ -1175,8 +1178,7 @@ impl Workspaces { ) -> Option<(SystemPathBuf, &mut Workspace)> { let path = url.to_file_path().ok()?; - // Realistically I don't think this can fail because we got the path from a Url - let system_path = SystemPathBuf::from_path_buf(path).ok()?; + let system_path = SystemPathBuf::from_path_buf(path).expect("URL to be valid UTF-8"); if let Some(workspace) = self.workspaces.get_mut(&system_path) { workspace.settings = Arc::new(settings);