Decouple resolver module from cli::Overrides

This commit is contained in:
Martin Fischer
2023-01-12 11:38:07 +01:00
committed by Charlie Marsh
parent bbc1e7804e
commit 74f14182ea
5 changed files with 121 additions and 104 deletions

View File

@@ -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<bool>,
}
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 {

View File

@@ -17,10 +17,10 @@ use crate::{directives, packaging, resolver};
fn resolve(path: &Path) -> Result<Settings> {
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)

View File

@@ -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))
}

View File

@@ -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<Configuration> {
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<Settings> {
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<Settings> {
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<Result<DirEntry, ignore::Error>>, Resolver)> {
// Normalize every path (e.g., convert from relative to absolute).
let mut paths: Vec<PathBuf> = 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<bool> {
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);
}
}

View File

@@ -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