diff --git a/resources/test/project/README.md b/resources/test/project/README.md index 1dccd78269..a521e34cb6 100644 --- a/resources/test/project/README.md +++ b/resources/test/project/README.md @@ -9,14 +9,13 @@ Running from the repo root should pick up and enforce the appropriate settings f ``` ∴ cargo run resources/test/project/ -Found 8 error(s). +Found 7 error(s). resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used resources/test/project/src/file.py:1:8: F401 `os` imported but unused -resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted 6 potentially fixable with the --fix option. ``` @@ -25,14 +24,13 @@ Running from the project directory itself should exhibit the same behavior: ``` ∴ (cd resources/test/project/ && cargo run .) -Found 8 error(s). +Found 7 error(s). examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used src/file.py:1:8: F401 `os` imported but unused -src/file.py:5:5: F841 Local variable `x` is assigned to but never used src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted 6 potentially fixable with the --fix option. ``` @@ -53,22 +51,19 @@ file paths from the current working directory: ``` ∴ (cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/) -Found 14 error(s). +Found 11 error(s). resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused -resources/test/project/examples/docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used +resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused -resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused -resources/test/project/examples/excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used resources/test/project/src/file.py:1:8: F401 `os` imported but unused -resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted -10 potentially fixable with the --fix option. +11 potentially fixable with the --fix option. ``` Running from a parent directory should this "ignore" the `exclude` (hence, `concepts/file.py` gets @@ -88,8 +83,7 @@ Passing an excluded directory directly should report errors in the contained fil ``` ∴ cargo run resources/test/project/examples/excluded/ -Found 2 error(s). +Found 1 error(s). resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused -resources/test/project/examples/excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used 1 potentially fixable with the --fix option. ``` diff --git a/resources/test/project/examples/docs/pyproject.toml b/resources/test/project/examples/docs/pyproject.toml index 6e20a48689..92f0b71d91 100644 --- a/resources/test/project/examples/docs/pyproject.toml +++ b/resources/test/project/examples/docs/pyproject.toml @@ -1,6 +1,7 @@ [tool.ruff] extend = "../../pyproject.toml" src = ["."] -extend-select = ["I001"] +# Enable I001, and re-enable F841, to test extension priority. +extend-select = ["I001", "F841"] extend-ignore = ["F401"] extend-exclude = ["./docs/concepts/file.py"] diff --git a/resources/test/project/pyproject.toml b/resources/test/project/pyproject.toml index e8bb938ffe..a4e9501344 100644 --- a/resources/test/project/pyproject.toml +++ b/resources/test/project/pyproject.toml @@ -2,3 +2,4 @@ src = [".", "python_modules/*"] exclude = ["examples/excluded"] extend-select = ["I001"] +extend-ignore = ["F841"] diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index bc05b7b1dc..ad27c160cf 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -24,9 +24,9 @@ pub struct Configuration { pub dummy_variable_rgx: Option, pub exclude: Option>, pub extend: Option, - pub extend_exclude: Option>, - pub extend_ignore: Option>, - pub extend_select: Option>, + pub extend_exclude: Vec, + pub extend_ignore: Vec>, + pub extend_select: Vec>, pub external: Option>, pub fix: Option, pub fixable: Option>, @@ -60,18 +60,12 @@ impl Configuration { pub fn from_options(options: Options, project_root: &Path) -> Result { Ok(Configuration { - extend: options.extend.map(PathBuf::from), allowed_confusables: options.allowed_confusables, dummy_variable_rgx: options .dummy_variable_rgx .map(|pattern| Regex::new(&pattern)) .transpose() .map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?, - src: options - .src - .map(|src| resolve_src(&src, project_root)) - .transpose()?, - target_version: options.target_version, exclude: options.exclude.map(|paths| { paths .into_iter() @@ -81,22 +75,24 @@ impl Configuration { }) .collect() }), - extend_exclude: options.extend_exclude.map(|paths| { - paths - .into_iter() - .map(|pattern| { - let absolute = fs::normalize_path_to(Path::new(&pattern), project_root); - FilePattern::User(pattern, absolute) - }) - .collect() - }), - extend_ignore: options.extend_ignore, - select: options.select, - extend_select: options.extend_select, + extend: options.extend.map(PathBuf::from), + extend_exclude: options + .extend_exclude + .map(|paths| { + paths + .into_iter() + .map(|pattern| { + let absolute = fs::normalize_path_to(Path::new(&pattern), project_root); + FilePattern::User(pattern, absolute) + }) + .collect() + }) + .unwrap_or_default(), + extend_ignore: vec![options.extend_ignore.unwrap_or_default()], + extend_select: vec![options.extend_select.unwrap_or_default()], external: options.external, fix: options.fix, fixable: options.fixable, - unfixable: options.unfixable, format: options.format, ignore: options.ignore, ignore_init_module_imports: options.ignore_init_module_imports, @@ -111,7 +107,14 @@ impl Configuration { .collect() }), respect_gitignore: options.respect_gitignore, + select: options.select, show_source: options.show_source, + src: options + .src + .map(|src| resolve_src(&src, project_root)) + .transpose()?, + target_version: options.target_version, + unfixable: options.unfixable, // Plugins flake8_annotations: options.flake8_annotations, flake8_bugbear: options.flake8_bugbear, @@ -133,9 +136,21 @@ impl Configuration { exclude: self.exclude.or(config.exclude), respect_gitignore: self.respect_gitignore.or(config.respect_gitignore), extend: self.extend.or(config.extend), - extend_exclude: self.extend_exclude.or(config.extend_exclude), - extend_ignore: self.extend_ignore.or(config.extend_ignore), - extend_select: self.extend_select.or(config.extend_select), + extend_exclude: config + .extend_exclude + .into_iter() + .chain(self.extend_exclude.into_iter()) + .collect(), + extend_ignore: config + .extend_ignore + .into_iter() + .chain(self.extend_ignore.into_iter()) + .collect(), + extend_select: config + .extend_select + .into_iter() + .chain(self.extend_select.into_iter()) + .collect(), external: self.external.or(config.external), fix: self.fix.or(config.fix), fixable: self.fixable.or(config.fixable), @@ -174,13 +189,7 @@ impl Configuration { self.exclude = Some(exclude); } if let Some(extend_exclude) = overrides.extend_exclude { - self.extend_exclude = Some(extend_exclude); - } - if let Some(extend_ignore) = overrides.extend_ignore { - self.extend_ignore = Some(extend_ignore); - } - if let Some(extend_select) = overrides.extend_select { - self.extend_select = Some(extend_select); + self.extend_exclude.extend(extend_exclude); } if let Some(fix) = overrides.fix { self.fix = Some(fix); @@ -220,6 +229,23 @@ impl Configuration { if let Some(unfixable) = overrides.unfixable { self.unfixable = Some(unfixable); } + // 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) => {} + } } } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 3cf70fe825..1aa5e742d3 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -97,26 +97,31 @@ impl Settings { .dummy_variable_rgx .unwrap_or_else(|| DEFAULT_DUMMY_VARIABLE_RGX.clone()), enabled: resolve_codes( - &config - .select - .unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]) - .into_iter() - .chain(config.extend_select.unwrap_or_default().into_iter()) - .collect::>(), - &config - .ignore - .unwrap_or_default() - .into_iter() - .chain(config.extend_ignore.unwrap_or_default().into_iter()) - .collect::>(), + [CheckCodeSpec { + select: &config + .select + .unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]), + ignore: &config.ignore.unwrap_or_default(), + }] + .into_iter() + .chain( + config + .extend_select + .iter() + .zip(config.extend_ignore.iter()) + .map(|(select, ignore)| CheckCodeSpec { select, ignore }), + ), ), exclude: resolve_globset(config.exclude.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()))?, - extend_exclude: resolve_globset(config.extend_exclude.unwrap_or_default())?, + extend_exclude: resolve_globset(config.extend_exclude)?, external: FxHashSet::from_iter(config.external.unwrap_or_default()), fix: config.fix.unwrap_or(false), fixable: resolve_codes( - &config.fixable.unwrap_or_else(|| CATEGORIES.to_vec()), - &config.unfixable.unwrap_or_default(), + [CheckCodeSpec { + select: &config.fixable.unwrap_or_else(|| CATEGORIES.to_vec()), + ignore: &config.unfixable.unwrap_or_default(), + }] + .into_iter(), ), format: config.format.unwrap_or(SerializationFormat::Text), ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(), @@ -301,26 +306,34 @@ pub fn resolve_per_file_ignores( .collect() } +#[derive(Debug)] +struct CheckCodeSpec<'a> { + select: &'a [CheckCodePrefix], + ignore: &'a [CheckCodePrefix], +} + /// Given a set of selected and ignored prefixes, resolve the set of enabled /// error codes. -fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet { +fn resolve_codes<'a>(specs: impl Iterator>) -> FxHashSet { let mut codes: FxHashSet = FxHashSet::default(); - for specificity in [ - SuffixLength::Zero, - SuffixLength::One, - SuffixLength::Two, - SuffixLength::Three, - SuffixLength::Four, - ] { - for prefix in select { - if prefix.specificity() == specificity { - codes.extend(prefix.codes()); + for spec in specs { + for specificity in [ + SuffixLength::Zero, + SuffixLength::One, + SuffixLength::Two, + SuffixLength::Three, + SuffixLength::Four, + ] { + for prefix in spec.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 spec.ignore { + if prefix.specificity() == specificity { + for code in prefix.codes() { + codes.remove(&code); + } } } } @@ -334,24 +347,80 @@ mod tests { use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix; - use crate::settings::resolve_codes; + use crate::settings::{resolve_codes, CheckCodeSpec}; #[test] - fn resolver() { - let actual = resolve_codes(&[CheckCodePrefix::W], &[]); + fn check_codes() { + let actual = resolve_codes( + [CheckCodeSpec { + select: &[CheckCodePrefix::W], + ignore: &[], + }] + .into_iter(), + ); let expected = FxHashSet::from_iter([CheckCode::W292, CheckCode::W605]); assert_eq!(actual, expected); - let actual = resolve_codes(&[CheckCodePrefix::W6], &[]); + let actual = resolve_codes( + [CheckCodeSpec { + select: &[CheckCodePrefix::W6], + ignore: &[], + }] + .into_iter(), + ); let expected = FxHashSet::from_iter([CheckCode::W605]); assert_eq!(actual, expected); - let actual = resolve_codes(&[CheckCodePrefix::W], &[CheckCodePrefix::W292]); + let actual = resolve_codes( + [CheckCodeSpec { + select: &[CheckCodePrefix::W], + ignore: &[CheckCodePrefix::W292], + }] + .into_iter(), + ); let expected = FxHashSet::from_iter([CheckCode::W605]); assert_eq!(actual, expected); - let actual = resolve_codes(&[CheckCodePrefix::W605], &[CheckCodePrefix::W605]); + let actual = resolve_codes( + [CheckCodeSpec { + select: &[CheckCodePrefix::W605], + ignore: &[CheckCodePrefix::W605], + }] + .into_iter(), + ); let expected = FxHashSet::from_iter([]); assert_eq!(actual, expected); + + let actual = resolve_codes( + [ + CheckCodeSpec { + select: &[CheckCodePrefix::W], + ignore: &[CheckCodePrefix::W292], + }, + CheckCodeSpec { + select: &[CheckCodePrefix::W292], + ignore: &[], + }, + ] + .into_iter(), + ); + let expected = FxHashSet::from_iter([CheckCode::W292, CheckCode::W605]); + assert_eq!(actual, expected); + + let actual = resolve_codes( + [ + CheckCodeSpec { + select: &[CheckCodePrefix::W], + ignore: &[CheckCodePrefix::W292], + }, + CheckCodeSpec { + select: &[CheckCodePrefix::W292], + ignore: &[CheckCodePrefix::W], + }, + ] + .into_iter(), + ); + let expected = FxHashSet::from_iter([CheckCode::W292]); + assert_eq!(actual, expected); } }