|
1 | 1 | use configuration_file::{ConfigurationFile, ConfigurationFileError}; |
| 2 | +use ruff_db::system::walk_directory::WalkState; |
2 | 3 | use ruff_db::system::{System, SystemPath, SystemPathBuf}; |
3 | 4 | use ruff_db::vendored::VendoredFileSystem; |
4 | 5 | use ruff_python_ast::name::Name; |
| 6 | +use std::collections::BTreeMap; |
5 | 7 | use std::sync::Arc; |
6 | 8 | use thiserror::Error; |
7 | 9 | use ty_combine::Combine; |
@@ -141,90 +143,25 @@ impl ProjectMetadata { |
141 | 143 | let mut closest_project: Option<ProjectMetadata> = None; |
142 | 144 |
|
143 | 145 | for project_root in path.ancestors() { |
144 | | - let pyproject_path = project_root.join("pyproject.toml"); |
145 | | - |
146 | | - let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) { |
147 | | - match PyProject::from_toml_str( |
148 | | - &pyproject_str, |
149 | | - ValueSource::File(Arc::new(pyproject_path.clone())), |
150 | | - ) { |
151 | | - Ok(pyproject) => Some(pyproject), |
152 | | - Err(error) => { |
153 | | - return Err(ProjectMetadataError::InvalidPyProject { |
154 | | - path: pyproject_path, |
155 | | - source: Box::new(error), |
156 | | - }); |
157 | | - } |
158 | | - } |
159 | | - } else { |
160 | | - None |
| 146 | + let Some(discovered) = Self::discover_in(project_root, system)? else { |
| 147 | + continue; |
161 | 148 | }; |
162 | 149 |
|
163 | | - // A `ty.toml` takes precedence over a `pyproject.toml`. |
164 | | - let ty_toml_path = project_root.join("ty.toml"); |
165 | | - if let Ok(ty_str) = system.read_to_string(&ty_toml_path) { |
166 | | - let options = match Options::from_toml_str( |
167 | | - &ty_str, |
168 | | - ValueSource::File(Arc::new(ty_toml_path.clone())), |
169 | | - ) { |
170 | | - Ok(options) => options, |
171 | | - Err(error) => { |
172 | | - return Err(ProjectMetadataError::InvalidTyToml { |
173 | | - path: ty_toml_path, |
174 | | - source: Box::new(error), |
175 | | - }); |
176 | | - } |
177 | | - }; |
178 | | - |
179 | | - if pyproject |
180 | | - .as_ref() |
181 | | - .is_some_and(|project| project.ty().is_some()) |
182 | | - { |
183 | | - // TODO: Consider using a diagnostic here |
184 | | - tracing::warn!( |
185 | | - "Ignoring the `tool.ty` section in `{pyproject_path}` because `{ty_toml_path}` takes precedence." |
186 | | - ); |
| 150 | + match discovered { |
| 151 | + DiscoveredProject::PyProject { |
| 152 | + has_ty_section: true, |
| 153 | + metadata, |
187 | 154 | } |
188 | | - |
189 | | - tracing::debug!("Found project at '{}'", project_root); |
190 | | - |
191 | | - let metadata = ProjectMetadata::from_options( |
192 | | - options, |
193 | | - project_root.to_path_buf(), |
194 | | - pyproject |
195 | | - .as_ref() |
196 | | - .and_then(|pyproject| pyproject.project.as_ref()), |
197 | | - ) |
198 | | - .map_err(|err| { |
199 | | - ProjectMetadataError::InvalidRequiresPythonConstraint { |
200 | | - source: err, |
201 | | - path: pyproject_path, |
202 | | - } |
203 | | - })?; |
204 | | - |
205 | | - return Ok(metadata); |
206 | | - } |
207 | | - |
208 | | - if let Some(pyproject) = pyproject { |
209 | | - let has_ty_section = pyproject.ty().is_some(); |
210 | | - let metadata = |
211 | | - ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf()) |
212 | | - .map_err( |
213 | | - |err| ProjectMetadataError::InvalidRequiresPythonConstraint { |
214 | | - source: err, |
215 | | - path: pyproject_path, |
216 | | - }, |
217 | | - )?; |
218 | | - |
219 | | - if has_ty_section { |
| 155 | + | DiscoveredProject::Ty { metadata } => { |
220 | 156 | tracing::debug!("Found project at '{}'", project_root); |
221 | 157 |
|
222 | 158 | return Ok(metadata); |
223 | 159 | } |
224 | | - |
225 | | - // Not a project itself, keep looking for an enclosing project. |
226 | | - if closest_project.is_none() { |
227 | | - closest_project = Some(metadata); |
| 160 | + DiscoveredProject::PyProject { metadata, .. } => { |
| 161 | + // Not a project itself, keep looking for an enclosing project. |
| 162 | + if closest_project.is_none() { |
| 163 | + closest_project = Some(metadata); |
| 164 | + } |
228 | 165 | } |
229 | 166 | } |
230 | 167 | } |
@@ -252,6 +189,101 @@ impl ProjectMetadata { |
252 | 189 | Ok(metadata) |
253 | 190 | } |
254 | 191 |
|
| 192 | + fn discover_in( |
| 193 | + project_root: &SystemPath, |
| 194 | + system: &dyn System, |
| 195 | + ) -> Result<Option<DiscoveredProject>, ProjectMetadataError> { |
| 196 | + tracing::debug!("Searching for a project in '{project_root}'"); |
| 197 | + |
| 198 | + if !system.is_directory(project_root) { |
| 199 | + return Err(ProjectMetadataError::NotADirectory( |
| 200 | + project_root.to_path_buf(), |
| 201 | + )); |
| 202 | + } |
| 203 | + |
| 204 | + let pyproject_path = project_root.join("pyproject.toml"); |
| 205 | + |
| 206 | + let pyproject = if let Ok(pyproject_str) = system.read_to_string(&pyproject_path) { |
| 207 | + match PyProject::from_toml_str( |
| 208 | + &pyproject_str, |
| 209 | + ValueSource::File(Arc::new(pyproject_path.clone())), |
| 210 | + ) { |
| 211 | + Ok(pyproject) => Some(pyproject), |
| 212 | + Err(error) => { |
| 213 | + return Err(ProjectMetadataError::InvalidPyProject { |
| 214 | + path: pyproject_path, |
| 215 | + source: Box::new(error), |
| 216 | + }); |
| 217 | + } |
| 218 | + } |
| 219 | + } else { |
| 220 | + None |
| 221 | + }; |
| 222 | + |
| 223 | + // A `ty.toml` takes precedence over a `pyproject.toml`. |
| 224 | + let ty_toml_path = project_root.join("ty.toml"); |
| 225 | + if let Ok(ty_str) = system.read_to_string(&ty_toml_path) { |
| 226 | + let options = match Options::from_toml_str( |
| 227 | + &ty_str, |
| 228 | + ValueSource::File(Arc::new(ty_toml_path.clone())), |
| 229 | + ) { |
| 230 | + Ok(options) => options, |
| 231 | + Err(error) => { |
| 232 | + return Err(ProjectMetadataError::InvalidTyToml { |
| 233 | + path: ty_toml_path, |
| 234 | + source: Box::new(error), |
| 235 | + }); |
| 236 | + } |
| 237 | + }; |
| 238 | + |
| 239 | + if pyproject |
| 240 | + .as_ref() |
| 241 | + .is_some_and(|project| project.ty().is_some()) |
| 242 | + { |
| 243 | + // TODO: Consider using a diagnostic here |
| 244 | + tracing::warn!( |
| 245 | + "Ignoring the `tool.ty` section in `{pyproject_path}` because `{ty_toml_path}` takes precedence." |
| 246 | + ); |
| 247 | + } |
| 248 | + |
| 249 | + tracing::debug!("Found project at '{}'", project_root); |
| 250 | + |
| 251 | + let metadata = ProjectMetadata::from_options( |
| 252 | + options, |
| 253 | + project_root.to_path_buf(), |
| 254 | + pyproject |
| 255 | + .as_ref() |
| 256 | + .and_then(|pyproject| pyproject.project.as_ref()), |
| 257 | + ) |
| 258 | + .map_err( |
| 259 | + |err| ProjectMetadataError::InvalidRequiresPythonConstraint { |
| 260 | + source: err, |
| 261 | + path: pyproject_path, |
| 262 | + }, |
| 263 | + )?; |
| 264 | + |
| 265 | + return Ok(Some(DiscoveredProject::Ty { metadata })); |
| 266 | + } |
| 267 | + |
| 268 | + if let Some(pyproject) = pyproject { |
| 269 | + let has_ty_section = pyproject.ty().is_some(); |
| 270 | + let metadata = ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf()) |
| 271 | + .map_err(|err| { |
| 272 | + ProjectMetadataError::InvalidRequiresPythonConstraint { |
| 273 | + source: err, |
| 274 | + path: pyproject_path, |
| 275 | + } |
| 276 | + })?; |
| 277 | + |
| 278 | + return Ok(Some(DiscoveredProject::PyProject { |
| 279 | + has_ty_section, |
| 280 | + metadata, |
| 281 | + })); |
| 282 | + } |
| 283 | + |
| 284 | + Ok(None) |
| 285 | + } |
| 286 | + |
255 | 287 | pub fn root(&self) -> &SystemPath { |
256 | 288 | &self.root |
257 | 289 | } |
@@ -344,6 +376,36 @@ pub enum ProjectMetadataError { |
344 | 376 | }, |
345 | 377 | } |
346 | 378 |
|
| 379 | +#[derive(Debug)] |
| 380 | +enum DiscoveredProject { |
| 381 | + PyProject { |
| 382 | + has_ty_section: bool, |
| 383 | + metadata: ProjectMetadata, |
| 384 | + }, |
| 385 | + Ty { |
| 386 | + metadata: ProjectMetadata, |
| 387 | + }, |
| 388 | +} |
| 389 | + |
| 390 | +impl DiscoveredProject { |
| 391 | + fn is_ty_or_project_with_ty_section(&self) -> bool { |
| 392 | + match self { |
| 393 | + DiscoveredProject::PyProject { |
| 394 | + has_ty_section: tool_ty, |
| 395 | + .. |
| 396 | + } => *tool_ty, |
| 397 | + DiscoveredProject::Ty { .. } => true, |
| 398 | + } |
| 399 | + } |
| 400 | + |
| 401 | + fn into_metadata(self) -> ProjectMetadata { |
| 402 | + match self { |
| 403 | + DiscoveredProject::PyProject { metadata, .. } => metadata, |
| 404 | + DiscoveredProject::Ty { metadata } => metadata, |
| 405 | + } |
| 406 | + } |
| 407 | +} |
| 408 | + |
347 | 409 | #[cfg(test)] |
348 | 410 | mod tests { |
349 | 411 | //! Integration tests for project discovery |
|
0 commit comments