From 73798327c62257c79effedb2f038a5649911e6b7 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Mon, 20 Jan 2025 14:57:57 +0100 Subject: [PATCH] Flatten `red_knot_project` import paths (#15616) --- crates/red_knot/src/main.rs | 5 +- crates/red_knot/tests/file_watching.rs | 7 +- .../src/{project => }/combine.rs | 2 +- crates/red_knot_project/src/db.rs | 4 +- crates/red_knot_project/src/db/changes.rs | 4 +- .../src/{project => }/files.rs | 6 +- crates/red_knot_project/src/lib.rs | 459 ++++++++++++++++- .../src/{project => }/metadata.rs | 16 +- .../src/{project => metadata}/options.rs | 4 +- .../package_name.rs => metadata/pyproject.rs} | 54 ++ crates/red_knot_project/src/project.rs | 460 ------------------ .../red_knot_project/src/project/pyproject.rs | 59 --- ...sts__nested_projects_in_root_project.snap} | 2 +- ...ests__nested_projects_in_sub_project.snap} | 2 +- ...ted_projects_with_outer_knot_section.snap} | 2 +- ...ested_projects_without_knot_sections.snap} | 2 +- ...project_with_knot_and_pyproject_toml.snap} | 2 +- ...adata__tests__project_with_pyproject.snap} | 2 +- ...ta__tests__project_without_pyproject.snap} | 2 +- crates/red_knot_project/tests/check.rs | 3 +- .../src/server/api/requests/diagnostic.rs | 2 +- .../red_knot_server/src/server/api/traits.rs | 2 +- crates/red_knot_server/src/session.rs | 3 +- crates/red_knot_wasm/src/lib.rs | 6 +- crates/ruff_benchmark/benches/red_knot.rs | 5 +- crates/ruff_macros/src/combine.rs | 4 +- 26 files changed, 553 insertions(+), 566 deletions(-) rename crates/red_knot_project/src/{project => }/combine.rs (99%) rename crates/red_knot_project/src/{project => }/files.rs (98%) rename crates/red_knot_project/src/{project => }/metadata.rs (98%) rename crates/red_knot_project/src/{project => metadata}/options.rs (97%) rename crates/red_knot_project/src/{project/pyproject/package_name.rs => metadata/pyproject.rs} (71%) delete mode 100644 crates/red_knot_project/src/project.rs delete mode 100644 crates/red_knot_project/src/project/pyproject.rs rename crates/red_knot_project/src/{project/snapshots/red_knot_project__project__metadata__tests__nested_projects_in_root_project.snap => snapshots/red_knot_project__metadata__tests__nested_projects_in_root_project.snap} (77%) rename crates/red_knot_project/src/{project/snapshots/red_knot_project__project__metadata__tests__nested_projects_in_sub_project.snap => snapshots/red_knot_project__metadata__tests__nested_projects_in_sub_project.snap} (78%) rename crates/red_knot_project/src/{project/snapshots/red_knot_project__project__metadata__tests__nested_projects_with_outer_knot_section.snap => snapshots/red_knot_project__metadata__tests__nested_projects_with_outer_knot_section.snap} (84%) rename crates/red_knot_project/src/{project/snapshots/red_knot_project__project__metadata__tests__nested_projects_without_knot_sections.snap => snapshots/red_knot_project__metadata__tests__nested_projects_without_knot_sections.snap} (74%) rename crates/red_knot_project/src/{project/snapshots/red_knot_project__project__metadata__tests__project_with_knot_and_pyproject_toml.snap => snapshots/red_knot_project__metadata__tests__project_with_knot_and_pyproject_toml.snap} (76%) rename crates/red_knot_project/src/{project/snapshots/red_knot_project__project__metadata__tests__project_with_pyproject.snap => snapshots/red_knot_project__metadata__tests__project_with_pyproject.snap} (72%) rename crates/red_knot_project/src/{project/snapshots/red_knot_project__project__metadata__tests__project_without_pyproject.snap => snapshots/red_knot_project__metadata__tests__project_without_pyproject.snap} (71%) diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs index 44920e5bb2..212c7b177d 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/red_knot/src/main.rs @@ -6,11 +6,10 @@ use clap::Parser; use colored::Colorize; use crossbeam::channel as crossbeam_channel; use python_version::PythonVersion; -use red_knot_project::db::ProjectDatabase; -use red_knot_project::project::options::{EnvironmentOptions, Options}; -use red_knot_project::project::ProjectMetadata; +use red_knot_project::metadata::options::{EnvironmentOptions, Options}; use red_knot_project::watch; use red_knot_project::watch::ProjectWatcher; +use red_knot_project::{ProjectDatabase, ProjectMetadata}; use red_knot_python_semantic::SitePackages; use red_knot_server::run_server; use ruff_db::diagnostic::Diagnostic; diff --git a/crates/red_knot/tests/file_watching.rs b/crates/red_knot/tests/file_watching.rs index 554f81c5e5..c9277a4691 100644 --- a/crates/red_knot/tests/file_watching.rs +++ b/crates/red_knot/tests/file_watching.rs @@ -4,11 +4,10 @@ use std::io::Write; use std::time::{Duration, Instant}; use anyhow::{anyhow, Context}; -use red_knot_project::db::{Db, ProjectDatabase}; -use red_knot_project::project::options::{EnvironmentOptions, Options}; -use red_knot_project::project::pyproject::{PyProject, Tool}; -use red_knot_project::project::ProjectMetadata; +use red_knot_project::metadata::options::{EnvironmentOptions, Options}; +use red_knot_project::metadata::pyproject::{PyProject, Tool}; use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher}; +use red_knot_project::{Db, ProjectDatabase, ProjectMetadata}; use red_knot_python_semantic::{ resolve_module, ModuleName, PythonPlatform, PythonVersion, SitePackages, }; diff --git a/crates/red_knot_project/src/project/combine.rs b/crates/red_knot_project/src/combine.rs similarity index 99% rename from crates/red_knot_project/src/project/combine.rs rename to crates/red_knot_project/src/combine.rs index f83f9d0c7b..5baf0e4c4c 100644 --- a/crates/red_knot_project/src/project/combine.rs +++ b/crates/red_knot_project/src/combine.rs @@ -148,7 +148,7 @@ impl_noop_combine!(String); #[cfg(test)] mod tests { - use crate::project::combine::Combine; + use crate::combine::Combine; use std::collections::HashMap; #[test] diff --git a/crates/red_knot_project/src/db.rs b/crates/red_knot_project/src/db.rs index 5f38c59d17..0cb7d33d84 100644 --- a/crates/red_knot_project/src/db.rs +++ b/crates/red_knot_project/src/db.rs @@ -1,8 +1,8 @@ use std::panic::RefUnwindSafe; use std::sync::Arc; -use crate::project::{check_file, Project, ProjectMetadata}; use crate::DEFAULT_LINT_REGISTRY; +use crate::{check_file, Project, ProjectMetadata}; use red_knot_python_semantic::lint::{LintRegistry, RuleSelection}; use red_knot_python_semantic::{Db as SemanticDb, Program}; use ruff_db::diagnostic::Diagnostic; @@ -179,8 +179,8 @@ pub(crate) mod tests { use ruff_db::{Db as SourceDb, Upcast}; use crate::db::Db; - use crate::project::{Project, ProjectMetadata}; use crate::DEFAULT_LINT_REGISTRY; + use crate::{Project, ProjectMetadata}; #[salsa::db] #[derive(Clone)] diff --git a/crates/red_knot_project/src/db/changes.rs b/crates/red_knot_project/src/db/changes.rs index 092da8ab7c..a1caf26376 100644 --- a/crates/red_knot_project/src/db/changes.rs +++ b/crates/red_knot_project/src/db/changes.rs @@ -1,7 +1,7 @@ use crate::db::{Db, ProjectDatabase}; -use crate::project::options::Options; -use crate::project::{Project, ProjectMetadata}; +use crate::metadata::options::Options; use crate::watch::{ChangeEvent, CreatedKind, DeletedKind}; +use crate::{Project, ProjectMetadata}; use red_knot_python_semantic::Program; use ruff_db::files::{system_path_to_file, File, Files}; diff --git a/crates/red_knot_project/src/project/files.rs b/crates/red_knot_project/src/files.rs similarity index 98% rename from crates/red_knot_project/src/project/files.rs rename to crates/red_knot_project/src/files.rs index 01ab9ad2de..46707cf14f 100644 --- a/crates/red_knot_project/src/project/files.rs +++ b/crates/red_knot_project/src/files.rs @@ -8,7 +8,7 @@ use salsa::Setter; use ruff_db::files::File; use crate::db::Db; -use crate::project::Project; +use crate::Project; /// Cheap cloneable hash set of files. type FileSet = Arc>; @@ -234,8 +234,8 @@ mod tests { use crate::db::tests::TestDb; use crate::db::Db; - use crate::project::files::Index; - use crate::project::ProjectMetadata; + use crate::files::Index; + use crate::ProjectMetadata; use ruff_db::files::system_path_to_file; use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_python_ast::name::Name; diff --git a/crates/red_knot_project/src/lib.rs b/crates/red_knot_project/src/lib.rs index 5ae74bf0e5..7268745457 100644 --- a/crates/red_knot_project/src/lib.rs +++ b/crates/red_knot_project/src/lib.rs @@ -1,8 +1,31 @@ +#![allow(clippy::ref_option)] + use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder}; use red_knot_python_semantic::register_lints; +use red_knot_python_semantic::types::check_types; +use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity}; +use ruff_db::files::{system_path_to_file, File}; +use ruff_db::parsed::parsed_module; +use ruff_db::source::{source_text, SourceTextError}; +use ruff_db::system::walk_directory::WalkState; +use ruff_db::system::{FileType, SystemPath}; +use ruff_python_ast::PySourceType; +use ruff_text_size::TextRange; +use rustc_hash::{FxBuildHasher, FxHashSet}; +use salsa::Durability; +use salsa::Setter; +use std::borrow::Cow; +use std::sync::Arc; -pub mod db; -pub mod project; +pub use db::{Db, ProjectDatabase}; +use files::{Index, Indexed, IndexedFiles}; +pub use metadata::{ProjectDiscoveryError, ProjectMetadata}; + +pub mod combine; + +mod db; +mod files; +pub mod metadata; pub mod watch; pub static DEFAULT_LINT_REGISTRY: std::sync::LazyLock = @@ -13,3 +36,435 @@ pub fn default_lints_registry() -> LintRegistry { register_lints(&mut builder); builder.build() } + +/// The project as a Salsa ingredient. +/// +/// ## 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 +/// 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 +/// it remains the same project. That's why program is a narrowed view of the project only +/// holding on to the most fundamental settings required for checking. +#[salsa::input] +pub struct Project { + /// The files that are open in the project. + /// + /// Setting the open files to a non-`None` value changes `check` to only check the + /// open files rather than all files in the project. + #[return_ref] + #[default] + open_fileset: Option>>, + + /// The first-party files of this project. + #[default] + #[return_ref] + file_set: IndexedFiles, + + /// The metadata describing the project, including the unresolved options. + #[return_ref] + pub metadata: ProjectMetadata, +} + +impl Project { + pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self { + Project::builder(metadata) + .durability(Durability::MEDIUM) + .open_fileset_durability(Durability::LOW) + .file_set_durability(Durability::LOW) + .new(db) + } + + pub fn root(self, db: &dyn Db) -> &SystemPath { + self.metadata(db).root() + } + + pub fn name(self, db: &dyn Db) -> &str { + self.metadata(db).name() + } + + pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) { + tracing::debug!("Reloading project"); + assert_eq!(self.root(db), metadata.root()); + + if &metadata != self.metadata(db) { + self.set_metadata(db).to(metadata); + } + + self.reload_files(db); + } + + /// Checks all open files in the project and its dependencies. + pub fn check(self, db: &ProjectDatabase) -> Vec> { + let project_span = tracing::debug_span!("Project::check"); + let _span = project_span.enter(); + + tracing::debug!("Checking project '{name}'", name = self.name(db)); + let result = Arc::new(std::sync::Mutex::new(Vec::new())); + let inner_result = Arc::clone(&result); + + let db = db.clone(); + let project_span = project_span.clone(); + + rayon::scope(move |scope| { + let files = ProjectFiles::new(&db, self); + for file in &files { + let result = inner_result.clone(); + let db = db.clone(); + let project_span = project_span.clone(); + + scope.spawn(move |_| { + let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", file=%file.path(&db)); + let _entered = check_file_span.entered(); + + let file_diagnostics = check_file(&db, file); + result.lock().unwrap().extend(file_diagnostics); + }); + } + }); + + Arc::into_inner(result).unwrap().into_inner().unwrap() + } + + /// Opens a file in the project. + /// + /// This changes the behavior of `check` to only check the open files rather than all files in the project. + pub fn open_file(self, db: &mut dyn Db, file: File) { + tracing::debug!("Opening file `{}`", file.path(db)); + + let mut open_files = self.take_open_files(db); + open_files.insert(file); + self.set_open_files(db, open_files); + } + + /// Closes a file in the project. + pub fn close_file(self, db: &mut dyn Db, file: File) -> bool { + tracing::debug!("Closing file `{}`", file.path(db)); + + let mut open_files = self.take_open_files(db); + let removed = open_files.remove(&file); + + if removed { + self.set_open_files(db, open_files); + } + + removed + } + + /// Returns the open files in the project or `None` if the entire project should be checked. + pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet> { + self.open_fileset(db).as_deref() + } + + /// Sets the open files in the project. + /// + /// This changes the behavior of `check` to only check the open files rather than all files in the project. + #[tracing::instrument(level = "debug", skip(self, db))] + pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet) { + tracing::debug!("Set open project files (count: {})", open_files.len()); + + self.set_open_fileset(db).to(Some(Arc::new(open_files))); + } + + /// This takes the open files from the project and returns them. + /// + /// This changes the behavior of `check` to check all files in the project instead of just the open files. + fn take_open_files(self, db: &mut dyn Db) -> FxHashSet { + tracing::debug!("Take open project files"); + + // Salsa will cancel any pending queries and remove its own reference to `open_files` + // so that the reference counter to `open_files` now drops to 1. + let open_files = self.set_open_fileset(db).to(None); + + if let Some(open_files) = open_files { + Arc::try_unwrap(open_files).unwrap() + } else { + FxHashSet::default() + } + } + + /// Returns `true` if the file is open in the project. + /// + /// A file is considered open when: + /// * explicitly set as an open file using [`open_file`](Self::open_file) + /// * It has a [`SystemPath`] and belongs to a package's `src` files + /// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath) + pub fn is_file_open(self, db: &dyn Db, file: File) -> bool { + if let Some(open_files) = self.open_files(db) { + open_files.contains(&file) + } else if file.path(db).is_system_path() { + self.contains_file(db, file) + } else { + file.path(db).is_system_virtual_path() + } + } + + /// Returns `true` if `file` is a first-party file part of this package. + pub fn contains_file(self, db: &dyn Db, file: File) -> bool { + self.files(db).contains(&file) + } + + #[tracing::instrument(level = "debug", skip(db))] + pub fn remove_file(self, db: &mut dyn Db, file: File) { + tracing::debug!( + "Removing file `{}` from project `{}`", + file.path(db), + self.name(db) + ); + + let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { + return; + }; + + index.remove(file); + } + + pub fn add_file(self, db: &mut dyn Db, file: File) { + tracing::debug!( + "Adding file `{}` to project `{}`", + file.path(db), + self.name(db) + ); + + let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { + return; + }; + + index.insert(file); + } + + /// Returns the files belonging to this project. + pub fn files(self, db: &dyn Db) -> Indexed<'_> { + let files = self.file_set(db); + + let indexed = match files.get() { + Index::Lazy(vacant) => { + let _entered = + tracing::debug_span!("Project::index_files", package = %self.name(db)) + .entered(); + + let files = discover_project_files(db, self); + tracing::info!("Found {} files in project `{}`", files.len(), self.name(db)); + vacant.set(files) + } + Index::Indexed(indexed) => indexed, + }; + + indexed + } + + pub fn reload_files(self, db: &mut dyn Db) { + tracing::debug!("Reloading files for project `{}`", self.name(db)); + + if !self.file_set(db).is_lazy() { + // Force a re-index of the files in the next revision. + self.set_file_set(db).to(IndexedFiles::lazy()); + } + } +} + +pub(crate) fn check_file(db: &dyn Db, file: File) -> Vec> { + let mut diagnostics: Vec> = Vec::new(); + // Abort checking if there are IO errors. + let source = source_text(db.upcast(), file); + + if let Some(read_error) = source.read_error() { + diagnostics.push(Box::new(IOErrorDiagnostic { + file, + error: read_error.clone(), + })); + return diagnostics; + } + + let parsed = parsed_module(db.upcast(), file); + diagnostics.extend(parsed.errors().iter().map(|error| { + let diagnostic: Box = Box::new(ParseDiagnostic::new(file, error.clone())); + diagnostic + })); + + diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| { + let boxed: Box = Box::new(diagnostic.clone()); + boxed + })); + + diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start()); + + diagnostics +} + +fn discover_project_files(db: &dyn Db, project: Project) -> FxHashSet { + let paths = std::sync::Mutex::new(Vec::new()); + + db.system().walk_directory(project.root(db)).run(|| { + Box::new(|entry| { + match entry { + Ok(entry) => { + // Skip over any non python files to avoid creating too many entries in `Files`. + match entry.file_type() { + FileType::File => { + if entry + .path() + .extension() + .and_then(PySourceType::try_from_extension) + .is_some() + { + let mut paths = paths.lock().unwrap(); + paths.push(entry.into_path()); + } + } + FileType::Directory | FileType::Symlink => {} + } + } + Err(error) => { + // TODO Handle error + tracing::error!("Failed to walk path: {error}"); + } + } + + WalkState::Continue + }) + }); + + let paths = paths.into_inner().unwrap(); + let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher); + + for path in paths { + // If this returns `None`, then the file was deleted between the `walk_directory` call and now. + // We can ignore this. + if let Ok(file) = system_path_to_file(db.upcast(), &path) { + files.insert(file); + } + } + + files +} + +#[derive(Debug)] +enum ProjectFiles<'a> { + OpenFiles(&'a FxHashSet), + Indexed(files::Indexed<'a>), +} + +impl<'a> ProjectFiles<'a> { + fn new(db: &'a dyn Db, project: Project) -> Self { + if let Some(open_files) = project.open_files(db) { + ProjectFiles::OpenFiles(open_files) + } else { + ProjectFiles::Indexed(project.files(db)) + } + } +} + +impl<'a> IntoIterator for &'a ProjectFiles<'a> { + type Item = File; + type IntoIter = ProjectFilesIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + match self { + ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()), + ProjectFiles::Indexed(indexed) => ProjectFilesIter::Indexed { + files: indexed.into_iter(), + }, + } + } +} + +enum ProjectFilesIter<'db> { + OpenFiles(std::collections::hash_set::Iter<'db, File>), + Indexed { files: files::IndexedIter<'db> }, +} + +impl Iterator for ProjectFilesIter<'_> { + type Item = File; + + fn next(&mut self) -> Option { + match self { + ProjectFilesIter::OpenFiles(files) => files.next().copied(), + ProjectFilesIter::Indexed { files } => files.next(), + } + } +} + +#[derive(Debug)] +pub struct IOErrorDiagnostic { + file: File, + error: SourceTextError, +} + +impl Diagnostic for IOErrorDiagnostic { + fn id(&self) -> DiagnosticId { + DiagnosticId::Io + } + + fn message(&self) -> Cow { + self.error.to_string().into() + } + + fn file(&self) -> File { + self.file + } + + fn range(&self) -> Option { + None + } + + fn severity(&self) -> Severity { + Severity::Error + } +} + +#[cfg(test)] +mod tests { + use crate::db::tests::TestDb; + use crate::{check_file, ProjectMetadata}; + use red_knot_python_semantic::types::check_types; + use ruff_db::diagnostic::Diagnostic; + use ruff_db::files::system_path_to_file; + use ruff_db::source::source_text; + use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf}; + use ruff_db::testing::assert_function_query_was_not_run; + use ruff_python_ast::name::Name; + + #[test] + fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { + let project = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("/")); + let mut db = TestDb::new(project); + let path = SystemPath::new("test.py"); + + db.write_file(path, "x = 10")?; + let file = system_path_to_file(&db, path).unwrap(); + + // Now the file gets deleted before we had a chance to read its source text. + db.memory_file_system().remove_file(path)?; + file.sync(&mut db); + + assert_eq!(source_text(&db, file).as_str(), ""); + assert_eq!( + check_file(&db, file) + .into_iter() + .map(|diagnostic| diagnostic.message().into_owned()) + .collect::>(), + vec!["Failed to read file: No such file or directory".to_string()] + ); + + let events = db.take_salsa_events(); + assert_function_query_was_not_run(&db, check_types, file, &events); + + // The user now creates a new file with an empty text. The source text + // content returned by `source_text` remains unchanged, but the diagnostics should get updated. + db.write_file(path, "").unwrap(); + + assert_eq!(source_text(&db, file).as_str(), ""); + assert_eq!( + check_file(&db, file) + .into_iter() + .map(|diagnostic| diagnostic.message().into_owned()) + .collect::>(), + vec![] as Vec + ); + + Ok(()) + } +} diff --git a/crates/red_knot_project/src/project/metadata.rs b/crates/red_knot_project/src/metadata.rs similarity index 98% rename from crates/red_knot_project/src/project/metadata.rs rename to crates/red_knot_project/src/metadata.rs index 9d30e6ed6b..52e25566b8 100644 --- a/crates/red_knot_project/src/project/metadata.rs +++ b/crates/red_knot_project/src/metadata.rs @@ -1,13 +1,15 @@ +use red_knot_python_semantic::ProgramSettings; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_python_ast::name::Name; - -use crate::project::combine::Combine; -use crate::project::options::Options; -use crate::project::pyproject::{Project, PyProject, PyProjectError}; -use red_knot_python_semantic::ProgramSettings; use thiserror::Error; -use super::options::KnotTomlError; +use crate::combine::Combine; +use crate::metadata::pyproject::{Project, PyProject, PyProjectError}; +use options::KnotTomlError; +use options::Options; + +pub mod options; +pub mod pyproject; #[derive(Debug, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize))] @@ -222,7 +224,7 @@ mod tests { use insta::assert_ron_snapshot; use ruff_db::system::{SystemPathBuf, TestSystem}; - use crate::project::{ProjectDiscoveryError, ProjectMetadata}; + use crate::{ProjectDiscoveryError, ProjectMetadata}; #[test] fn project_without_pyproject() -> anyhow::Result<()> { diff --git a/crates/red_knot_project/src/project/options.rs b/crates/red_knot_project/src/metadata/options.rs similarity index 97% rename from crates/red_knot_project/src/project/options.rs rename to crates/red_knot_project/src/metadata/options.rs index d9db769a73..7337540436 100644 --- a/crates/red_knot_project/src/project/options.rs +++ b/crates/red_knot_project/src/metadata/options.rs @@ -16,12 +16,12 @@ pub struct Options { } impl Options { - pub(super) fn from_toml_str(content: &str) -> Result { + pub(crate) fn from_toml_str(content: &str) -> Result { let options = toml::from_str(content)?; Ok(options) } - pub(super) fn to_program_settings( + pub(crate) fn to_program_settings( &self, project_root: &SystemPath, system: &dyn System, diff --git a/crates/red_knot_project/src/project/pyproject/package_name.rs b/crates/red_knot_project/src/metadata/pyproject.rs similarity index 71% rename from crates/red_knot_project/src/project/pyproject/package_name.rs rename to crates/red_knot_project/src/metadata/pyproject.rs index 4c5c5b91d9..bcf6b2ed95 100644 --- a/crates/red_knot_project/src/project/pyproject/package_name.rs +++ b/crates/red_knot_project/src/metadata/pyproject.rs @@ -1,7 +1,61 @@ +use pep440_rs::{Version, VersionSpecifiers}; use serde::{Deserialize, Deserializer, Serialize}; use std::ops::Deref; use thiserror::Error; +use crate::metadata::options::Options; + +/// A `pyproject.toml` as specified in PEP 517. +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct PyProject { + /// PEP 621-compliant project metadata. + pub project: Option, + /// Tool-specific metadata. + pub tool: Option, +} + +impl PyProject { + pub(crate) fn knot(&self) -> Option<&Options> { + self.tool.as_ref().and_then(|tool| tool.knot.as_ref()) + } +} + +#[derive(Error, Debug)] +pub enum PyProjectError { + #[error(transparent)] + TomlSyntax(#[from] toml::de::Error), +} + +impl PyProject { + pub(crate) fn from_toml_str(content: &str) -> Result { + toml::from_str(content).map_err(PyProjectError::TomlSyntax) + } +} + +/// PEP 621 project metadata (`project`). +/// +/// See . +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub struct Project { + /// The name of the project + /// + /// Note: Intentionally option to be more permissive during deserialization. + /// `PackageMetadata::from_pyproject` reports missing names. + pub name: Option, + /// The version of the project + pub version: Option, + /// The Python versions this project is compatible with. + pub requires_python: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct Tool { + pub knot: Option, +} + /// The normalized name of a package. /// /// Converts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`. diff --git a/crates/red_knot_project/src/project.rs b/crates/red_knot_project/src/project.rs deleted file mode 100644 index 173eedbe48..0000000000 --- a/crates/red_knot_project/src/project.rs +++ /dev/null @@ -1,460 +0,0 @@ -#![allow(clippy::ref_option)] - -use crate::db::Db; -use crate::db::ProjectDatabase; -use crate::project::files::{Index, Indexed, IndexedFiles, IndexedIter}; -pub use metadata::{ProjectDiscoveryError, ProjectMetadata}; - -use red_knot_python_semantic::types::check_types; -use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity}; -use ruff_db::parsed::parsed_module; -use ruff_db::source::{source_text, SourceTextError}; -use ruff_db::system::FileType; -use ruff_db::{ - files::{system_path_to_file, File}, - system::{walk_directory::WalkState, SystemPath}, -}; -use ruff_python_ast::PySourceType; -use ruff_text_size::TextRange; -use rustc_hash::{FxBuildHasher, FxHashSet}; -use salsa::{Durability, Setter as _}; -use std::borrow::Cow; -use std::sync::Arc; - -pub mod combine; -mod files; -mod metadata; -pub mod options; -pub mod pyproject; - -/// The project as a Salsa ingredient. -/// -/// ## 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 -/// 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 -/// it remains the same project. That's why program is a narrowed view of the project only -/// holding on to the most fundamental settings required for checking. -#[salsa::input] -pub struct Project { - /// The files that are open in the project. - /// - /// Setting the open files to a non-`None` value changes `check` to only check the - /// open files rather than all files in the project. - #[return_ref] - #[default] - open_fileset: Option>>, - - /// The first-party files of this project. - #[default] - #[return_ref] - file_set: IndexedFiles, - - /// The metadata describing the project, including the unresolved options. - #[return_ref] - pub metadata: ProjectMetadata, -} - -impl Project { - pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self { - Project::builder(metadata) - .durability(Durability::MEDIUM) - .open_fileset_durability(Durability::LOW) - .file_set_durability(Durability::LOW) - .new(db) - } - - pub fn root(self, db: &dyn Db) -> &SystemPath { - self.metadata(db).root() - } - - pub fn name(self, db: &dyn Db) -> &str { - self.metadata(db).name() - } - - pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) { - tracing::debug!("Reloading project"); - assert_eq!(self.root(db), metadata.root()); - - if &metadata != self.metadata(db) { - self.set_metadata(db).to(metadata); - } - - self.reload_files(db); - } - - /// Checks all open files in the project and its dependencies. - pub fn check(self, db: &ProjectDatabase) -> Vec> { - let project_span = tracing::debug_span!("Project::check"); - let _span = project_span.enter(); - - tracing::debug!("Checking project '{name}'", name = self.name(db)); - let result = Arc::new(std::sync::Mutex::new(Vec::new())); - let inner_result = Arc::clone(&result); - - let db = db.clone(); - let project_span = project_span.clone(); - - rayon::scope(move |scope| { - let files = ProjectFiles::new(&db, self); - for file in &files { - let result = inner_result.clone(); - let db = db.clone(); - let project_span = project_span.clone(); - - scope.spawn(move |_| { - let check_file_span = tracing::debug_span!(parent: &project_span, "check_file", file=%file.path(&db)); - let _entered = check_file_span.entered(); - - let file_diagnostics = check_file(&db, file); - result.lock().unwrap().extend(file_diagnostics); - }); - } - }); - - Arc::into_inner(result).unwrap().into_inner().unwrap() - } - - /// Opens a file in the project. - /// - /// This changes the behavior of `check` to only check the open files rather than all files in the project. - pub fn open_file(self, db: &mut dyn Db, file: File) { - tracing::debug!("Opening file `{}`", file.path(db)); - - let mut open_files = self.take_open_files(db); - open_files.insert(file); - self.set_open_files(db, open_files); - } - - /// Closes a file in the project. - pub fn close_file(self, db: &mut dyn Db, file: File) -> bool { - tracing::debug!("Closing file `{}`", file.path(db)); - - let mut open_files = self.take_open_files(db); - let removed = open_files.remove(&file); - - if removed { - self.set_open_files(db, open_files); - } - - removed - } - - /// Returns the open files in the project or `None` if the entire project should be checked. - pub fn open_files(self, db: &dyn Db) -> Option<&FxHashSet> { - self.open_fileset(db).as_deref() - } - - /// Sets the open files in the project. - /// - /// This changes the behavior of `check` to only check the open files rather than all files in the project. - #[tracing::instrument(level = "debug", skip(self, db))] - pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet) { - tracing::debug!("Set open project files (count: {})", open_files.len()); - - self.set_open_fileset(db).to(Some(Arc::new(open_files))); - } - - /// This takes the open files from the project and returns them. - /// - /// This changes the behavior of `check` to check all files in the project instead of just the open files. - fn take_open_files(self, db: &mut dyn Db) -> FxHashSet { - tracing::debug!("Take open project files"); - - // Salsa will cancel any pending queries and remove its own reference to `open_files` - // so that the reference counter to `open_files` now drops to 1. - let open_files = self.set_open_fileset(db).to(None); - - if let Some(open_files) = open_files { - Arc::try_unwrap(open_files).unwrap() - } else { - FxHashSet::default() - } - } - - /// Returns `true` if the file is open in the project. - /// - /// A file is considered open when: - /// * explicitly set as an open file using [`open_file`](Self::open_file) - /// * It has a [`SystemPath`] and belongs to a package's `src` files - /// * It has a [`SystemVirtualPath`](ruff_db::system::SystemVirtualPath) - pub fn is_file_open(self, db: &dyn Db, file: File) -> bool { - if let Some(open_files) = self.open_files(db) { - open_files.contains(&file) - } else if file.path(db).is_system_path() { - self.contains_file(db, file) - } else { - file.path(db).is_system_virtual_path() - } - } - - /// Returns `true` if `file` is a first-party file part of this package. - pub fn contains_file(self, db: &dyn Db, file: File) -> bool { - self.files(db).contains(&file) - } - - #[tracing::instrument(level = "debug", skip(db))] - pub fn remove_file(self, db: &mut dyn Db, file: File) { - tracing::debug!( - "Removing file `{}` from project `{}`", - file.path(db), - self.name(db) - ); - - let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { - return; - }; - - index.remove(file); - } - - pub fn add_file(self, db: &mut dyn Db, file: File) { - tracing::debug!( - "Adding file `{}` to project `{}`", - file.path(db), - self.name(db) - ); - - let Some(mut index) = IndexedFiles::indexed_mut(db, self) else { - return; - }; - - index.insert(file); - } - - /// Returns the files belonging to this project. - pub fn files(self, db: &dyn Db) -> Indexed<'_> { - let files = self.file_set(db); - - let indexed = match files.get() { - Index::Lazy(vacant) => { - let _entered = - tracing::debug_span!("Project::index_files", package = %self.name(db)) - .entered(); - - let files = discover_project_files(db, self); - tracing::info!("Found {} files in project `{}`", files.len(), self.name(db)); - vacant.set(files) - } - Index::Indexed(indexed) => indexed, - }; - - indexed - } - - pub fn reload_files(self, db: &mut dyn Db) { - tracing::debug!("Reloading files for project `{}`", self.name(db)); - - if !self.file_set(db).is_lazy() { - // Force a re-index of the files in the next revision. - self.set_file_set(db).to(IndexedFiles::lazy()); - } - } -} - -pub(super) fn check_file(db: &dyn Db, file: File) -> Vec> { - let mut diagnostics: Vec> = Vec::new(); - // Abort checking if there are IO errors. - let source = source_text(db.upcast(), file); - - if let Some(read_error) = source.read_error() { - diagnostics.push(Box::new(IOErrorDiagnostic { - file, - error: read_error.clone(), - })); - return diagnostics; - } - - let parsed = parsed_module(db.upcast(), file); - diagnostics.extend(parsed.errors().iter().map(|error| { - let diagnostic: Box = Box::new(ParseDiagnostic::new(file, error.clone())); - diagnostic - })); - - diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| { - let boxed: Box = Box::new(diagnostic.clone()); - boxed - })); - - diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start()); - - diagnostics -} - -fn discover_project_files(db: &dyn Db, project: Project) -> FxHashSet { - let paths = std::sync::Mutex::new(Vec::new()); - - db.system().walk_directory(project.root(db)).run(|| { - Box::new(|entry| { - match entry { - Ok(entry) => { - // Skip over any non python files to avoid creating too many entries in `Files`. - match entry.file_type() { - FileType::File => { - if entry - .path() - .extension() - .and_then(PySourceType::try_from_extension) - .is_some() - { - let mut paths = paths.lock().unwrap(); - paths.push(entry.into_path()); - } - } - FileType::Directory | FileType::Symlink => {} - } - } - Err(error) => { - // TODO Handle error - tracing::error!("Failed to walk path: {error}"); - } - } - - WalkState::Continue - }) - }); - - let paths = paths.into_inner().unwrap(); - let mut files = FxHashSet::with_capacity_and_hasher(paths.len(), FxBuildHasher); - - for path in paths { - // If this returns `None`, then the file was deleted between the `walk_directory` call and now. - // We can ignore this. - if let Ok(file) = system_path_to_file(db.upcast(), &path) { - files.insert(file); - } - } - - files -} - -#[derive(Debug)] -enum ProjectFiles<'a> { - OpenFiles(&'a FxHashSet), - Indexed(Indexed<'a>), -} - -impl<'a> ProjectFiles<'a> { - fn new(db: &'a dyn Db, project: Project) -> Self { - if let Some(open_files) = project.open_files(db) { - ProjectFiles::OpenFiles(open_files) - } else { - ProjectFiles::Indexed(project.files(db)) - } - } -} - -impl<'a> IntoIterator for &'a ProjectFiles<'a> { - type Item = File; - type IntoIter = ProjectFilesIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - match self { - ProjectFiles::OpenFiles(files) => ProjectFilesIter::OpenFiles(files.iter()), - ProjectFiles::Indexed(indexed) => ProjectFilesIter::Indexed { - files: indexed.into_iter(), - }, - } - } -} - -enum ProjectFilesIter<'db> { - OpenFiles(std::collections::hash_set::Iter<'db, File>), - Indexed { files: IndexedIter<'db> }, -} - -impl Iterator for ProjectFilesIter<'_> { - type Item = File; - - fn next(&mut self) -> Option { - match self { - ProjectFilesIter::OpenFiles(files) => files.next().copied(), - ProjectFilesIter::Indexed { files } => files.next(), - } - } -} - -#[derive(Debug)] -pub struct IOErrorDiagnostic { - file: File, - error: SourceTextError, -} - -impl Diagnostic for IOErrorDiagnostic { - fn id(&self) -> DiagnosticId { - DiagnosticId::Io - } - - fn message(&self) -> Cow { - self.error.to_string().into() - } - - fn file(&self) -> File { - self.file - } - - fn range(&self) -> Option { - None - } - - fn severity(&self) -> Severity { - Severity::Error - } -} - -#[cfg(test)] -mod tests { - use crate::db::tests::TestDb; - use crate::project::{check_file, ProjectMetadata}; - use red_knot_python_semantic::types::check_types; - use ruff_db::diagnostic::Diagnostic; - use ruff_db::files::system_path_to_file; - use ruff_db::source::source_text; - use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf}; - use ruff_db::testing::assert_function_query_was_not_run; - use ruff_python_ast::name::Name; - - #[test] - fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> { - let project = ProjectMetadata::new(Name::new_static("test"), SystemPathBuf::from("/")); - let mut db = TestDb::new(project); - let path = SystemPath::new("test.py"); - - db.write_file(path, "x = 10")?; - let file = system_path_to_file(&db, path).unwrap(); - - // Now the file gets deleted before we had a chance to read its source text. - db.memory_file_system().remove_file(path)?; - file.sync(&mut db); - - assert_eq!(source_text(&db, file).as_str(), ""); - assert_eq!( - check_file(&db, file) - .into_iter() - .map(|diagnostic| diagnostic.message().into_owned()) - .collect::>(), - vec!["Failed to read file: No such file or directory".to_string()] - ); - - let events = db.take_salsa_events(); - assert_function_query_was_not_run(&db, check_types, file, &events); - - // The user now creates a new file with an empty text. The source text - // content returned by `source_text` remains unchanged, but the diagnostics should get updated. - db.write_file(path, "").unwrap(); - - assert_eq!(source_text(&db, file).as_str(), ""); - assert_eq!( - check_file(&db, file) - .into_iter() - .map(|diagnostic| diagnostic.message().into_owned()) - .collect::>(), - vec![] as Vec - ); - - Ok(()) - } -} diff --git a/crates/red_knot_project/src/project/pyproject.rs b/crates/red_knot_project/src/project/pyproject.rs deleted file mode 100644 index 84a2ebdccb..0000000000 --- a/crates/red_knot_project/src/project/pyproject.rs +++ /dev/null @@ -1,59 +0,0 @@ -mod package_name; - -use pep440_rs::{Version, VersionSpecifiers}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use crate::project::options::Options; -pub(crate) use package_name::PackageName; - -/// A `pyproject.toml` as specified in PEP 517. -#[derive(Deserialize, Serialize, Debug, Default, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct PyProject { - /// PEP 621-compliant project metadata. - pub project: Option, - /// Tool-specific metadata. - pub tool: Option, -} - -impl PyProject { - pub(crate) fn knot(&self) -> Option<&Options> { - self.tool.as_ref().and_then(|tool| tool.knot.as_ref()) - } -} - -#[derive(Error, Debug)] -pub enum PyProjectError { - #[error(transparent)] - TomlSyntax(#[from] toml::de::Error), -} - -impl PyProject { - pub(crate) fn from_toml_str(content: &str) -> Result { - toml::from_str(content).map_err(PyProjectError::TomlSyntax) - } -} - -/// PEP 621 project metadata (`project`). -/// -/// See . -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "kebab-case")] -pub struct Project { - /// The name of the project - /// - /// Note: Intentionally option to be more permissive during deserialization. - /// `PackageMetadata::from_pyproject` reports missing names. - pub name: Option, - /// The version of the project - pub version: Option, - /// The Python versions this project is compatible with. - pub requires_python: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct Tool { - pub knot: Option, -} diff --git a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_in_root_project.snap b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_in_root_project.snap similarity index 77% rename from crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_in_root_project.snap rename to crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_in_root_project.snap index b799a45bcd..4575e512ee 100644 --- a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_in_root_project.snap +++ b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_in_root_project.snap @@ -1,5 +1,5 @@ --- -source: crates/red_knot_workspace/src/project/metadata.rs +source: crates/red_knot_project/src/metadata.rs expression: root --- ProjectMetadata( diff --git a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_in_sub_project.snap b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_in_sub_project.snap similarity index 78% rename from crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_in_sub_project.snap rename to crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_in_sub_project.snap index 2b92013e8e..7ac807ac1e 100644 --- a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_in_sub_project.snap +++ b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_in_sub_project.snap @@ -1,5 +1,5 @@ --- -source: crates/red_knot_workspace/src/project/metadata.rs +source: crates/red_knot_project/src/metadata.rs expression: sub_project --- ProjectMetadata( diff --git a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_with_outer_knot_section.snap b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_with_outer_knot_section.snap similarity index 84% rename from crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_with_outer_knot_section.snap rename to crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_with_outer_knot_section.snap index 11555f615a..7ffc98a58f 100644 --- a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_with_outer_knot_section.snap +++ b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_with_outer_knot_section.snap @@ -1,5 +1,5 @@ --- -source: crates/red_knot_workspace/src/project/metadata.rs +source: crates/red_knot_project/src/metadata.rs expression: root --- ProjectMetadata( diff --git a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_without_knot_sections.snap b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_without_knot_sections.snap similarity index 74% rename from crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_without_knot_sections.snap rename to crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_without_knot_sections.snap index f0e4f6b4b4..33ce4573c2 100644 --- a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__nested_projects_without_knot_sections.snap +++ b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__nested_projects_without_knot_sections.snap @@ -1,5 +1,5 @@ --- -source: crates/red_knot_workspace/src/project/metadata.rs +source: crates/red_knot_project/src/metadata.rs expression: sub_project --- ProjectMetadata( diff --git a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_with_knot_and_pyproject_toml.snap b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_with_knot_and_pyproject_toml.snap similarity index 76% rename from crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_with_knot_and_pyproject_toml.snap rename to crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_with_knot_and_pyproject_toml.snap index 2ba17fbeec..b4321e8b8e 100644 --- a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_with_knot_and_pyproject_toml.snap +++ b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_with_knot_and_pyproject_toml.snap @@ -1,5 +1,5 @@ --- -source: crates/red_knot_workspace/src/project/metadata.rs +source: crates/red_knot_project/src/metadata.rs expression: root --- ProjectMetadata( diff --git a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_with_pyproject.snap b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_with_pyproject.snap similarity index 72% rename from crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_with_pyproject.snap rename to crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_with_pyproject.snap index 23b4878a11..0638de89fe 100644 --- a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_with_pyproject.snap +++ b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_with_pyproject.snap @@ -1,5 +1,5 @@ --- -source: crates/red_knot_workspace/src/project/metadata.rs +source: crates/red_knot_project/src/metadata.rs expression: project --- ProjectMetadata( diff --git a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_without_pyproject.snap b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_without_pyproject.snap similarity index 71% rename from crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_without_pyproject.snap rename to crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_without_pyproject.snap index c168486330..7bbb775bee 100644 --- a/crates/red_knot_project/src/project/snapshots/red_knot_project__project__metadata__tests__project_without_pyproject.snap +++ b/crates/red_knot_project/src/snapshots/red_knot_project__metadata__tests__project_without_pyproject.snap @@ -1,5 +1,5 @@ --- -source: crates/red_knot_workspace/src/project/metadata.rs +source: crates/red_knot_project/src/metadata.rs expression: project --- ProjectMetadata( diff --git a/crates/red_knot_project/tests/check.rs b/crates/red_knot_project/tests/check.rs index f2d5bb1b83..551f9ded82 100644 --- a/crates/red_knot_project/tests/check.rs +++ b/crates/red_knot_project/tests/check.rs @@ -1,6 +1,5 @@ use anyhow::{anyhow, Context}; -use red_knot_project::db::ProjectDatabase; -use red_knot_project::project::ProjectMetadata; +use red_knot_project::{ProjectDatabase, ProjectMetadata}; use red_knot_python_semantic::{HasTy, SemanticModel}; use ruff_db::files::{system_path_to_file, File}; use ruff_db::parsed::parsed_module; diff --git a/crates/red_knot_server/src/server/api/requests/diagnostic.rs b/crates/red_knot_server/src/server/api/requests/diagnostic.rs index 1d72e90af5..c93467a911 100644 --- a/crates/red_knot_server/src/server/api/requests/diagnostic.rs +++ b/crates/red_knot_server/src/server/api/requests/diagnostic.rs @@ -11,7 +11,7 @@ use crate::edit::ToRangeExt; use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; use crate::server::{client::Notifier, Result}; use crate::session::DocumentSnapshot; -use red_knot_project::db::{Db, ProjectDatabase}; +use red_knot_project::{Db, ProjectDatabase}; use ruff_db::diagnostic::Severity; use ruff_db::source::{line_index, source_text}; diff --git a/crates/red_knot_server/src/server/api/traits.rs b/crates/red_knot_server/src/server/api/traits.rs index 07d225a09d..3d95d11f0e 100644 --- a/crates/red_knot_server/src/server/api/traits.rs +++ b/crates/red_knot_server/src/server/api/traits.rs @@ -5,7 +5,7 @@ use crate::session::{DocumentSnapshot, Session}; use lsp_types::notification::Notification as LSPNotification; use lsp_types::request::Request; -use red_knot_project::db::ProjectDatabase; +use red_knot_project::ProjectDatabase; /// A supertrait for any server request handler. pub(super) trait RequestHandler { diff --git a/crates/red_knot_server/src/session.rs b/crates/red_knot_server/src/session.rs index 38b30b12e2..3ad3869cc7 100644 --- a/crates/red_knot_server/src/session.rs +++ b/crates/red_knot_server/src/session.rs @@ -8,8 +8,7 @@ use std::sync::Arc; use anyhow::anyhow; use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url}; -use red_knot_project::db::ProjectDatabase; -use red_knot_project::project::ProjectMetadata; +use red_knot_project::{ProjectDatabase, ProjectMetadata}; use ruff_db::files::{system_path_to_file, File}; use ruff_db::system::SystemPath; use ruff_db::Db; diff --git a/crates/red_knot_wasm/src/lib.rs b/crates/red_knot_wasm/src/lib.rs index e2dd7c98c2..27b2a580e5 100644 --- a/crates/red_knot_wasm/src/lib.rs +++ b/crates/red_knot_wasm/src/lib.rs @@ -3,9 +3,9 @@ use std::any::Any; use js_sys::Error; use wasm_bindgen::prelude::*; -use red_knot_project::db::{Db, ProjectDatabase}; -use red_knot_project::project::options::{EnvironmentOptions, Options}; -use red_knot_project::project::ProjectMetadata; +use red_knot_project::metadata::options::{EnvironmentOptions, Options}; +use red_knot_project::ProjectMetadata; +use red_knot_project::{Db, ProjectDatabase}; use ruff_db::diagnostic::Diagnostic; use ruff_db::files::{system_path_to_file, File}; use ruff_db::system::walk_directory::WalkDirectoryBuilder; diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index 0fb01eed1d..4c6822ed9a 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -1,10 +1,9 @@ #![allow(clippy::disallowed_names)] use rayon::ThreadPoolBuilder; -use red_knot_project::db::{Db, ProjectDatabase}; -use red_knot_project::project::options::{EnvironmentOptions, Options}; -use red_knot_project::project::ProjectMetadata; +use red_knot_project::metadata::options::{EnvironmentOptions, Options}; use red_knot_project::watch::{ChangeEvent, ChangedKind}; +use red_knot_project::{Db, ProjectDatabase, ProjectMetadata}; use red_knot_python_semantic::PythonVersion; use ruff_benchmark::criterion::{criterion_group, criterion_main, BatchSize, Criterion}; use ruff_benchmark::TestFile; diff --git a/crates/ruff_macros/src/combine.rs b/crates/ruff_macros/src/combine.rs index 7beb2563fd..01b3635272 100644 --- a/crates/ruff_macros/src/combine.rs +++ b/crates/ruff_macros/src/combine.rs @@ -19,14 +19,14 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result crate::project::combine::Combine::combine_with(&mut self.#ident, other.#ident) + ident.span() => crate::combine::Combine::combine_with(&mut self.#ident, other.#ident) ) }) .collect(); Ok(quote! { #[automatically_derived] - impl crate::project::combine::Combine for #ident { + impl crate::combine::Combine for #ident { fn combine_with(&mut self, other: Self) { #( #output