diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index 5f2d1e08c38a4..9fb138553b27f 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -10,7 +10,6 @@ use ruff_linter::{warn_user, warn_user_once}; use ruff_python_ast::{PySourceType, SourceType}; use ruff_workspace::resolver::{python_files_in_path, ResolvedFile}; use rustc_hash::FxHashMap; -use std::collections::BTreeMap; use std::path::Path; use std::sync::Arc; @@ -53,13 +52,14 @@ pub(crate) fn analyze_graph( .collect::>(); // Create a database for each source root. - let databases = package_roots - .values() - .filter_map(|package| package.as_deref()) - .filter_map(|package| package.parent()) - .map(Path::to_path_buf) - .map(|source_root| Ok((source_root.clone(), ModuleDb::from_src_root(source_root)?))) - .collect::>>()?; + let db = ModuleDb::from_src_roots( + package_roots + .values() + .filter_map(|package| package.as_deref()) + .filter_map(|package| package.parent()) + .map(Path::to_path_buf) + .filter_map(|path| SystemPathBuf::from_path_buf(path).ok()), + )?; // Collect and resolve the imports for each file. let result = Arc::new(std::sync::Mutex::new(Vec::new())); @@ -76,17 +76,6 @@ pub(crate) fn analyze_graph( .parent() .and_then(|parent| package_roots.get(parent)) .and_then(Clone::clone); - let Some(src_root) = package - .as_ref() - .and_then(|package| package.parent()) - .map(Path::to_path_buf) - else { - debug!("Ignoring file outside of source root: {}", path.display()); - continue; - }; - let Some(db) = databases.get(&src_root).map(ModuleDb::snapshot) else { - continue; - }; // Resolve the per-file settings. let settings = resolver.resolve(&path); @@ -118,8 +107,9 @@ pub(crate) fn analyze_graph( warn!("Failed to convert path to system path"); continue; }; - let root = root.clone(); + let db = db.snapshot(); + let root = root.clone(); let result = inner_result.clone(); scope.spawn(move |_| { // Identify any imports via static analysis. diff --git a/crates/ruff/tests/analyze_graph.rs b/crates/ruff/tests/analyze_graph.rs index ee097224cf9d4..81901eefc1fac 100644 --- a/crates/ruff/tests/analyze_graph.rs +++ b/crates/ruff/tests/analyze_graph.rs @@ -19,6 +19,11 @@ fn command() -> Command { command } +const INSTA_FILTERS: &[(&str, &str)] = &[ + // Rewrite Windows output to Unix output + (r"\\", "/"), +]; + #[test] fn dependencies() -> Result<()> { let tempdir = TempDir::new()?; @@ -51,29 +56,33 @@ fn dependencies() -> Result<()> { def f(): pass "#})?; - assert_cmd_snapshot!(command().current_dir(&root), @r###" - success: true - exit_code: 0 - ----- stdout ----- - { - "ruff/__init__.py": [], - "ruff/a.py": [ - "ruff/b.py" - ], - "ruff/b.py": [ - "ruff/c.py" - ], - "ruff/c.py": [ - "ruff/d.py" - ], - "ruff/d.py": [ - "ruff/e.py" - ], - "ruff/e.py": [] - } + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!(command().current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [ + "ruff/b.py" + ], + "ruff/b.py": [ + "ruff/c.py" + ], + "ruff/c.py": [ + "ruff/d.py" + ], + "ruff/d.py": [ + "ruff/e.py" + ], + "ruff/e.py": [] + } - ----- stderr ----- - "###); + ----- stderr ----- + "###); + }); Ok(()) } @@ -111,29 +120,33 @@ fn dependents() -> Result<()> { def f(): pass "#})?; - assert_cmd_snapshot!(command().arg("--direction").arg("dependents").current_dir(&root), @r###" - success: true - exit_code: 0 - ----- stdout ----- - { - "ruff/__init__.py": [], - "ruff/a.py": [], - "ruff/b.py": [ - "ruff/a.py" - ], - "ruff/c.py": [ - "ruff/b.py" - ], - "ruff/d.py": [ - "ruff/c.py" - ], - "ruff/e.py": [ - "ruff/d.py" - ] - } + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!(command().arg("--direction").arg("dependents").current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [], + "ruff/b.py": [ + "ruff/a.py" + ], + "ruff/c.py": [ + "ruff/b.py" + ], + "ruff/d.py": [ + "ruff/c.py" + ], + "ruff/e.py": [ + "ruff/d.py" + ] + } - ----- stderr ----- - "###); + ----- stderr ----- + "###); + }); Ok(()) } @@ -159,39 +172,47 @@ fn string_detection() -> Result<()> { "#})?; root.child("ruff").child("c.py").write_str("")?; - assert_cmd_snapshot!(command().current_dir(&root), @r###" - success: true - exit_code: 0 - ----- stdout ----- - { - "ruff/__init__.py": [], - "ruff/a.py": [ - "ruff/b.py" - ], - "ruff/b.py": [], - "ruff/c.py": [] - } + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!(command().current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [ + "ruff/b.py" + ], + "ruff/b.py": [], + "ruff/c.py": [] + } - ----- stderr ----- - "###); + ----- stderr ----- + "###); + }); - assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###" - success: true - exit_code: 0 - ----- stdout ----- - { - "ruff/__init__.py": [], - "ruff/a.py": [ - "ruff/b.py" - ], - "ruff/b.py": [ - "ruff/c.py" - ], - "ruff/c.py": [] - } + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [ + "ruff/b.py" + ], + "ruff/b.py": [ + "ruff/c.py" + ], + "ruff/c.py": [] + } - ----- stderr ----- - "###); + ----- stderr ----- + "###); + }); Ok(()) } @@ -212,26 +233,30 @@ fn globs() -> Result<()> { root.child("ruff").child("b.py").write_str("")?; root.child("ruff").child("c.py").write_str("")?; - assert_cmd_snapshot!(command().current_dir(&root), @r###" - success: true - exit_code: 0 - ----- stdout ----- - { - "ruff/__init__.py": [], - "ruff/a.py": [ - "ruff/b.py" - ], - "ruff/b.py": [ - "ruff/__init__.py", - "ruff/a.py", - "ruff/b.py", - "ruff/c.py" - ], - "ruff/c.py": [] - } - - ----- stderr ----- - "###); + insta::with_settings!({ + filters => INSTA_FILTERS.to_vec(), + }, { + assert_cmd_snapshot!(command().current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [ + "ruff/b.py" + ], + "ruff/b.py": [ + "ruff/__init__.py", + "ruff/a.py", + "ruff/b.py", + "ruff/c.py" + ], + "ruff/c.py": [] + } + + ----- stderr ----- + "###); + }); Ok(()) } diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index 85b011de4b024..e5ce1e0541ffc 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -388,9 +388,10 @@ formatter.magic_trailing_comma = respect formatter.docstring_code_format = disabled formatter.docstring_code_line_width = dynamic -# Import Map Settings -import_map.detect_string_imports = false -import_map.extension = ExtensionMapping({}) -import_map.include_dependencies = {} +# Analyze Settings +analyze.preview = disabled +analyze.detect_string_imports = false +analyze.extension = ExtensionMapping({}) +analyze.include_dependencies = {} ----- stderr ----- diff --git a/crates/ruff_graph/src/db.rs b/crates/ruff_graph/src/db.rs index 194a0cc9286a1..9e786eee0549b 100644 --- a/crates/ruff_graph/src/db.rs +++ b/crates/ruff_graph/src/db.rs @@ -4,7 +4,6 @@ use ruff_db::files::{File, Files}; use ruff_db::system::{OsSystem, System, SystemPathBuf}; use ruff_db::vendored::VendoredFileSystem; use ruff_db::{Db as SourceDb, Upcast}; -use std::path::PathBuf; #[salsa::db] #[derive(Default)] @@ -17,21 +16,36 @@ pub struct ModuleDb { impl ModuleDb { /// Initialize a [`ModuleDb`] from the given source root. - pub fn from_src_root(src_root: PathBuf) -> Result { + pub fn from_src_roots(mut src_roots: impl Iterator) -> Result { + let search_paths = { + // Use the first source root. + let src_root = src_roots + .next() + .ok_or_else(|| anyhow::anyhow!("No source roots provided"))?; + + let mut search_paths = SearchPathSettings::new(src_root.to_path_buf()); + + // Add the remaining source roots as extra paths. + for src_root in src_roots { + search_paths.extra_paths.push(src_root.to_path_buf()); + } + + search_paths + }; + let db = Self::default(); Program::from_settings( &db, &ProgramSettings { target_version: PythonVersion::default(), - search_paths: SearchPathSettings::new( - SystemPathBuf::from_path_buf(src_root) - .map_err(|path| anyhow::anyhow!("Invalid path: {}", path.display()))?, - ), + search_paths, }, )?; + Ok(db) } + /// Create a snapshot of the current database. #[must_use] pub fn snapshot(&self) -> Self { Self { diff --git a/crates/ruff_graph/src/lib.rs b/crates/ruff_graph/src/lib.rs index 8d406c78d87f5..3d6f92c7ef3a5 100644 --- a/crates/ruff_graph/src/lib.rs +++ b/crates/ruff_graph/src/lib.rs @@ -21,14 +21,17 @@ mod settings; pub struct ModuleImports(BTreeSet); impl ModuleImports { + /// Insert a file path into the module imports. pub fn insert(&mut self, path: SystemPathBuf) { self.0.insert(path); } + /// Returns `true` if the module imports are empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } + /// Returns the number of module imports. pub fn len(&self) -> usize { self.0.len() } @@ -36,12 +39,17 @@ impl ModuleImports { /// Convert the file paths to be relative to a given path. #[must_use] pub fn relative_to(self, path: &SystemPath) -> Self { - Self(BTreeSet::from_iter(self.0.into_iter().map(|import| { - import - .strip_prefix(path) - .map(SystemPath::to_path_buf) - .unwrap_or(import) - }))) + Self( + self.0 + .into_iter() + .map(|import| { + import + .strip_prefix(path) + .map(SystemPath::to_path_buf) + .unwrap_or(import) + }) + .collect(), + ) } } @@ -50,10 +58,12 @@ impl ModuleImports { pub struct ImportMap(BTreeMap); impl ImportMap { + /// Insert a module's imports into the map. pub fn insert(&mut self, path: SystemPathBuf, imports: ModuleImports) { self.0.insert(path, imports); } + /// Reverse the [`ImportMap`], e.g., to convert from dependencies to dependents. #[must_use] pub fn reverse(imports: impl IntoIterator) -> Self { let mut reverse = ImportMap::default(); diff --git a/crates/ruff_graph/src/resolver.rs b/crates/ruff_graph/src/resolver.rs index 6fe463b5094dc..1de2968eb7278 100644 --- a/crates/ruff_graph/src/resolver.rs +++ b/crates/ruff_graph/src/resolver.rs @@ -9,10 +9,12 @@ pub(crate) struct Resolver<'a> { } impl<'a> Resolver<'a> { + /// Initialize a [`Resolver`] with a given [`SemanticModel`]. pub(crate) fn new(semantic: &'a SemanticModel<'a>) -> Self { Self { semantic } } + /// Resolve the [`CollectedImport`] into a [`FilePath`]. pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> { match import { CollectedImport::Import(import) => self