diff --git a/src/cli.rs b/src/cli.rs index 5f5b89fe58..f1a788a6d6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,12 +4,13 @@ use clap::{command, Parser}; use regex::Regex; use rustc_hash::FxHashMap; -use crate::fs; use crate::logging::LogLevel; use crate::registry::{RuleCode, RuleCodePrefix}; +use crate::resolver::ConfigProcessor; use crate::settings::types::{ FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat, }; +use crate::{fs, mccabe}; #[derive(Debug, Parser)] #[command(author, about = "Ruff: An extremely fast Python linter.")] @@ -344,6 +345,87 @@ pub struct Overrides { pub update_check: Option, } +impl ConfigProcessor for &Overrides { + fn process_config(&self, config: &mut crate::settings::configuration::Configuration) { + if let Some(cache_dir) = &self.cache_dir { + config.cache_dir = Some(cache_dir.clone()); + } + if let Some(dummy_variable_rgx) = &self.dummy_variable_rgx { + config.dummy_variable_rgx = Some(dummy_variable_rgx.clone()); + } + if let Some(exclude) = &self.exclude { + config.exclude = Some(exclude.clone()); + } + if let Some(extend_exclude) = &self.extend_exclude { + config.extend_exclude.extend(extend_exclude.clone()); + } + if let Some(fix) = &self.fix { + config.fix = Some(*fix); + } + if let Some(fix_only) = &self.fix_only { + config.fix_only = Some(*fix_only); + } + if let Some(fixable) = &self.fixable { + config.fixable = Some(fixable.clone()); + } + if let Some(format) = &self.format { + config.format = Some(*format); + } + if let Some(force_exclude) = &self.force_exclude { + config.force_exclude = Some(*force_exclude); + } + if let Some(ignore) = &self.ignore { + config.ignore = Some(ignore.clone()); + } + if let Some(line_length) = &self.line_length { + config.line_length = Some(*line_length); + } + if let Some(max_complexity) = &self.max_complexity { + config.mccabe = Some(mccabe::settings::Options { + max_complexity: Some(*max_complexity), + }); + } + if let Some(per_file_ignores) = &self.per_file_ignores { + config.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone())); + } + if let Some(respect_gitignore) = &self.respect_gitignore { + config.respect_gitignore = Some(*respect_gitignore); + } + if let Some(select) = &self.select { + config.select = Some(select.clone()); + } + if let Some(show_source) = &self.show_source { + config.show_source = Some(*show_source); + } + if let Some(target_version) = &self.target_version { + config.target_version = Some(*target_version); + } + if let Some(unfixable) = &self.unfixable { + config.unfixable = Some(unfixable.clone()); + } + if let Some(update_check) = &self.update_check { + config.update_check = Some(*update_check); + } + // Special-case: `extend_ignore` and `extend_select` are parallel arrays, so + // push an empty array if only one of the two is provided. + match (&self.extend_ignore, &self.extend_select) { + (Some(extend_ignore), Some(extend_select)) => { + config.extend_ignore.push(extend_ignore.clone()); + config.extend_select.push(extend_select.clone()); + } + (Some(extend_ignore), None) => { + config.extend_ignore.push(extend_ignore.clone()); + config.extend_select.push(Vec::new()); + } + (None, Some(extend_select)) => { + config.extend_ignore.push(Vec::new()); + config.extend_select.push(extend_select.clone()); + } + (None, None) => {} + } + } +} + /// Map the CLI settings to a `LogLevel`. pub fn extract_log_level(cli: &Arguments) -> LogLevel { if cli.silent { diff --git a/src/lib_native.rs b/src/lib_native.rs index 2863464e55..5b0fd6cb36 100644 --- a/src/lib_native.rs +++ b/src/lib_native.rs @@ -17,10 +17,10 @@ use crate::{directives, packaging, resolver}; fn resolve(path: &Path) -> Result { if let Some(pyproject) = pyproject::find_settings_toml(path)? { // First priority: `pyproject.toml` in the current `Path`. - resolver::resolve_settings(&pyproject, &Relativity::Parent, None) + resolver::resolve_settings(&pyproject, &Relativity::Parent) } else if let Some(pyproject) = pyproject::find_user_settings_toml() { // Second priority: user-specific `pyproject.toml`. - resolver::resolve_settings(&pyproject, &Relativity::Cwd, None) + resolver::resolve_settings(&pyproject, &Relativity::Cwd) } else { // Fallback: default settings. Settings::from_configuration(Configuration::default(), &path_dedot::CWD) diff --git a/src/main_native.rs b/src/main_native.rs index 60aad35bbb..82e9cee96b 100644 --- a/src/main_native.rs +++ b/src/main_native.rs @@ -6,7 +6,9 @@ use std::sync::mpsc::channel; use ::ruff::cli::{extract_log_level, Cli, Overrides}; use ::ruff::logging::{set_up_logging, LogLevel}; use ::ruff::printer::{Printer, Violations}; -use ::ruff::resolver::{resolve_settings, FileDiscovery, PyprojectDiscovery, Relativity}; +use ::ruff::resolver::{ + resolve_settings_with_processor, ConfigProcessor, FileDiscovery, PyprojectDiscovery, Relativity, +}; use ::ruff::settings::configuration::Configuration; use ::ruff::settings::types::SerializationFormat; use ::ruff::settings::{pyproject, Settings}; @@ -30,14 +32,14 @@ fn resolve( if isolated { // First priority: if we're running in isolated mode, use the default settings. let mut config = Configuration::default(); - config.apply(overrides.clone()); + overrides.process_config(&mut config); let settings = Settings::from_configuration(config, &path_dedot::CWD)?; Ok(PyprojectDiscovery::Fixed(settings)) } else if let Some(pyproject) = config { // Second priority: the user specified a `pyproject.toml` file. Use that // `pyproject.toml` for _all_ configuration, and resolve paths relative to the // current working directory. (This matches ESLint's behavior.) - let settings = resolve_settings(pyproject, &Relativity::Cwd, Some(overrides))?; + let settings = resolve_settings_with_processor(pyproject, &Relativity::Cwd, overrides)?; Ok(PyprojectDiscovery::Fixed(settings)) } else if let Some(pyproject) = pyproject::find_settings_toml( stdin_filename @@ -49,14 +51,14 @@ fn resolve( // that directory. (With `Strategy::Hierarchical`, we'll end up finding // the "closest" `pyproject.toml` file for every Python file later on, // so these act as the "default" settings.) - let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?; + let settings = resolve_settings_with_processor(&pyproject, &Relativity::Parent, overrides)?; Ok(PyprojectDiscovery::Hierarchical(settings)) } else if let Some(pyproject) = pyproject::find_user_settings_toml() { // Fourth priority: find a user-specific `pyproject.toml`, but resolve all paths // relative the current working directory. (With `Strategy::Hierarchical`, we'll // end up the "closest" `pyproject.toml` file for every Python file later on, so // these act as the "default" settings.) - let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?; + let settings = resolve_settings_with_processor(&pyproject, &Relativity::Cwd, overrides)?; Ok(PyprojectDiscovery::Hierarchical(settings)) } else { // Fallback: load Ruff's default settings, and resolve all paths relative to the @@ -64,7 +66,7 @@ fn resolve( // "closest" `pyproject.toml` file for every Python file later on, so these act // as the "default" settings.) let mut config = Configuration::default(); - config.apply(overrides.clone()); + overrides.process_config(&mut config); let settings = Settings::from_configuration(config, &path_dedot::CWD)?; Ok(PyprojectDiscovery::Hierarchical(settings)) } diff --git a/src/resolver.rs b/src/resolver.rs index b66443e805..686439ab67 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -11,7 +11,6 @@ use log::debug; use path_absolutize::path_dedot; use rustc_hash::FxHashSet; -use crate::cli::Overrides; use crate::fs; use crate::settings::configuration::Configuration; use crate::settings::pyproject::settings_toml; @@ -113,6 +112,15 @@ impl Resolver { } } +pub trait ConfigProcessor: Copy + Send + Sync { + fn process_config(&self, config: &mut Configuration); +} + +struct NoOpProcessor; +impl ConfigProcessor for &NoOpProcessor { + fn process_config(&self, _config: &mut Configuration) {} +} + /// Recursively resolve a `Configuration` from a `pyproject.toml` file at the /// specified `Path`. // TODO(charlie): This whole system could do with some caching. Right now, if a @@ -122,7 +130,7 @@ impl Resolver { pub fn resolve_configuration( pyproject: &Path, relativity: &Relativity, - overrides: Option<&Overrides>, + processor: impl ConfigProcessor, ) -> Result { let mut seen = FxHashSet::default(); let mut stack = vec![]; @@ -158,9 +166,7 @@ pub fn resolve_configuration( while let Some(extend) = stack.pop() { configuration = configuration.combine(extend); } - if let Some(overrides) = overrides { - configuration.apply(overrides.clone()); - } + processor.process_config(&mut configuration); Ok(configuration) } @@ -169,21 +175,28 @@ pub fn resolve_configuration( pub fn resolve_scoped_settings( pyproject: &Path, relativity: &Relativity, - overrides: Option<&Overrides>, + processor: impl ConfigProcessor, ) -> Result<(PathBuf, Settings)> { let project_root = relativity.resolve(pyproject); - let configuration = resolve_configuration(pyproject, relativity, overrides)?; + let configuration = resolve_configuration(pyproject, relativity, processor)?; let settings = Settings::from_configuration(configuration, &project_root)?; Ok((project_root, settings)) } /// Extract the `Settings` from a given `pyproject.toml`. -pub fn resolve_settings( +pub fn resolve_settings(pyproject: &Path, relativity: &Relativity) -> Result { + let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, &NoOpProcessor)?; + Ok(settings) +} + +/// Extract the `Settings` from a given `pyproject.toml` and process the +/// configuration with the given [`ConfigProcessor`]. +pub fn resolve_settings_with_processor( pyproject: &Path, relativity: &Relativity, - overrides: Option<&Overrides>, + processor: impl ConfigProcessor, ) -> Result { - let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, overrides)?; + let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, processor)?; Ok(settings) } @@ -212,7 +225,7 @@ pub fn python_files_in_path( paths: &[PathBuf], pyproject_strategy: &PyprojectDiscovery, file_strategy: &FileDiscovery, - overrides: &Overrides, + processor: impl ConfigProcessor, ) -> Result<(Vec>, Resolver)> { // Normalize every path (e.g., convert from relative to absolute). let mut paths: Vec = paths.iter().map(fs::normalize_path).collect(); @@ -223,7 +236,7 @@ pub fn python_files_in_path( for ancestor in path.ancestors() { if let Some(pyproject) = settings_toml(ancestor)? { let (root, settings) = - resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?; + resolve_scoped_settings(&pyproject, &Relativity::Parent, processor)?; resolver.add(root, settings); } } @@ -268,7 +281,7 @@ pub fn python_files_in_path( Ok(Some(pyproject)) => match resolve_scoped_settings( &pyproject, &Relativity::Parent, - Some(overrides), + processor, ) { Ok((root, settings)) => { resolver.write().unwrap().add(root, settings); @@ -342,7 +355,7 @@ pub fn python_file_at_path( path: &Path, pyproject_strategy: &PyprojectDiscovery, file_strategy: &FileDiscovery, - overrides: &Overrides, + processor: impl ConfigProcessor, ) -> Result { if !file_strategy.force_exclude { return Ok(true); @@ -356,7 +369,7 @@ pub fn python_file_at_path( for ancestor in path.ancestors() { if let Some(pyproject) = settings_toml(ancestor)? { let (root, settings) = - resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?; + resolve_scoped_settings(&pyproject, &Relativity::Parent, processor)?; resolver.add(root, settings); } } diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 4fbd970eea..092cb47e30 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -12,7 +12,6 @@ use regex::Regex; use shellexpand; use shellexpand::LookupError; -use crate::cli::{collect_per_file_ignores, Overrides}; use crate::registry::RuleCodePrefix; use crate::settings::options::Options; use crate::settings::pyproject::load_options; @@ -245,85 +244,6 @@ impl Configuration { pyupgrade: self.pyupgrade.or(config.pyupgrade), } } - - pub fn apply(&mut self, overrides: Overrides) { - if let Some(cache_dir) = overrides.cache_dir { - self.cache_dir = Some(cache_dir); - } - if let Some(dummy_variable_rgx) = overrides.dummy_variable_rgx { - self.dummy_variable_rgx = Some(dummy_variable_rgx); - } - if let Some(exclude) = overrides.exclude { - self.exclude = Some(exclude); - } - if let Some(extend_exclude) = overrides.extend_exclude { - self.extend_exclude.extend(extend_exclude); - } - if let Some(fix) = overrides.fix { - self.fix = Some(fix); - } - if let Some(fix_only) = overrides.fix_only { - self.fix_only = Some(fix_only); - } - if let Some(fixable) = overrides.fixable { - self.fixable = Some(fixable); - } - if let Some(format) = overrides.format { - self.format = Some(format); - } - if let Some(force_exclude) = overrides.force_exclude { - self.force_exclude = Some(force_exclude); - } - if let Some(ignore) = overrides.ignore { - self.ignore = Some(ignore); - } - if let Some(line_length) = overrides.line_length { - self.line_length = Some(line_length); - } - if let Some(max_complexity) = overrides.max_complexity { - self.mccabe = Some(mccabe::settings::Options { - max_complexity: Some(max_complexity), - }); - } - if let Some(per_file_ignores) = overrides.per_file_ignores { - self.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores)); - } - if let Some(respect_gitignore) = overrides.respect_gitignore { - self.respect_gitignore = Some(respect_gitignore); - } - if let Some(select) = overrides.select { - self.select = Some(select); - } - if let Some(show_source) = overrides.show_source { - self.show_source = Some(show_source); - } - if let Some(target_version) = overrides.target_version { - self.target_version = Some(target_version); - } - if let Some(unfixable) = overrides.unfixable { - self.unfixable = Some(unfixable); - } - if let Some(update_check) = overrides.update_check { - self.update_check = Some(update_check); - } - // Special-case: `extend_ignore` and `extend_select` are parallel arrays, so - // push an empty array if only one of the two is provided. - match (overrides.extend_ignore, overrides.extend_select) { - (Some(extend_ignore), Some(extend_select)) => { - self.extend_ignore.push(extend_ignore); - self.extend_select.push(extend_select); - } - (Some(extend_ignore), None) => { - self.extend_ignore.push(extend_ignore); - self.extend_select.push(Vec::new()); - } - (None, Some(extend_select)) => { - self.extend_ignore.push(Vec::new()); - self.extend_select.push(extend_select); - } - (None, None) => {} - } - } } /// Given a list of source paths, which could include glob patterns, resolve the