diff --git a/crates/flake8_to_ruff/src/converter.rs b/crates/flake8_to_ruff/src/converter.rs index 650e055125..1eb74c1ebb 100644 --- a/crates/flake8_to_ruff/src/converter.rs +++ b/crates/flake8_to_ruff/src/converter.rs @@ -42,7 +42,10 @@ pub fn convert(config: HashMap>>) -> Resu } "per-file-ignores" | "per_file_ignores" => { match parser::parse_files_to_codes_mapping(value.as_ref()) { - Ok(per_file_ignores) => options.per_file_ignores = Some(per_file_ignores), + Ok(per_file_ignores) => { + options.per_file_ignores = + Some(parser::collect_per_file_ignores(per_file_ignores)) + } Err(e) => eprintln!("Unable to parse '{key}' property: {e}"), } } diff --git a/crates/flake8_to_ruff/src/parser.rs b/crates/flake8_to_ruff/src/parser.rs index 07103daf9e..c1c09dcef7 100644 --- a/crates/flake8_to_ruff/src/parser.rs +++ b/crates/flake8_to_ruff/src/parser.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::str::FromStr; use anyhow::Result; @@ -5,7 +6,7 @@ use once_cell::sync::Lazy; use regex::Regex; use ruff::checks_gen::CheckCodePrefix; -use ruff::settings::types::StrCheckCodePair; +use ruff::settings::types::PatternPrefixPair; static COMMA_SEPARATED_LIST_RE: Lazy = Lazy::new(|| Regex::new(r"[,\s]").unwrap()); @@ -70,15 +71,15 @@ impl State { } /// Generate the list of `StrCheckCodePair` pairs for the current state. - fn parse(&self) -> Vec { - let mut codes: Vec = vec![]; + fn parse(&self) -> Vec { + let mut codes: Vec = vec![]; for code in &self.codes { match CheckCodePrefix::from_str(code) { Ok(code) => { for filename in &self.filenames { - codes.push(StrCheckCodePair { + codes.push(PatternPrefixPair { pattern: filename.clone(), - code: code.clone(), + prefix: code.clone(), }); } } @@ -127,11 +128,11 @@ fn tokenize_files_to_codes_mapping(value: &str) -> Vec { /// Parse a 'files-to-codes' mapping, mimicking Flake8's internal logic. /// /// See: https://github.com/PyCQA/flake8/blob/7dfe99616fc2f07c0017df2ba5fa884158f3ea8a/src/flake8/utils.py#L45 -pub fn parse_files_to_codes_mapping(value: &str) -> Result> { +pub fn parse_files_to_codes_mapping(value: &str) -> Result> { if value.trim().is_empty() { return Ok(vec![]); } - let mut codes: Vec = vec![]; + let mut codes: Vec = vec![]; let mut state = State::new(); for token in tokenize_files_to_codes_mapping(value) { if matches!(token.token_name, TokenType::Comma | TokenType::Ws) { @@ -166,12 +167,26 @@ pub fn parse_files_to_codes_mapping(value: &str) -> Result Ok(codes) } +/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`. +pub fn collect_per_file_ignores( + pairs: Vec, +) -> BTreeMap> { + let mut per_file_ignores: BTreeMap> = BTreeMap::new(); + for pair in pairs { + per_file_ignores + .entry(pair.pattern) + .or_insert_with(Vec::new) + .push(pair.prefix); + } + per_file_ignores +} + #[cfg(test)] mod tests { use anyhow::Result; use ruff::checks_gen::CheckCodePrefix; - use ruff::settings::types::StrCheckCodePair; + use ruff::settings::types::PatternPrefixPair; use crate::parser::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings}; @@ -232,11 +247,11 @@ mod tests { #[test] fn it_parse_files_to_codes_mapping() -> Result<()> { let actual = parse_files_to_codes_mapping("")?; - let expected: Vec = vec![]; + let expected: Vec = vec![]; assert_eq!(actual, expected); let actual = parse_files_to_codes_mapping(" ")?; - let expected: Vec = vec![]; + let expected: Vec = vec![]; assert_eq!(actual, expected); // Ex) locust @@ -248,14 +263,14 @@ mod tests { .strip_prefix("per-file-ignores =") .unwrap(), )?; - let expected: Vec = vec![ - StrCheckCodePair { + let expected: Vec = vec![ + PatternPrefixPair { pattern: "locust/test/*".to_string(), - code: CheckCodePrefix::F841, + prefix: CheckCodePrefix::F841, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "examples/*".to_string(), - code: CheckCodePrefix::F841, + prefix: CheckCodePrefix::F841, }, ]; assert_eq!(actual, expected); @@ -268,26 +283,26 @@ mod tests { .strip_prefix("per-file-ignores =") .unwrap(), )?; - let expected: Vec = vec![ - StrCheckCodePair { + let expected: Vec = vec![ + PatternPrefixPair { pattern: "t/*".to_string(), - code: CheckCodePrefix::D, + prefix: CheckCodePrefix::D, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "setup.py".to_string(), - code: CheckCodePrefix::D, + prefix: CheckCodePrefix::D, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "examples/*".to_string(), - code: CheckCodePrefix::D, + prefix: CheckCodePrefix::D, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "docs/*".to_string(), - code: CheckCodePrefix::D, + prefix: CheckCodePrefix::D, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "extra/*".to_string(), - code: CheckCodePrefix::D, + prefix: CheckCodePrefix::D, }, ]; assert_eq!(actual, expected); @@ -306,50 +321,50 @@ mod tests { .strip_prefix("per-file-ignores =") .unwrap(), )?; - let expected: Vec = vec![ - StrCheckCodePair { + let expected: Vec = vec![ + PatternPrefixPair { pattern: "scrapy/__init__.py".to_string(), - code: CheckCodePrefix::E402, + prefix: CheckCodePrefix::E402, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/core/downloader/handlers/http.py".to_string(), - code: CheckCodePrefix::F401, + prefix: CheckCodePrefix::F401, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/http/__init__.py".to_string(), - code: CheckCodePrefix::F401, + prefix: CheckCodePrefix::F401, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/linkextractors/__init__.py".to_string(), - code: CheckCodePrefix::E402, + prefix: CheckCodePrefix::E402, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/linkextractors/__init__.py".to_string(), - code: CheckCodePrefix::F401, + prefix: CheckCodePrefix::F401, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/selector/__init__.py".to_string(), - code: CheckCodePrefix::F401, + prefix: CheckCodePrefix::F401, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/spiders/__init__.py".to_string(), - code: CheckCodePrefix::E402, + prefix: CheckCodePrefix::E402, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/spiders/__init__.py".to_string(), - code: CheckCodePrefix::F401, + prefix: CheckCodePrefix::F401, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/utils/url.py".to_string(), - code: CheckCodePrefix::F403, + prefix: CheckCodePrefix::F403, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "scrapy/utils/url.py".to_string(), - code: CheckCodePrefix::F405, + prefix: CheckCodePrefix::F405, }, - StrCheckCodePair { + PatternPrefixPair { pattern: "tests/test_loader.py".to_string(), - code: CheckCodePrefix::E741, + prefix: CheckCodePrefix::E741, }, ]; assert_eq!(actual, expected); diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index f5995de45e..2696deb573 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -1,13 +1,11 @@ [tool.ruff] line-length = 88 extend-exclude = [ - "excluded.py", - "migrations", - "directory/also_excluded.py", -] -per-file-ignores = [ - "__init__.py:F401", + "excluded.py", + "migrations", + "directory/also_excluded.py", ] +per-file-ignores = { "__init__.py" = ["F401"] } [tool.ruff.flake8-quotes] inline-quotes = "single" @@ -17,22 +15,22 @@ avoid-escape = true [tool.ruff.pep8-naming] ignore-names = [ - "setUp", - "tearDown", - "setUpClass", - "tearDownClass", - "setUpModule", - "tearDownModule", - "asyncSetUp", - "asyncTearDown", - "setUpTestData", - "failureException", - "longMessage", - "maxDiff", + "setUp", + "tearDown", + "setUpClass", + "tearDownClass", + "setUpModule", + "tearDownModule", + "asyncSetUp", + "asyncTearDown", + "setUpTestData", + "failureException", + "longMessage", + "maxDiff", ] classmethod-decorators = [ - "classmethod", + "classmethod", ] staticmethod-decorators = [ - "staticmethod", + "staticmethod", ] diff --git a/src/cli.rs b/src/cli.rs index 21ca856bc2..be65986fd3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::fmt; use std::path::PathBuf; @@ -8,8 +9,8 @@ use regex::Regex; use crate::checks_gen::CheckCodePrefix; use crate::printer::SerializationFormat; use crate::settings::configuration::Configuration; +use crate::settings::types::PatternPrefixPair; use crate::settings::types::PythonVersion; -use crate::settings::types::StrCheckCodePair; #[derive(Debug, Parser)] #[command(author, about = "ruff: An extremely fast Python linter.")] @@ -61,7 +62,7 @@ pub struct Cli { pub extend_exclude: Vec, /// List of mappings from file pattern to code to exclude #[arg(long, value_delimiter = ',')] - pub per_file_ignores: Vec, + pub per_file_ignores: Vec, /// Output serialization format for error messages. #[arg(long, value_enum, default_value_t=SerializationFormat::Text)] pub format: SerializationFormat, @@ -143,3 +144,17 @@ pub fn warn_on( } } } + +/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`. +pub fn collect_per_file_ignores( + pairs: Vec, +) -> BTreeMap> { + let mut per_file_ignores: BTreeMap> = BTreeMap::new(); + for pair in pairs { + per_file_ignores + .entry(pair.pattern) + .or_insert_with(Vec::new) + .push(pair.prefix); + } + per_file_ignores +} diff --git a/src/main.rs b/src/main.rs index 1edf0d5636..fd97c035d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -17,7 +18,8 @@ use walkdir::DirEntry; use ruff::cache; use ruff::checks::CheckCode; use ruff::checks::CheckKind; -use ruff::cli::{warn_on, Cli, Warnable}; +use ruff::checks_gen::CheckCodePrefix; +use ruff::cli::{collect_per_file_ignores, warn_on, Cli, Warnable}; use ruff::fs::iter_python_files; use ruff::linter::add_noqa_to_path; use ruff::linter::autoformat_path; @@ -27,7 +29,7 @@ use ruff::message::Message; use ruff::printer::{Printer, SerializationFormat}; use ruff::settings::configuration::Configuration; use ruff::settings::pyproject; -use ruff::settings::types::{FilePattern, PerFileIgnore}; +use ruff::settings::types::FilePattern; use ruff::settings::user::UserConfiguration; use ruff::settings::Settings; use ruff::tell_user; @@ -255,11 +257,8 @@ fn inner_main() -> Result { .iter() .map(|path| FilePattern::from_user(path, &project_root)) .collect(); - let per_file_ignores: Vec = cli - .per_file_ignores - .into_iter() - .map(|pair| PerFileIgnore::new(pair, &project_root)) - .collect(); + let per_file_ignores: BTreeMap> = + collect_per_file_ignores(cli.per_file_ignores); let mut configuration = Configuration::from_pyproject(&pyproject, &project_root)?; if !exclude.is_empty() { diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 3f280227cf..3c48391a01 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -1,6 +1,7 @@ //! User-provided program settings, taking into account pyproject.toml and command-line options. //! Structure mirrors the user-facing representation of the various parameters. +use std::collections::BTreeMap; use std::path::PathBuf; use anyhow::{anyhow, Result}; @@ -9,7 +10,7 @@ use regex::Regex; use crate::checks_gen::CheckCodePrefix; use crate::settings::pyproject::load_options; -use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; +use crate::settings::types::{FilePattern, PythonVersion}; use crate::{flake8_quotes, pep8_naming}; #[derive(Debug)] @@ -21,7 +22,7 @@ pub struct Configuration { pub extend_select: Vec, pub ignore: Vec, pub line_length: usize, - pub per_file_ignores: Vec, + pub per_file_ignores: BTreeMap>, pub select: Vec, pub target_version: PythonVersion, // Plugins @@ -91,12 +92,7 @@ impl Configuration { extend_select: options.extend_select.unwrap_or_default(), ignore: options.ignore.unwrap_or_default(), line_length: options.line_length.unwrap_or(88), - per_file_ignores: options - .per_file_ignores - .unwrap_or_default() - .into_iter() - .map(|pair| PerFileIgnore::new(pair, project_root)) - .collect(), + per_file_ignores: options.per_file_ignores.unwrap_or_default(), // Plugins flake8_quotes: options .flake8_quotes diff --git a/src/settings/mod.rs b/src/settings/mod.rs index b124fb3bd3..b14e0f8f82 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -1,7 +1,7 @@ //! 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::collections::{BTreeMap, BTreeSet}; use std::hash::{Hash, Hasher}; use regex::Regex; @@ -47,7 +47,7 @@ impl Settings { flake8_quotes: config.flake8_quotes, line_length: config.line_length, pep8_naming: config.pep8_naming, - per_file_ignores: config.per_file_ignores, + per_file_ignores: resolve_per_file_ignores(&config.per_file_ignores), target_version: config.target_version, } } @@ -56,10 +56,10 @@ impl Settings { 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![], + exclude: Default::default(), + extend_exclude: Default::default(), line_length: 88, - per_file_ignores: vec![], + per_file_ignores: Default::default(), target_version: PythonVersion::Py310, flake8_quotes: Default::default(), pep8_naming: Default::default(), @@ -70,10 +70,10 @@ impl Settings { 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![], + exclude: Default::default(), + extend_exclude: Default::default(), line_length: 88, - per_file_ignores: vec![], + per_file_ignores: Default::default(), target_version: PythonVersion::Py310, flake8_quotes: Default::default(), pep8_naming: Default::default(), @@ -136,6 +136,15 @@ fn resolve_codes( codes } +fn resolve_per_file_ignores( + per_file_ignores: &BTreeMap>, +) -> Vec { + per_file_ignores + .iter() + .map(|(pattern, prefixes)| PerFileIgnore::new(pattern, prefixes, &None)) + .collect() +} + #[cfg(test)] mod tests { use std::collections::BTreeSet; diff --git a/src/settings/options.rs b/src/settings/options.rs index 1ef93d8677..3980632445 100644 --- a/src/settings/options.rs +++ b/src/settings/options.rs @@ -1,9 +1,11 @@ //! Options that the user can provide via pyproject.toml. +use std::collections::BTreeMap; + use serde::{Deserialize, Serialize}; use crate::checks_gen::CheckCodePrefix; -use crate::settings::types::{PythonVersion, StrCheckCodePair}; +use crate::settings::types::PythonVersion; use crate::{flake8_quotes, pep8_naming}; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] @@ -16,7 +18,7 @@ pub struct Options { pub extend_select: Option>, pub ignore: Option>, pub extend_ignore: Option>, - pub per_file_ignores: Option>, + pub per_file_ignores: Option>>, pub dummy_variable_rgx: Option, pub target_version: Option, // Plugins diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index cb8d172e9e..b29b49729b 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -96,6 +96,7 @@ pub fn load_options(pyproject: &Option) -> Result { #[cfg(test)] mod tests { + use std::collections::BTreeMap; use std::env::current_dir; use std::path::PathBuf; use std::str::FromStr; @@ -107,7 +108,7 @@ mod tests { use crate::settings::pyproject::{ find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; - use crate::settings::types::StrCheckCodePair; + use crate::settings::types::PatternPrefixPair; use crate::{flake8_quotes, pep8_naming}; #[test] @@ -319,10 +320,10 @@ other-attribute = 1 extend_select: None, ignore: None, extend_ignore: None, - per_file_ignores: Some(vec![StrCheckCodePair { - pattern: "__init__.py".to_string(), - code: CheckCodePrefix::F401 - }]), + per_file_ignores: Some(BTreeMap::from([( + "__init__.py".to_string(), + vec![CheckCodePrefix::F401] + ),])), dummy_variable_rgx: None, target_version: None, flake8_quotes: Some(flake8_quotes::settings::Options { @@ -357,21 +358,21 @@ other-attribute = 1 #[test] fn str_check_code_pair_strings() { - let result = StrCheckCodePair::from_str("foo:E501"); + let result = PatternPrefixPair::from_str("foo:E501"); assert!(result.is_ok()); - let result = StrCheckCodePair::from_str("foo: E501"); + let result = PatternPrefixPair::from_str("foo: E501"); assert!(result.is_ok()); - let result = StrCheckCodePair::from_str("E501:foo"); + let result = PatternPrefixPair::from_str("E501:foo"); assert!(result.is_err()); - let result = StrCheckCodePair::from_str("E501"); + let result = PatternPrefixPair::from_str("E501"); assert!(result.is_err()); - let result = StrCheckCodePair::from_str("foo"); + let result = PatternPrefixPair::from_str("foo"); assert!(result.is_err()); - let result = StrCheckCodePair::from_str("foo:E501:E402"); + let result = PatternPrefixPair::from_str("foo:E501:E402"); assert!(result.is_err()); - let result = StrCheckCodePair::from_str("**/bar:E501"); + let result = PatternPrefixPair::from_str("**/bar:E501"); assert!(result.is_ok()); - let result = StrCheckCodePair::from_str("bar:E502"); + let result = PatternPrefixPair::from_str("bar:E502"); assert!(result.is_err()); } } diff --git a/src/settings/types.rs b/src/settings/types.rs index 7b0847992c..e7472d146f 100644 --- a/src/settings/types.rs +++ b/src/settings/types.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use anyhow::{anyhow, Result}; use glob::Pattern; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{de, Deserialize, Deserializer, Serialize}; use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix; @@ -75,24 +75,28 @@ pub struct PerFileIgnore { } 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()); + pub fn new( + pattern: &str, + prefixes: &[CheckCodePrefix], + project_root: &Option, + ) -> Self { + let pattern = FilePattern::from_user(pattern, project_root); + let codes = BTreeSet::from_iter(prefixes.iter().flat_map(|prefix| prefix.codes())); Self { pattern, codes } } } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct StrCheckCodePair { +pub struct PatternPrefixPair { pub pattern: String, - pub code: CheckCodePrefix, + pub prefix: CheckCodePrefix, } -impl StrCheckCodePair { +impl PatternPrefixPair { const EXPECTED_PATTERN: &'static str = ": pattern"; } -impl<'de> Deserialize<'de> for StrCheckCodePair { +impl<'de> Deserialize<'de> for PatternPrefixPair { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -107,17 +111,7 @@ impl<'de> Deserialize<'de> for StrCheckCodePair { } } -impl Serialize for StrCheckCodePair { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let as_str = format!("{}:{}", self.pattern, self.code.as_ref()); - serializer.serialize_str(&as_str) - } -} - -impl FromStr for StrCheckCodePair { +impl FromStr for PatternPrefixPair { type Err = anyhow::Error; fn from_str(string: &str) -> Result { @@ -128,8 +122,8 @@ impl FromStr for StrCheckCodePair { } (tokens[0].trim(), tokens[1].trim()) }; - let code = CheckCodePrefix::from_str(code_string)?; let pattern = pattern_str.into(); - Ok(Self { pattern, code }) + let prefix = CheckCodePrefix::from_str(code_string)?; + Ok(Self { pattern, prefix }) } } diff --git a/src/settings/user.rs b/src/settings/user.rs index d6f7f6fdc8..84e87baaca 100644 --- a/src/settings/user.rs +++ b/src/settings/user.rs @@ -1,11 +1,12 @@ //! Structs to render user-facing settings. +use std::collections::BTreeMap; use std::path::PathBuf; use regex::Regex; use crate::checks_gen::CheckCodePrefix; -use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; +use crate::settings::types::{FilePattern, PythonVersion}; use crate::{flake8_quotes, pep8_naming, Configuration}; /// Struct to render user-facing exclusion patterns. @@ -41,7 +42,7 @@ pub struct UserConfiguration { pub extend_select: Vec, pub ignore: Vec, pub line_length: usize, - pub per_file_ignores: Vec, + pub per_file_ignores: BTreeMap>, pub select: Vec, pub target_version: PythonVersion, // Plugins