Split out `discover_in` from `ProjectMetadata::discover`

This commit is contained in:
Micha Reiser 2025-11-07 19:28:26 -05:00
parent efb23b01af
commit 1e77da4d17
No known key found for this signature in database
2 changed files with 143 additions and 79 deletions

View File

@ -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,6 +143,64 @@ impl ProjectMetadata {
let mut closest_project: Option<ProjectMetadata> = None;
for project_root in path.ancestors() {
let Some(discovered) = Self::discover_in(project_root, system)? else {
continue;
};
match discovered {
DiscoveredProject::PyProject {
has_ty_section: true,
metadata,
}
| DiscoveredProject::Ty { metadata } => {
tracing::debug!("Found project at '{}'", project_root);
return Ok(metadata);
}
DiscoveredProject::PyProject { metadata, .. } => {
// Not a project itself, keep looking for an enclosing project.
if closest_project.is_none() {
closest_project = Some(metadata);
}
}
}
}
// No project found, but maybe a pyproject.toml was found.
let metadata = if let Some(closest_project) = closest_project {
tracing::debug!(
"Project without `tool.ty` section: '{}'",
closest_project.root()
);
closest_project
} else {
tracing::debug!(
"The ancestor directories contain no `pyproject.toml`. Falling back to a virtual project."
);
// Create a project with a default configuration
Self::new(
path.file_name().unwrap_or("root").into(),
path.to_path_buf(),
)
};
Ok(metadata)
}
fn discover_in(
project_root: &SystemPath,
system: &dyn System,
) -> Result<Option<DiscoveredProject>, 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) {
@ -195,20 +255,6 @@ impl ProjectMetadata {
.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,
@ -216,40 +262,26 @@ impl ProjectMetadata {
},
)?;
if has_ty_section {
tracing::debug!("Found project at '{}'", project_root);
return Ok(metadata);
return Ok(Some(DiscoveredProject::Ty { metadata }));
}
// Not a project itself, keep looking for an enclosing project.
if closest_project.is_none() {
closest_project = Some(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,
}));
}
// No project found, but maybe a pyproject.toml was found.
let metadata = if let Some(closest_project) = closest_project {
tracing::debug!(
"Project without `tool.ty` section: '{}'",
closest_project.root()
);
closest_project
} else {
tracing::debug!(
"The ancestor directories contain no `pyproject.toml`. Falling back to a virtual project."
);
// Create a project with a default configuration
Self::new(
path.file_name().unwrap_or("root").into(),
path.to_path_buf(),
)
};
Ok(metadata)
Ok(None)
}
pub fn root(&self) -> &SystemPath {
@ -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

View File

@ -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);