diff --git a/src/commands.rs b/src/commands.rs index 7134fd0661..2d2181fe4e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,13 +1,12 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, Result}; +use itertools::Itertools; use serde::Serialize; -use walkdir::DirEntry; use crate::checks::CheckCode; use crate::cli::Overrides; -use crate::fs::iter_python_files; -use crate::resolver::{discover_settings, Resolver}; +use crate::fs::collect_python_files; use crate::settings::types::SerializationFormat; use crate::{Configuration, Settings}; @@ -24,23 +23,15 @@ pub fn show_settings( /// Show the list of files to be checked based on current settings. pub fn show_files(files: &[PathBuf], default: &Settings, overrides: &Overrides) { - // Discover the settings for the filesystem hierarchy. - let settings = discover_settings(files, overrides); - let resolver = Resolver { - default, - settings: &settings, - }; - // Collect all files in the hierarchy. - let mut entries: Vec = files - .iter() - .flat_map(|path| iter_python_files(path, &resolver)) - .flatten() - .collect(); - entries.sort_by(|a, b| a.path().cmp(b.path())); + let (paths, _resolver) = collect_python_files(files, overrides, default); // Print the list of files. - for entry in entries { + for entry in paths + .iter() + .flatten() + .sorted_by(|a, b| a.path().cmp(b.path())) + { println!("{}", entry.path().to_string_lossy()); } } diff --git a/src/fs.rs b/src/fs.rs index 285c4ea499..4b7c390b5c 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -5,13 +5,16 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; use globset::GlobMatcher; -use log::debug; +use log::{debug, error}; use path_absolutize::{path_dedot, Absolutize}; use rustc_hash::FxHashSet; use walkdir::{DirEntry, WalkDir}; use crate::checks::CheckCode; +use crate::cli::Overrides; +use crate::resolver; use crate::resolver::Resolver; +use crate::settings::Settings; /// Extract the absolute path and basename (as strings) from a Path. fn extract_path_names(path: &Path) -> Result<(&str, &str)> { @@ -35,59 +38,71 @@ fn is_included(path: &Path) -> bool { .map_or(false, |ext| ext == "py" || ext == "pyi") } -/// Find all `pyproject.toml` files for a given `Path`. Both parents and -/// children will be included in the resulting `Vec`. -pub fn iter_pyproject_files(path: &Path) -> Vec { - let mut paths = Vec::new(); +/// Find all Python (`.py` and `.pyi` files) in a set of `Path`. +pub fn collect_python_files<'a>( + paths: &'a [PathBuf], + overrides: &'a Overrides, + default: &'a Settings, +) -> (Vec>, Resolver) { + let mut files = Vec::new(); + let mut resolver = Resolver::default(); + for path in paths { + let (files_in_path, file_resolver) = python_files_in_path(path, overrides, default); + files.extend(files_in_path); + resolver.merge(file_resolver); + } + (files, resolver) +} + +/// Find all Python (`.py` and `.pyi` files) in a given `Path`. +fn python_files_in_path<'a>( + path: &'a Path, + overrides: &'a Overrides, + default: &'a Settings, +) -> (Vec>, Resolver) { + let path = normalize_path(path); // Search for `pyproject.toml` files in all parent directories. - let path = normalize_path(path); + let mut resolver = Resolver::default(); for path in path.ancestors() { if path.is_dir() { - let toml_path = path.join("pyproject.toml"); - if toml_path.exists() { - paths.push(toml_path); + let pyproject = path.join("pyproject.toml"); + if pyproject.is_file() { + match resolver::settings_for_path(&pyproject, overrides) { + Ok((root, settings)) => resolver.add(root, settings), + Err(err) => error!("Failed to read settings: {err}"), + } } } } - // Search for `pyproject.toml` files in all child directories. - for path in WalkDir::new(path) + // Collect all Python files. + let files: Vec> = WalkDir::new(path) .into_iter() .filter_entry(|entry| { - entry.file_name().to_str().map_or(false, |file_name| { - entry.depth() == 0 || !file_name.starts_with('.') - }) - }) - .filter_map(std::result::Result::ok) - .filter(|entry| entry.path().ends_with("pyproject.toml")) - { - paths.push(path.into_path()); - } + // Search for the `pyproject.toml` file in this directory, before we visit any + // of its contents. + if entry.file_type().is_dir() { + let pyproject = entry.path().join("pyproject.toml"); + if pyproject.is_file() { + match resolver::settings_for_path(&pyproject, overrides) { + Ok((root, settings)) => resolver.add(root, settings), + Err(err) => error!("Failed to read settings: {err}"), + } + } + } - paths -} - -/// Find all Python (`.py` and `.pyi` files) in a given `Path`. -pub fn iter_python_files<'a>( - path: &'a Path, - resolver: &'a Resolver<'a>, -) -> impl Iterator> + 'a { - WalkDir::new(normalize_path(path)) - .into_iter() - .filter_entry(move |entry| { let path = entry.path(); - let settings = resolver.resolve(path); - let exclude = &settings.exclude; - let extend_exclude = &settings.extend_exclude; - + let settings = resolver.resolve(path).unwrap_or(default); match extract_path_names(path) { Ok((file_path, file_basename)) => { - if !exclude.is_empty() && is_excluded(file_path, file_basename, exclude) { + if !settings.exclude.is_empty() + && is_excluded(file_path, file_basename, &settings.exclude) + { debug!("Ignored path via `exclude`: {:?}", path); false - } else if !extend_exclude.is_empty() - && is_excluded(file_path, file_basename, extend_exclude) + } else if !settings.extend_exclude.is_empty() + && is_excluded(file_path, file_basename, &settings.extend_exclude) { debug!("Ignored path via `extend-exclude`: {:?}", path); false @@ -108,6 +123,9 @@ pub fn iter_python_files<'a>( && !(entry.file_type().is_symlink() && entry.path().is_dir()) }) }) + .collect::>(); + + (files, resolver) } /// Create tree set with codes matching the pattern/code pairs. diff --git a/src/linter.rs b/src/linter.rs index 56587be811..6edfdcfd8e 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -211,7 +211,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result { } /// Apply autoformatting to the source code at the given `Path`. -pub fn autoformat_path(path: &Path) -> Result<()> { +pub fn autoformat_path(path: &Path, _settings: &Settings) -> Result<()> { // Read the file from disk. let contents = fs::read_file(path)?; diff --git a/src/main.rs b/src/main.rs index 6c4abf1024..25da49ffb0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,13 +20,12 @@ use std::time::Instant; use ::ruff::autofix::fixer; use ::ruff::checks::{CheckCode, CheckKind}; use ::ruff::cli::{extract_log_level, Cli, Overrides}; -use ::ruff::fs::iter_python_files; +use ::ruff::fs::collect_python_files; use ::ruff::iterators::par_iter; use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics}; use ::ruff::logging::{set_up_logging, LogLevel}; use ::ruff::message::Message; use ::ruff::printer::Printer; -use ::ruff::resolver::Resolver; use ::ruff::settings::configuration::Configuration; use ::ruff::settings::types::SerializationFormat; use ::ruff::settings::{pyproject, Settings}; @@ -40,9 +39,7 @@ use log::{debug, error}; use notify::{recommended_watcher, RecursiveMode, Watcher}; #[cfg(not(target_family = "wasm"))] use rayon::prelude::*; -use ruff::resolver::discover_settings; use rustpython_ast::Location; -use walkdir::DirEntry; fn read_from_stdin() -> Result { let mut buffer = String::new(); @@ -68,19 +65,9 @@ fn run_once( cache: bool, autofix: &fixer::Mode, ) -> Diagnostics { - // Discover the settings for the filesystem hierarchy. - let settings = discover_settings(files, overrides); - let resolver = Resolver { - default, - settings: &settings, - }; - // Collect all the files to check. let start = Instant::now(); - let paths: Vec> = files - .iter() - .flat_map(|path| iter_python_files(path, &resolver)) - .collect(); + let (paths, resolver) = collect_python_files(files, overrides, default); let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); @@ -90,7 +77,7 @@ fn run_once( match entry { Ok(entry) => { let path = entry.path(); - let settings = resolver.resolve(path); + let settings = resolver.resolve(path).unwrap_or(default); lint_path(path, settings, &cache.into(), autofix) .map_err(|e| (Some(path.to_owned()), e.to_string())) } @@ -101,8 +88,8 @@ fn run_once( )), } .unwrap_or_else(|(path, message)| { - if let Some(path) = path { - let settings = resolver.resolve(&path); + if let Some(path) = &path { + let settings = resolver.resolve(path).unwrap_or(default); if settings.enabled.contains(&CheckCode::E902) { Diagnostics::new(vec![Message { kind: CheckKind::IOError(message), @@ -135,28 +122,18 @@ fn run_once( } fn add_noqa(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usize { - // Discover the settings for the filesystem hierarchy. - let settings = discover_settings(files, overrides); - let resolver = Resolver { - default, - settings: &settings, - }; - // Collect all the files to check. let start = Instant::now(); - let paths: Vec = files - .iter() - .flat_map(|path| iter_python_files(path, &resolver)) - .flatten() - .collect(); + let (paths, resolver) = collect_python_files(files, overrides, default); let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); let start = Instant::now(); let modifications: usize = par_iter(&paths) + .flatten() .filter_map(|entry| { let path = entry.path(); - let settings = resolver.resolve(path); + let settings = resolver.resolve(path).unwrap_or(default); match add_noqa_to_path(path, settings) { Ok(count) => Some(count), Err(e) => { @@ -174,28 +151,19 @@ fn add_noqa(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usi } fn autoformat(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usize { - // Discover the settings for the filesystem hierarchy. - let settings = discover_settings(files, overrides); - let resolver = Resolver { - default, - settings: &settings, - }; - // Collect all the files to format. let start = Instant::now(); - let paths: Vec = files - .iter() - .flat_map(|path| iter_python_files(path, &resolver)) - .flatten() - .collect(); + let (paths, resolver) = collect_python_files(files, overrides, default); let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); let start = Instant::now(); let modifications = par_iter(&paths) + .flatten() .filter_map(|entry| { let path = entry.path(); - match autoformat_path(path) { + let settings = resolver.resolve(path).unwrap_or(default); + match autoformat_path(path, settings) { Ok(()) => Some(()), Err(e) => { error!("Failed to autoformat {}: {e}", path.to_string_lossy()); diff --git a/src/resolver.rs b/src/resolver.rs index feac81003c..9fdde0976a 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,27 +1,36 @@ //! Discover and resolve `Settings` from the filesystem hierarchy. -use std::cmp::Reverse; +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; -use log::error; use crate::cli::Overrides; -use crate::fs::iter_pyproject_files; use crate::settings::configuration::Configuration; use crate::settings::{pyproject, Settings}; -pub struct Resolver<'a> { - pub settings: &'a [(PathBuf, Settings)], - pub default: &'a Settings, +#[derive(Default)] +pub struct Resolver { + settings: BTreeMap, } -impl<'a> Resolver<'a> { - pub fn resolve(&'a self, path: &Path) -> &'a Settings { - self.settings - .iter() - .find(|(root, _)| path.starts_with(root)) - .map_or(self.default, |(_, settings)| settings) +impl Resolver { + pub fn merge(&mut self, resolver: Resolver) { + self.settings.extend(resolver.settings); + } + + pub fn add(&mut self, path: PathBuf, settings: Settings) { + self.settings.insert(path, settings); + } + + pub fn resolve(&self, path: &Path) -> Option<&Settings> { + self.settings.iter().rev().find_map(|(root, settings)| { + if path.starts_with(root) { + Some(settings) + } else { + None + } + }) } } @@ -37,26 +46,3 @@ pub fn settings_for_path(pyproject: &Path, overrides: &Overrides) -> Result<(Pat let settings = Settings::from_configuration(configuration, Some(&project_root))?; Ok((project_root, settings)) } - -/// Discover all `Settings` objects within the relevant filesystem hierarchy. -pub fn discover_settings(files: &[PathBuf], overrides: &Overrides) -> Vec<(PathBuf, Settings)> { - // Collect all `pyproject.toml` files. - let mut pyprojects: Vec = files - .iter() - .flat_map(|path| iter_pyproject_files(path)) - .collect(); - pyprojects.sort_unstable_by_key(|path| Reverse(path.to_string_lossy().len())); - pyprojects.dedup(); - - // Read every `pyproject.toml`. - pyprojects - .into_iter() - .filter_map(|pyproject| match settings_for_path(&pyproject, overrides) { - Ok((project_root, settings)) => Some((project_root, settings)), - Err(error) => { - error!("Failed to read settings: {error}"); - None - } - }) - .collect::>() -}