diff --git a/src/check_ast.rs b/src/check_ast.rs index d3ace5bd27..9b278d271d 100644 --- a/src/check_ast.rs +++ b/src/check_ast.rs @@ -26,7 +26,8 @@ use crate::checks::{Check, CheckCode, CheckKind}; use crate::docstrings::definition::{Definition, DefinitionKind, Documentable}; use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS}; use crate::python::future::ALL_FEATURE_NAMES; -use crate::settings::{PythonVersion, Settings}; +use crate::settings::types::PythonVersion; +use crate::settings::Settings; use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope}; use crate::{ docstrings, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_print, pep8_naming, diff --git a/src/check_lines.rs b/src/check_lines.rs index 9032bb3c49..621036c2fe 100644 --- a/src/check_lines.rs +++ b/src/check_lines.rs @@ -229,7 +229,7 @@ pub fn check_lines( mod tests { use crate::autofix::fixer; use crate::checks::{Check, CheckCode}; - use crate::settings; + use crate::settings::Settings; use super::check_lines; @@ -243,9 +243,9 @@ mod tests { &mut checks, line, &noqa_line_for, - &settings::Settings { + &Settings { line_length, - ..settings::Settings::for_rule(CheckCode::E501) + ..Settings::for_rule(CheckCode::E501) }, &fixer::Mode::Generate, ); diff --git a/src/cli.rs b/src/cli.rs index a1365a7f29..21ca856bc2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,9 +7,9 @@ use regex::Regex; use crate::checks_gen::CheckCodePrefix; use crate::printer::SerializationFormat; -use crate::pyproject::StrCheckCodePair; -use crate::settings::PythonVersion; -use crate::RawSettings; +use crate::settings::configuration::Configuration; +use crate::settings::types::PythonVersion; +use crate::settings::types::StrCheckCodePair; #[derive(Debug, Parser)] #[command(author, about = "ruff: An extremely fast Python linter.")] @@ -109,7 +109,7 @@ pub fn warn_on( codes: &[CheckCodePrefix], cli_ignore: &[CheckCodePrefix], cli_extend_ignore: &[CheckCodePrefix], - pyproject_settings: &RawSettings, + pyproject_configuration: &Configuration, pyproject_path: &Option, ) { for code in codes { @@ -117,7 +117,7 @@ pub fn warn_on( if cli_ignore.contains(code) { warn!("{code:?} was passed to {flag}, but ignored via --ignore") } - } else if pyproject_settings.ignore.contains(code) { + } else if pyproject_configuration.ignore.contains(code) { if let Some(path) = pyproject_path { warn!( "{code:?} was passed to {flag}, but ignored by the `ignore` field in {}", @@ -131,7 +131,7 @@ pub fn warn_on( if cli_extend_ignore.contains(code) { warn!("{code:?} was passed to {flag}, but ignored via --extend-ignore") } - } else if pyproject_settings.extend_ignore.contains(code) { + } else if pyproject_configuration.extend_ignore.contains(code) { if let Some(path) = pyproject_path { warn!( "{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}", diff --git a/src/flake8_quotes/checks.rs b/src/flake8_quotes/checks.rs index 0003be2c7b..e865072e9a 100644 --- a/src/flake8_quotes/checks.rs +++ b/src/flake8_quotes/checks.rs @@ -148,15 +148,10 @@ mod tests { use crate::checks::{Check, CheckCode}; use crate::flake8_quotes::settings::Quote; use crate::linter::tokenize; - use crate::settings; - use crate::{flake8_quotes, linter}; + use crate::{flake8_quotes, linter, Settings}; use crate::{fs, noqa}; - fn check_path( - path: &Path, - settings: &settings::Settings, - autofix: &fixer::Mode, - ) -> Result> { + fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result> { let contents = fs::read_file(path)?; let tokens: Vec = tokenize(&contents); let noqa_line_for = noqa::extract_noqa_line_for(&tokens); @@ -174,14 +169,14 @@ mod tests { Path::new("./resources/test/fixtures/flake8_quotes") .join(path) .as_path(), - &settings::Settings { + &Settings { flake8_quotes: flake8_quotes::settings::Settings { inline_quotes: Quote::Single, multiline_quotes: Quote::Single, docstring_quotes: Quote::Single, avoid_escape: true, }, - ..settings::Settings::for_rules(vec![ + ..Settings::for_rules(vec![ CheckCode::Q000, CheckCode::Q001, CheckCode::Q002, @@ -206,14 +201,14 @@ mod tests { Path::new("./resources/test/fixtures/flake8_quotes") .join(path) .as_path(), - &settings::Settings { + &Settings { flake8_quotes: flake8_quotes::settings::Settings { inline_quotes: Quote::Double, multiline_quotes: Quote::Double, docstring_quotes: Quote::Double, avoid_escape: true, }, - ..settings::Settings::for_rules(vec![ + ..Settings::for_rules(vec![ CheckCode::Q000, CheckCode::Q001, CheckCode::Q002, @@ -243,14 +238,14 @@ mod tests { Path::new("./resources/test/fixtures/flake8_quotes") .join(path) .as_path(), - &settings::Settings { + &Settings { flake8_quotes: flake8_quotes::settings::Settings { inline_quotes: Quote::Single, multiline_quotes: Quote::Single, docstring_quotes: Quote::Double, avoid_escape: true, }, - ..settings::Settings::for_rules(vec![ + ..Settings::for_rules(vec![ CheckCode::Q000, CheckCode::Q001, CheckCode::Q002, @@ -280,14 +275,14 @@ mod tests { Path::new("./resources/test/fixtures/flake8_quotes") .join(path) .as_path(), - &settings::Settings { + &Settings { flake8_quotes: flake8_quotes::settings::Settings { inline_quotes: Quote::Single, multiline_quotes: Quote::Double, docstring_quotes: Quote::Single, avoid_escape: true, }, - ..settings::Settings::for_rules(vec![ + ..Settings::for_rules(vec![ CheckCode::Q000, CheckCode::Q001, CheckCode::Q002, diff --git a/src/flake8_quotes/settings.rs b/src/flake8_quotes/settings.rs index 98fb7f0117..559e78d59c 100644 --- a/src/flake8_quotes/settings.rs +++ b/src/flake8_quotes/settings.rs @@ -11,7 +11,7 @@ pub enum Quote { #[derive(Debug, PartialEq, Eq, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] -pub struct Config { +pub struct Options { pub inline_quotes: Option, pub multiline_quotes: Option, pub docstring_quotes: Option, @@ -27,7 +27,7 @@ pub struct Settings { } impl Settings { - pub fn from_config(config: Config) -> Self { + pub fn from_config(config: Options) -> Self { Self { inline_quotes: config.inline_quotes.unwrap_or(Quote::Single), multiline_quotes: config.multiline_quotes.unwrap_or(Quote::Double), diff --git a/src/fs.rs b/src/fs.rs index 77f792ad41..ef6c193bfe 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -12,7 +12,7 @@ use path_absolutize::Absolutize; use walkdir::{DirEntry, WalkDir}; use crate::checks::CheckCode; -use crate::settings::{FilePattern, PerFileIgnore}; +use crate::settings::types::{FilePattern, PerFileIgnore}; /// Extract the absolute path and basename (as strings) from a Path. fn extract_path_names(path: &Path) -> Result<(&str, &str)> { @@ -178,7 +178,7 @@ mod tests { use path_absolutize::Absolutize; use crate::fs::{extract_path_names, is_excluded, is_included}; - use crate::settings::FilePattern; + use crate::settings::types::FilePattern; #[test] fn inclusions() { diff --git a/src/lib.rs b/src/lib.rs index 0702cef802..f56447df65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,11 +5,13 @@ use std::path::Path; use anyhow::Result; use log::debug; use rustpython_parser::lexer::LexResult; +use settings::pyproject; use crate::autofix::fixer::Mode; use crate::linter::{check_path, tokenize}; use crate::message::Message; -use crate::settings::{RawSettings, Settings}; +use crate::settings::configuration::Configuration; +use settings::Settings; mod ast; mod autofix; @@ -38,7 +40,6 @@ pub mod printer; mod pycodestyle; mod pydocstyle; mod pyflakes; -pub mod pyproject; mod python; mod pyupgrade; pub mod settings; @@ -58,7 +59,7 @@ pub fn check(path: &Path, contents: &str, quiet: bool) -> Result> { None => debug!("Unable to find pyproject.toml; using default settings..."), }; - let settings = Settings::from_raw(RawSettings::from_pyproject( + let settings = Settings::from_configuration(Configuration::from_pyproject( &pyproject, &project_root, quiet, diff --git a/src/linter.rs b/src/linter.rs index 66f94fa1ff..59510966bf 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -236,16 +236,12 @@ mod tests { use crate::autofix::fixer; use crate::checks::{Check, CheckCode}; - use crate::linter; use crate::linter::tokenize; use crate::settings; use crate::{fs, noqa}; + use crate::{linter, Settings}; - fn check_path( - path: &Path, - settings: &settings::Settings, - autofix: &fixer::Mode, - ) -> Result> { + fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result> { let contents = fs::read_file(path)?; let tokens: Vec = tokenize(&contents); let noqa_line_for = noqa::extract_noqa_line_for(&tokens); diff --git a/src/main.rs b/src/main.rs index 9f1e1e6b48..019ca1f28a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,10 +25,11 @@ use ruff::linter::{lint_path, lint_stdin}; use ruff::logging::set_up_logging; use ruff::message::Message; use ruff::printer::{Printer, SerializationFormat}; -use ruff::pyproject::{self}; -use ruff::settings::CurrentSettings; -use ruff::settings::RawSettings; -use ruff::settings::{FilePattern, PerFileIgnore, Settings}; +use ruff::settings::configuration::Configuration; +use ruff::settings::pyproject; +use ruff::settings::types::{FilePattern, PerFileIgnore}; +use ruff::settings::user::UserConfiguration; +use ruff::settings::Settings; use ruff::tell_user; #[cfg(feature = "update-informer")] @@ -73,10 +74,14 @@ fn check_for_updates() { } } -fn show_settings(settings: RawSettings, project_root: Option, pyproject: Option) { +fn show_settings( + configuration: Configuration, + project_root: Option, + pyproject: Option, +) { println!( "{:#?}", - CurrentSettings::from_settings(settings, project_root, pyproject) + UserConfiguration::from_configuration(configuration, project_root, pyproject) ); } @@ -256,15 +261,15 @@ fn inner_main() -> Result { .map(|pair| PerFileIgnore::new(pair, &project_root)) .collect(); - let mut settings = RawSettings::from_pyproject(&pyproject, &project_root, cli.quiet)?; + let mut configuration = Configuration::from_pyproject(&pyproject, &project_root, cli.quiet)?; if !exclude.is_empty() { - settings.exclude = exclude; + configuration.exclude = exclude; } if !extend_exclude.is_empty() { - settings.extend_exclude = extend_exclude; + configuration.extend_exclude = extend_exclude; } if !per_file_ignores.is_empty() { - settings.per_file_ignores = per_file_ignores; + configuration.per_file_ignores = per_file_ignores; } if !cli.select.is_empty() { warn_on( @@ -272,10 +277,10 @@ fn inner_main() -> Result { &cli.select, &cli.ignore, &cli.extend_ignore, - &settings, + &configuration, &pyproject, ); - settings.select = cli.select; + configuration.select = cli.select; } if !cli.extend_select.is_empty() { warn_on( @@ -283,22 +288,22 @@ fn inner_main() -> Result { &cli.extend_select, &cli.ignore, &cli.extend_ignore, - &settings, + &configuration, &pyproject, ); - settings.extend_select = cli.extend_select; + configuration.extend_select = cli.extend_select; } if !cli.ignore.is_empty() { - settings.ignore = cli.ignore; + configuration.ignore = cli.ignore; } if !cli.extend_ignore.is_empty() { - settings.extend_ignore = cli.extend_ignore; + configuration.extend_ignore = cli.extend_ignore; } if let Some(target_version) = cli.target_version { - settings.target_version = target_version; + configuration.target_version = target_version; } if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx { - settings.dummy_variable_rgx = dummy_variable_rgx; + configuration.dummy_variable_rgx = dummy_variable_rgx; } if cli.show_settings && cli.show_files { @@ -306,11 +311,11 @@ fn inner_main() -> Result { return Ok(ExitCode::FAILURE); } if cli.show_settings { - show_settings(settings, project_root, pyproject); + show_settings(configuration, project_root, pyproject); return Ok(ExitCode::SUCCESS); } - let settings = Settings::from_raw(settings); + let settings = Settings::from_configuration(configuration); if cli.show_files { show_files(&cli.files, &settings); diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index 824ea7cba2..0000000000 --- a/src/settings.rs +++ /dev/null @@ -1,367 +0,0 @@ -use std::collections::BTreeSet; -use std::hash::{Hash, Hasher}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; - -use anyhow::{anyhow, Result}; -use glob::Pattern; -use once_cell::sync::Lazy; -use regex::Regex; -use serde::{Deserialize, Serialize}; - -use crate::checks::CheckCode; -use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity}; -use crate::pyproject::{load_config, StrCheckCodePair}; -use crate::{flake8_quotes, fs}; - -#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)] -pub enum PythonVersion { - Py33, - Py34, - Py35, - Py36, - Py37, - Py38, - Py39, - Py310, - Py311, -} - -impl FromStr for PythonVersion { - type Err = anyhow::Error; - - fn from_str(string: &str) -> Result { - match string { - "py33" => Ok(PythonVersion::Py33), - "py34" => Ok(PythonVersion::Py34), - "py35" => Ok(PythonVersion::Py35), - "py36" => Ok(PythonVersion::Py36), - "py37" => Ok(PythonVersion::Py37), - "py38" => Ok(PythonVersion::Py38), - "py39" => Ok(PythonVersion::Py39), - "py310" => Ok(PythonVersion::Py310), - "py311" => Ok(PythonVersion::Py311), - _ => Err(anyhow!("Unknown version: {}", string)), - } - } -} - -#[derive(Debug, Clone, Hash)] -pub enum FilePattern { - Simple(&'static str), - Complex(Pattern, Option), -} - -impl FilePattern { - pub fn from_user(pattern: &str, project_root: &Option) -> Self { - let path = Path::new(pattern); - let absolute_path = match project_root { - Some(project_root) => fs::normalize_path_to(path, project_root), - None => fs::normalize_path(path), - }; - - let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern."); - let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) { - Some(Pattern::new(pattern).expect("Invalid pattern.")) - } else { - None - }; - - FilePattern::Complex(absolute, basename) - } -} - -#[derive(Debug, Clone, Hash)] -pub struct PerFileIgnore { - pub pattern: FilePattern, - pub codes: BTreeSet, -} - -impl PerFileIgnore { - pub fn new(user_in: StrCheckCodePair, project_root: &Option) -> Self { - let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root); - let codes = BTreeSet::from_iter(user_in.code.codes()); - Self { pattern, codes } - } -} - -#[derive(Debug)] -pub struct RawSettings { - pub dummy_variable_rgx: Regex, - pub exclude: Vec, - pub extend_exclude: Vec, - pub extend_ignore: Vec, - pub extend_select: Vec, - pub ignore: Vec, - pub line_length: usize, - pub per_file_ignores: Vec, - pub select: Vec, - pub target_version: PythonVersion, - // Plugins - pub flake8_quotes: flake8_quotes::settings::Settings, -} - -static DEFAULT_EXCLUDE: Lazy> = Lazy::new(|| { - vec![ - FilePattern::Simple(".bzr"), - FilePattern::Simple(".direnv"), - FilePattern::Simple(".eggs"), - FilePattern::Simple(".git"), - FilePattern::Simple(".hg"), - FilePattern::Simple(".mypy_cache"), - FilePattern::Simple(".nox"), - FilePattern::Simple(".pants.d"), - FilePattern::Simple(".ruff_cache"), - FilePattern::Simple(".svn"), - FilePattern::Simple(".tox"), - FilePattern::Simple(".venv"), - FilePattern::Simple("__pypackages__"), - FilePattern::Simple("_build"), - FilePattern::Simple("buck-out"), - FilePattern::Simple("build"), - FilePattern::Simple("dist"), - FilePattern::Simple("node_modules"), - FilePattern::Simple("venv"), - ] -}); - -static DEFAULT_DUMMY_VARIABLE_RGX: Lazy = - Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap()); - -impl RawSettings { - pub fn from_pyproject( - pyproject: &Option, - project_root: &Option, - quiet: bool, - ) -> Result { - let config = load_config(pyproject, quiet)?; - Ok(RawSettings { - dummy_variable_rgx: match config.dummy_variable_rgx { - Some(pattern) => Regex::new(&pattern) - .map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?, - None => DEFAULT_DUMMY_VARIABLE_RGX.clone(), - }, - target_version: config.target_version.unwrap_or(PythonVersion::Py310), - exclude: config - .exclude - .map(|paths| { - paths - .iter() - .map(|path| FilePattern::from_user(path, project_root)) - .collect() - }) - .unwrap_or_else(|| DEFAULT_EXCLUDE.clone()), - extend_exclude: config - .extend_exclude - .iter() - .map(|path| FilePattern::from_user(path, project_root)) - .collect(), - extend_ignore: config.extend_ignore, - select: config - .select - .unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]), - extend_select: config.extend_select, - ignore: config.ignore, - line_length: config.line_length.unwrap_or(88), - per_file_ignores: config - .per_file_ignores - .into_iter() - .map(|pair| PerFileIgnore::new(pair, project_root)) - .collect(), - // Plugins - flake8_quotes: config - .flake8_quotes - .map(flake8_quotes::settings::Settings::from_config) - .unwrap_or_default(), - }) - } -} - -#[derive(Debug)] -pub struct Settings { - pub dummy_variable_rgx: Regex, - pub enabled: BTreeSet, - pub exclude: Vec, - pub extend_exclude: Vec, - pub line_length: usize, - pub per_file_ignores: Vec, - pub target_version: PythonVersion, - // Plugins - pub flake8_quotes: flake8_quotes::settings::Settings, -} - -/// Given a set of selected and ignored prefixes, resolve the set of enabled error codes. -fn resolve_codes( - select: &[CheckCodePrefix], - extend_select: &[CheckCodePrefix], - ignore: &[CheckCodePrefix], - extend_ignore: &[CheckCodePrefix], -) -> BTreeSet { - let mut codes: BTreeSet = BTreeSet::new(); - for specificity in [ - PrefixSpecificity::Category, - PrefixSpecificity::Hundreds, - PrefixSpecificity::Tens, - PrefixSpecificity::Explicit, - ] { - for prefix in select { - if prefix.specificity() == specificity { - codes.extend(prefix.codes()); - } - } - for prefix in extend_select { - if prefix.specificity() == specificity { - codes.extend(prefix.codes()); - } - } - for prefix in ignore { - if prefix.specificity() == specificity { - for code in prefix.codes() { - codes.remove(&code); - } - } - } - for prefix in extend_ignore { - if prefix.specificity() == specificity { - for code in prefix.codes() { - codes.remove(&code); - } - } - } - } - codes -} - -impl Settings { - pub fn from_raw(settings: RawSettings) -> Self { - Self { - dummy_variable_rgx: settings.dummy_variable_rgx, - enabled: resolve_codes( - &settings.select, - &settings.extend_select, - &settings.ignore, - &settings.extend_ignore, - ), - exclude: settings.exclude, - extend_exclude: settings.extend_exclude, - flake8_quotes: settings.flake8_quotes, - line_length: settings.line_length, - per_file_ignores: settings.per_file_ignores, - target_version: settings.target_version, - } - } - - pub fn for_rule(check_code: CheckCode) -> Self { - Self { - dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(), - enabled: BTreeSet::from([check_code]), - exclude: vec![], - extend_exclude: vec![], - line_length: 88, - per_file_ignores: vec![], - target_version: PythonVersion::Py310, - flake8_quotes: Default::default(), - } - } - - pub fn for_rules(check_codes: Vec) -> Self { - Self { - dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(), - enabled: BTreeSet::from_iter(check_codes), - exclude: vec![], - extend_exclude: vec![], - line_length: 88, - per_file_ignores: vec![], - target_version: PythonVersion::Py310, - flake8_quotes: Default::default(), - } - } -} - -impl Hash for Settings { - fn hash(&self, state: &mut H) { - self.line_length.hash(state); - self.dummy_variable_rgx.as_str().hash(state); - for value in self.enabled.iter() { - value.hash(state); - } - for value in self.per_file_ignores.iter() { - value.hash(state); - } - } -} - -/// Struct to render user-facing exclusion patterns. -#[derive(Debug)] -#[allow(dead_code)] -pub struct Exclusion { - basename: Option, - absolute: Option, -} - -impl Exclusion { - pub fn from_file_pattern(file_pattern: FilePattern) -> Self { - match file_pattern { - FilePattern::Simple(basename) => Exclusion { - basename: Some(basename.to_string()), - absolute: None, - }, - FilePattern::Complex(absolute, basename) => Exclusion { - basename: basename.map(|pattern| pattern.to_string()), - absolute: Some(absolute.to_string()), - }, - } - } -} - -/// Struct to render user-facing Settings. -#[derive(Debug)] -pub struct CurrentSettings { - pub dummy_variable_rgx: Regex, - pub exclude: Vec, - pub extend_exclude: Vec, - pub extend_ignore: Vec, - pub extend_select: Vec, - pub ignore: Vec, - pub line_length: usize, - pub per_file_ignores: Vec, - pub select: Vec, - pub target_version: PythonVersion, - // Plugins - pub flake8_quotes: flake8_quotes::settings::Settings, - // Non-settings exposed to the user - pub project_root: Option, - pub pyproject: Option, -} - -impl CurrentSettings { - pub fn from_settings( - settings: RawSettings, - project_root: Option, - pyproject: Option, - ) -> Self { - Self { - dummy_variable_rgx: settings.dummy_variable_rgx, - exclude: settings - .exclude - .into_iter() - .map(Exclusion::from_file_pattern) - .collect(), - extend_exclude: settings - .extend_exclude - .into_iter() - .map(Exclusion::from_file_pattern) - .collect(), - extend_ignore: settings.extend_ignore, - extend_select: settings.extend_select, - ignore: settings.ignore, - line_length: settings.line_length, - per_file_ignores: settings.per_file_ignores, - select: settings.select, - target_version: settings.target_version, - flake8_quotes: settings.flake8_quotes, - project_root, - pyproject, - } - } -} diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs new file mode 100644 index 0000000000..2376999cd5 --- /dev/null +++ b/src/settings/configuration.rs @@ -0,0 +1,102 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, Result}; +use once_cell::sync::Lazy; +use regex::Regex; + +use crate::checks_gen::CheckCodePrefix; +use crate::flake8_quotes; +use crate::settings::pyproject::load_options; +use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; + +#[derive(Debug)] +pub struct Configuration { + pub dummy_variable_rgx: Regex, + pub exclude: Vec, + pub extend_exclude: Vec, + pub extend_ignore: Vec, + pub extend_select: Vec, + pub ignore: Vec, + pub line_length: usize, + pub per_file_ignores: Vec, + pub select: Vec, + pub target_version: PythonVersion, + // Plugins + pub flake8_quotes: flake8_quotes::settings::Settings, +} + +static DEFAULT_EXCLUDE: Lazy> = Lazy::new(|| { + vec![ + FilePattern::Simple(".bzr"), + FilePattern::Simple(".direnv"), + FilePattern::Simple(".eggs"), + FilePattern::Simple(".git"), + FilePattern::Simple(".hg"), + FilePattern::Simple(".mypy_cache"), + FilePattern::Simple(".nox"), + FilePattern::Simple(".pants.d"), + FilePattern::Simple(".ruff_cache"), + FilePattern::Simple(".svn"), + FilePattern::Simple(".tox"), + FilePattern::Simple(".venv"), + FilePattern::Simple("__pypackages__"), + FilePattern::Simple("_build"), + FilePattern::Simple("buck-out"), + FilePattern::Simple("build"), + FilePattern::Simple("dist"), + FilePattern::Simple("node_modules"), + FilePattern::Simple("venv"), + ] +}); + +static DEFAULT_DUMMY_VARIABLE_RGX: Lazy = + Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap()); + +impl Configuration { + pub fn from_pyproject( + pyproject: &Option, + project_root: &Option, + quiet: bool, + ) -> Result { + let options = load_options(pyproject, quiet)?; + Ok(Configuration { + dummy_variable_rgx: match options.dummy_variable_rgx { + Some(pattern) => Regex::new(&pattern) + .map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?, + None => DEFAULT_DUMMY_VARIABLE_RGX.clone(), + }, + target_version: options.target_version.unwrap_or(PythonVersion::Py310), + exclude: options + .exclude + .map(|paths| { + paths + .iter() + .map(|path| FilePattern::from_user(path, project_root)) + .collect() + }) + .unwrap_or_else(|| DEFAULT_EXCLUDE.clone()), + extend_exclude: options + .extend_exclude + .iter() + .map(|path| FilePattern::from_user(path, project_root)) + .collect(), + extend_ignore: options.extend_ignore, + select: options + .select + .unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]), + extend_select: options.extend_select, + ignore: options.ignore, + line_length: options.line_length.unwrap_or(88), + per_file_ignores: options + .per_file_ignores + .into_iter() + .map(|pair| PerFileIgnore::new(pair, project_root)) + .collect(), + // Plugins + flake8_quotes: options + .flake8_quotes + .map(flake8_quotes::settings::Settings::from_config) + .unwrap_or_default(), + }) + } +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 0000000000..fc8dfbcaf0 --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,133 @@ +//! Effective program settings, taking into account pyproject.toml and command-line options. +//! Structure is optimized for internal usage, as opposed to external visibility or parsing. + +use std::collections::BTreeSet; +use std::hash::{Hash, Hasher}; + +use regex::Regex; + +use crate::checks::CheckCode; +use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity}; +use crate::flake8_quotes; +use crate::settings::configuration::Configuration; +use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; + +pub mod configuration; +pub mod options; +pub mod pyproject; +pub mod types; +pub mod user; + +#[derive(Debug)] +pub struct Settings { + pub dummy_variable_rgx: Regex, + pub enabled: BTreeSet, + pub exclude: Vec, + pub extend_exclude: Vec, + pub line_length: usize, + pub per_file_ignores: Vec, + pub target_version: PythonVersion, + // Plugins + pub flake8_quotes: flake8_quotes::settings::Settings, +} + +impl Settings { + pub fn from_configuration(config: Configuration) -> Self { + Self { + dummy_variable_rgx: config.dummy_variable_rgx, + enabled: resolve_codes( + &config.select, + &config.extend_select, + &config.ignore, + &config.extend_ignore, + ), + exclude: config.exclude, + extend_exclude: config.extend_exclude, + flake8_quotes: config.flake8_quotes, + line_length: config.line_length, + per_file_ignores: config.per_file_ignores, + target_version: config.target_version, + } + } + + pub fn for_rule(check_code: CheckCode) -> Self { + Self { + dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(), + enabled: BTreeSet::from([check_code]), + exclude: vec![], + extend_exclude: vec![], + line_length: 88, + per_file_ignores: vec![], + target_version: PythonVersion::Py310, + flake8_quotes: Default::default(), + } + } + + pub fn for_rules(check_codes: Vec) -> Self { + Self { + dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(), + enabled: BTreeSet::from_iter(check_codes), + exclude: vec![], + extend_exclude: vec![], + line_length: 88, + per_file_ignores: vec![], + target_version: PythonVersion::Py310, + flake8_quotes: Default::default(), + } + } +} + +impl Hash for Settings { + fn hash(&self, state: &mut H) { + self.line_length.hash(state); + self.dummy_variable_rgx.as_str().hash(state); + for value in self.enabled.iter() { + value.hash(state); + } + for value in self.per_file_ignores.iter() { + value.hash(state); + } + } +} + +/// Given a set of selected and ignored prefixes, resolve the set of enabled error codes. +fn resolve_codes( + select: &[CheckCodePrefix], + extend_select: &[CheckCodePrefix], + ignore: &[CheckCodePrefix], + extend_ignore: &[CheckCodePrefix], +) -> BTreeSet { + let mut codes: BTreeSet = BTreeSet::new(); + for specificity in [ + PrefixSpecificity::Category, + PrefixSpecificity::Hundreds, + PrefixSpecificity::Tens, + PrefixSpecificity::Explicit, + ] { + for prefix in select { + if prefix.specificity() == specificity { + codes.extend(prefix.codes()); + } + } + for prefix in extend_select { + if prefix.specificity() == specificity { + codes.extend(prefix.codes()); + } + } + for prefix in ignore { + if prefix.specificity() == specificity { + for code in prefix.codes() { + codes.remove(&code); + } + } + } + for prefix in extend_ignore { + if prefix.specificity() == specificity { + for code in prefix.codes() { + codes.remove(&code); + } + } + } + } + codes +} diff --git a/src/settings/options.rs b/src/settings/options.rs new file mode 100644 index 0000000000..0a5834c6c0 --- /dev/null +++ b/src/settings/options.rs @@ -0,0 +1,28 @@ +//! Options that the user can provide via pyproject.toml. + +use serde::Deserialize; + +use crate::checks_gen::CheckCodePrefix; +use crate::flake8_quotes; +use crate::settings::types::{PythonVersion, StrCheckCodePair}; + +#[derive(Debug, PartialEq, Eq, Deserialize, Default)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct Options { + pub line_length: Option, + pub exclude: Option>, + #[serde(default)] + pub extend_exclude: Vec, + pub select: Option>, + #[serde(default)] + pub extend_select: Vec, + #[serde(default)] + pub ignore: Vec, + #[serde(default)] + pub extend_ignore: Vec, + #[serde(default)] + pub per_file_ignores: Vec, + pub dummy_variable_rgx: Option, + pub target_version: Option, + pub flake8_quotes: Option, +} diff --git a/src/pyproject.rs b/src/settings/pyproject.rs similarity index 72% rename from src/pyproject.rs rename to src/settings/pyproject.rs index bc5214a2b1..68e33616f8 100644 --- a/src/pyproject.rs +++ b/src/settings/pyproject.rs @@ -1,106 +1,26 @@ -use std::path::{Path, PathBuf}; -use std::str::FromStr; +//! Utilities for locating (and extracting configuration from) a pyproject.toml. -use anyhow::{anyhow, Result}; +use std::path::{Path, PathBuf}; + +use anyhow::Result; use common_path::common_path_all; use path_absolutize::Absolutize; -use serde::de; -use serde::{Deserialize, Deserializer}; +use serde::Deserialize; -use crate::checks_gen::CheckCodePrefix; -use crate::settings::PythonVersion; -use crate::{flake8_quotes, fs}; - -pub fn load_config(pyproject: &Option, quiet: bool) -> Result { - match pyproject { - Some(pyproject) => Ok(parse_pyproject_toml(pyproject)? - .tool - .and_then(|tool| tool.ruff) - .unwrap_or_default()), - None => { - if !quiet { - eprintln!("No pyproject.toml found."); - eprintln!("Falling back to default configuration..."); - } - Ok(Default::default()) - } - } -} - -#[derive(Debug, PartialEq, Eq, Deserialize, Default)] -#[serde(deny_unknown_fields, rename_all = "kebab-case")] -pub struct Config { - pub line_length: Option, - pub exclude: Option>, - #[serde(default)] - pub extend_exclude: Vec, - pub select: Option>, - #[serde(default)] - pub extend_select: Vec, - #[serde(default)] - pub ignore: Vec, - #[serde(default)] - pub extend_ignore: Vec, - #[serde(default)] - pub per_file_ignores: Vec, - pub dummy_variable_rgx: Option, - pub target_version: Option, - pub flake8_quotes: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct StrCheckCodePair { - pub pattern: String, - pub code: CheckCodePrefix, -} - -impl StrCheckCodePair { - const EXPECTED_PATTERN: &'static str = ": pattern"; -} - -impl<'de> Deserialize<'de> for StrCheckCodePair { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let str_result = String::deserialize(deserializer)?; - Self::from_str(str_result.as_str()).map_err(|_| { - de::Error::invalid_value( - de::Unexpected::Str(str_result.as_str()), - &Self::EXPECTED_PATTERN, - ) - }) - } -} - -impl FromStr for StrCheckCodePair { - type Err = anyhow::Error; - - fn from_str(string: &str) -> Result { - let (pattern_str, code_string) = { - let tokens = string.split(':').collect::>(); - if tokens.len() != 2 { - return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN)); - } - (tokens[0].trim(), tokens[1].trim()) - }; - let code = CheckCodePrefix::from_str(code_string)?; - let pattern = pattern_str.into(); - Ok(Self { pattern, code }) - } -} +use crate::fs; +use crate::settings::options::Options; #[derive(Debug, PartialEq, Eq, Deserialize)] struct Tools { - ruff: Option, + ruff: Option, } #[derive(Debug, PartialEq, Eq, Deserialize)] -struct PyProject { +struct Pyproject { tool: Option, } -fn parse_pyproject_toml(path: &Path) -> Result { +fn parse_pyproject_toml(path: &Path) -> Result { let contents = fs::read_file(path)?; toml::from_str(&contents).map_err(|e| e.into()) } @@ -149,6 +69,22 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option { None } +pub fn load_options(pyproject: &Option, quiet: bool) -> Result { + match pyproject { + Some(pyproject) => Ok(parse_pyproject_toml(pyproject)? + .tool + .and_then(|tool| tool.ruff) + .unwrap_or_default()), + None => { + if !quiet { + eprintln!("No pyproject.toml found."); + eprintln!("Falling back to default configuration..."); + } + Ok(Default::default()) + } + } +} + #[cfg(test)] mod tests { use std::env::current_dir; @@ -160,25 +96,24 @@ mod tests { use crate::checks_gen::CheckCodePrefix; use crate::flake8_quotes; use crate::flake8_quotes::settings::Quote; - use crate::pyproject::{ - find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools, + use crate::settings::pyproject::{ + find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; - - use super::StrCheckCodePair; + use crate::settings::types::StrCheckCodePair; #[test] fn deserialize() -> Result<()> { - let pyproject: PyProject = toml::from_str(r#""#)?; + let pyproject: Pyproject = toml::from_str(r#""#)?; assert_eq!(pyproject.tool, None); - let pyproject: PyProject = toml::from_str( + let pyproject: Pyproject = toml::from_str( r#" [tool.black] "#, )?; assert_eq!(pyproject.tool, Some(Tools { ruff: None })); - let pyproject: PyProject = toml::from_str( + let pyproject: Pyproject = toml::from_str( r#" [tool.black] [tool.ruff] @@ -187,7 +122,7 @@ mod tests { assert_eq!( pyproject.tool, Some(Tools { - ruff: Some(Config { + ruff: Some(Options { line_length: None, exclude: None, extend_exclude: vec![], @@ -203,7 +138,7 @@ mod tests { }) ); - let pyproject: PyProject = toml::from_str( + let pyproject: Pyproject = toml::from_str( r#" [tool.black] [tool.ruff] @@ -213,7 +148,7 @@ line-length = 79 assert_eq!( pyproject.tool, Some(Tools { - ruff: Some(Config { + ruff: Some(Options { line_length: Some(79), exclude: None, extend_exclude: vec![], @@ -229,7 +164,7 @@ line-length = 79 }) ); - let pyproject: PyProject = toml::from_str( + let pyproject: Pyproject = toml::from_str( r#" [tool.black] [tool.ruff] @@ -239,7 +174,7 @@ exclude = ["foo.py"] assert_eq!( pyproject.tool, Some(Tools { - ruff: Some(Config { + ruff: Some(Options { line_length: None, exclude: Some(vec!["foo.py".to_string()]), extend_exclude: vec![], @@ -255,7 +190,7 @@ exclude = ["foo.py"] }) ); - let pyproject: PyProject = toml::from_str( + let pyproject: Pyproject = toml::from_str( r#" [tool.black] [tool.ruff] @@ -265,7 +200,7 @@ select = ["E501"] assert_eq!( pyproject.tool, Some(Tools { - ruff: Some(Config { + ruff: Some(Options { line_length: None, exclude: None, extend_exclude: vec![], @@ -281,7 +216,7 @@ select = ["E501"] }) ); - let pyproject: PyProject = toml::from_str( + let pyproject: Pyproject = toml::from_str( r#" [tool.black] [tool.ruff] @@ -292,7 +227,7 @@ ignore = ["E501"] assert_eq!( pyproject.tool, Some(Tools { - ruff: Some(Config { + ruff: Some(Options { line_length: None, exclude: None, extend_exclude: vec![], @@ -308,7 +243,7 @@ ignore = ["E501"] }) ); - assert!(toml::from_str::( + assert!(toml::from_str::( r#" [tool.black] [tool.ruff] @@ -317,7 +252,7 @@ line_length = 79 ) .is_err()); - assert!(toml::from_str::( + assert!(toml::from_str::( r#" [tool.black] [tool.ruff] @@ -326,7 +261,7 @@ select = ["E123"] ) .is_err()); - assert!(toml::from_str::( + assert!(toml::from_str::( r#" [tool.black] [tool.ruff] @@ -358,7 +293,7 @@ other-attribute = 1 .expect("Unable to find tool.ruff."); assert_eq!( config, - Config { + Options { line_length: Some(88), exclude: None, extend_exclude: vec![ @@ -376,7 +311,7 @@ other-attribute = 1 }], dummy_variable_rgx: None, target_version: None, - flake8_quotes: Some(flake8_quotes::settings::Config { + flake8_quotes: Some(flake8_quotes::settings::Options { inline_quotes: Some(Quote::Single), multiline_quotes: Some(Quote::Double), docstring_quotes: Some(Quote::Double), diff --git a/src/settings/types.rs b/src/settings/types.rs new file mode 100644 index 0000000000..8144d678b6 --- /dev/null +++ b/src/settings/types.rs @@ -0,0 +1,125 @@ +use std::collections::BTreeSet; +use std::hash::Hash; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use anyhow::{anyhow, Result}; +use glob::Pattern; +use serde::{de, Deserialize, Deserializer, Serialize}; + +use crate::checks::CheckCode; +use crate::checks_gen::CheckCodePrefix; +use crate::fs; + +#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize)] +pub enum PythonVersion { + Py33, + Py34, + Py35, + Py36, + Py37, + Py38, + Py39, + Py310, + Py311, +} + +impl FromStr for PythonVersion { + type Err = anyhow::Error; + + fn from_str(string: &str) -> Result { + match string { + "py33" => Ok(PythonVersion::Py33), + "py34" => Ok(PythonVersion::Py34), + "py35" => Ok(PythonVersion::Py35), + "py36" => Ok(PythonVersion::Py36), + "py37" => Ok(PythonVersion::Py37), + "py38" => Ok(PythonVersion::Py38), + "py39" => Ok(PythonVersion::Py39), + "py310" => Ok(PythonVersion::Py310), + "py311" => Ok(PythonVersion::Py311), + _ => Err(anyhow!("Unknown version: {}", string)), + } + } +} + +#[derive(Debug, Clone, Hash)] +pub enum FilePattern { + Simple(&'static str), + Complex(Pattern, Option), +} + +impl FilePattern { + pub fn from_user(pattern: &str, project_root: &Option) -> Self { + let path = Path::new(pattern); + let absolute_path = match project_root { + Some(project_root) => fs::normalize_path_to(path, project_root), + None => fs::normalize_path(path), + }; + + let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern."); + let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) { + Some(Pattern::new(pattern).expect("Invalid pattern.")) + } else { + None + }; + + FilePattern::Complex(absolute, basename) + } +} + +#[derive(Debug, Clone, Hash)] +pub struct PerFileIgnore { + pub pattern: FilePattern, + pub codes: BTreeSet, +} + +impl PerFileIgnore { + pub fn new(user_in: StrCheckCodePair, project_root: &Option) -> Self { + let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root); + let codes = BTreeSet::from_iter(user_in.code.codes()); + Self { pattern, codes } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StrCheckCodePair { + pub pattern: String, + pub code: CheckCodePrefix, +} + +impl StrCheckCodePair { + const EXPECTED_PATTERN: &'static str = ": pattern"; +} + +impl<'de> Deserialize<'de> for StrCheckCodePair { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let str_result = String::deserialize(deserializer)?; + Self::from_str(str_result.as_str()).map_err(|_| { + de::Error::invalid_value( + de::Unexpected::Str(str_result.as_str()), + &Self::EXPECTED_PATTERN, + ) + }) + } +} + +impl FromStr for StrCheckCodePair { + type Err = anyhow::Error; + + fn from_str(string: &str) -> Result { + let (pattern_str, code_string) = { + let tokens = string.split(':').collect::>(); + if tokens.len() != 2 { + return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN)); + } + (tokens[0].trim(), tokens[1].trim()) + }; + let code = CheckCodePrefix::from_str(code_string)?; + let pattern = pattern_str.into(); + Ok(Self { pattern, code }) + } +} diff --git a/src/settings/user.rs b/src/settings/user.rs new file mode 100644 index 0000000000..5c11f45fa7 --- /dev/null +++ b/src/settings/user.rs @@ -0,0 +1,84 @@ +//! Structs to render user-facing settings. + +use std::path::PathBuf; + +use regex::Regex; + +use crate::checks_gen::CheckCodePrefix; +use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; +use crate::{flake8_quotes, Configuration}; + +/// Struct to render user-facing exclusion patterns. +#[derive(Debug)] +#[allow(dead_code)] +pub struct Exclusion { + basename: Option, + absolute: Option, +} + +impl Exclusion { + pub fn from_file_pattern(file_pattern: FilePattern) -> Self { + match file_pattern { + FilePattern::Simple(basename) => Exclusion { + basename: Some(basename.to_string()), + absolute: None, + }, + FilePattern::Complex(absolute, basename) => Exclusion { + basename: basename.map(|pattern| pattern.to_string()), + absolute: Some(absolute.to_string()), + }, + } + } +} + +/// Struct to render user-facing configuration. +#[derive(Debug)] +pub struct UserConfiguration { + pub dummy_variable_rgx: Regex, + pub exclude: Vec, + pub extend_exclude: Vec, + pub extend_ignore: Vec, + pub extend_select: Vec, + pub ignore: Vec, + pub line_length: usize, + pub per_file_ignores: Vec, + pub select: Vec, + pub target_version: PythonVersion, + // Plugins + pub flake8_quotes: flake8_quotes::settings::Settings, + // Non-settings exposed to the user + pub project_root: Option, + pub pyproject: Option, +} + +impl UserConfiguration { + pub fn from_configuration( + settings: Configuration, + project_root: Option, + pyproject: Option, + ) -> Self { + Self { + dummy_variable_rgx: settings.dummy_variable_rgx, + exclude: settings + .exclude + .into_iter() + .map(Exclusion::from_file_pattern) + .collect(), + extend_exclude: settings + .extend_exclude + .into_iter() + .map(Exclusion::from_file_pattern) + .collect(), + extend_ignore: settings.extend_ignore, + extend_select: settings.extend_select, + ignore: settings.ignore, + line_length: settings.line_length, + per_file_ignores: settings.per_file_ignores, + select: settings.select, + target_version: settings.target_version, + flake8_quotes: settings.flake8_quotes, + project_root, + pyproject, + } + } +}