diff --git a/Cargo.lock b/Cargo.lock index 53012203e9..fd52e25888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -769,10 +769,17 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.0" +name = "globset" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr 0.2.17", + "fnv", + "log", + "regex", +] [[package]] name = "half" @@ -1787,7 +1794,7 @@ dependencies = [ "fern", "filetime", "getrandom 0.2.8", - "glob", + "globset", "insta", "itertools", "libcst", diff --git a/Cargo.toml b/Cargo.toml index 5a6f061354..65de8065e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ common-path = { version = "1.0.0" } dirs = { version = "4.0.0" } fern = { version = "0.6.1" } filetime = { version = "0.2.17" } -glob = { version = "0.3.0" } +globset = {version = "0.4.9" } itertools = { version = "0.10.5" } libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" } log = { version = "0.4.17" } diff --git a/resources/test/fixtures/with_excluded_mdirectory/__init__.py b/resources/test/fixtures/with_excluded_directory/__init__.py similarity index 100% rename from resources/test/fixtures/with_excluded_mdirectory/__init__.py rename to resources/test/fixtures/with_excluded_directory/__init__.py diff --git a/resources/test/fixtures/with_excluded_mdirectory/migrations/__init__.py b/resources/test/fixtures/with_excluded_directory/migrations/__init__.py similarity index 100% rename from resources/test/fixtures/with_excluded_mdirectory/migrations/__init__.py rename to resources/test/fixtures/with_excluded_directory/migrations/__init__.py diff --git a/resources/test/fixtures/with_excluded_mdirectory/migrations/migration.py b/resources/test/fixtures/with_excluded_directory/migrations/migration.py similarity index 100% rename from resources/test/fixtures/with_excluded_mdirectory/migrations/migration.py rename to resources/test/fixtures/with_excluded_directory/migrations/migration.py diff --git a/src/cli.rs b/src/cli.rs index fba82043e6..daa32a72d2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; -use anyhow::Result; use clap::{command, Parser}; use regex::Regex; use rustc_hash::FxHashMap; @@ -9,7 +8,7 @@ use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix; use crate::logging::LogLevel; use crate::printer::SerializationFormat; -use crate::settings::types::{PatternPrefixPair, PerFileIgnore, PythonVersion}; +use crate::settings::types::{FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion}; #[derive(Debug, Parser)] #[command(author, about = "Ruff: An extremely fast Python linter.")] @@ -61,11 +60,11 @@ pub struct Cli { pub extend_ignore: Vec, /// List of paths, used to exclude files and/or directories from checks. #[arg(long, value_delimiter = ',')] - pub exclude: Vec, + pub exclude: Vec, /// Like --exclude, but adds additional files and directories on top of the /// excluded ones. #[arg(long, value_delimiter = ',')] - pub extend_exclude: Vec, + pub extend_exclude: Vec, /// List of error codes to treat as eligible for autofix. Only applicable /// when autofix itself is enabled (e.g., via `--fix`). #[arg(long, value_delimiter = ',')] @@ -78,7 +77,7 @@ pub struct Cli { #[arg(long, value_delimiter = ',')] pub per_file_ignores: Vec, /// Output serialization format for error messages. - #[arg(long, value_enum, default_value_t=SerializationFormat::Text)] + #[arg(long, value_enum, default_value_t = SerializationFormat::Text)] pub format: SerializationFormat, /// Show violations with source code. #[arg(long)] @@ -149,10 +148,7 @@ pub fn extract_log_level(cli: &Cli) -> LogLevel { } /// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`. -pub fn collect_per_file_ignores( - pairs: Vec, - project_root: Option<&PathBuf>, -) -> Result> { +pub fn collect_per_file_ignores(pairs: Vec) -> Vec { let mut per_file_ignores: FxHashMap> = FxHashMap::default(); for pair in pairs { per_file_ignores @@ -161,7 +157,7 @@ pub fn collect_per_file_ignores( .push(pair.prefix); } per_file_ignores - .iter() - .map(|(pattern, prefixes)| PerFileIgnore::new(pattern, prefixes, project_root)) + .into_iter() + .map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes)) .collect() } diff --git a/src/fs.rs b/src/fs.rs index ea5a7fc830..15a78867fc 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,16 +1,16 @@ use std::borrow::Cow; +use std::collections::BTreeSet; use std::fs::File; use std::io::{BufReader, Read}; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; +use globset::GlobMatcher; use log::debug; use path_absolutize::{path_dedot, Absolutize}; -use rustc_hash::FxHashSet; use walkdir::{DirEntry, WalkDir}; use crate::checks::CheckCode; -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)> { @@ -25,32 +25,8 @@ fn extract_path_names(path: &Path) -> Result<(&str, &str)> { Ok((file_path, file_basename)) } -fn is_excluded<'a, T>(file_path: &str, file_basename: &str, exclude: T) -> bool -where - T: Iterator, -{ - for pattern in exclude { - match pattern { - FilePattern::Simple(basename) => { - if *basename == file_basename { - return true; - } - } - FilePattern::Complex(absolute, basename) => { - if absolute.matches(file_path) { - return true; - } - if basename - .as_ref() - .map(|pattern| pattern.matches(file_basename)) - .unwrap_or_default() - { - return true; - } - } - }; - } - false +fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet) -> bool { + exclude.is_match(file_path) || exclude.is_match(file_basename) } fn is_included(path: &Path) -> bool { @@ -60,18 +36,12 @@ fn is_included(path: &Path) -> bool { pub fn iter_python_files<'a>( path: &'a Path, - exclude: &'a [FilePattern], - extend_exclude: &'a [FilePattern], + exclude: &'a globset::GlobSet, + extend_exclude: &'a globset::GlobSet, ) -> impl Iterator> + 'a { // Run some checks over the provided patterns, to enable optimizations below. let has_exclude = !exclude.is_empty(); let has_extend_exclude = !extend_exclude.is_empty(); - let exclude_simple = exclude - .iter() - .all(|pattern| matches!(pattern, FilePattern::Simple(_))); - let extend_exclude_simple = extend_exclude - .iter() - .all(|pattern| matches!(pattern, FilePattern::Simple(_))); WalkDir::new(normalize_path(path)) .into_iter() @@ -83,17 +53,11 @@ pub fn iter_python_files<'a>( let path = entry.path(); match extract_path_names(path) { Ok((file_path, file_basename)) => { - let file_type = entry.file_type(); - - if has_exclude - && (!exclude_simple || file_type.is_dir()) - && is_excluded(file_path, file_basename, exclude.iter()) - { + if has_exclude && is_excluded(file_path, file_basename, exclude) { debug!("Ignored path via `exclude`: {:?}", path); false } else if has_extend_exclude - && (!extend_exclude_simple || file_type.is_dir()) - && is_excluded(file_path, file_basename, extend_exclude.iter()) + && is_excluded(file_path, file_basename, extend_exclude) { debug!("Ignored path via `extend-exclude`: {:?}", path); false @@ -119,19 +83,15 @@ pub fn iter_python_files<'a>( /// Create tree set with codes matching the pattern/code pairs. pub(crate) fn ignores_from_path<'a>( path: &Path, - pattern_code_pairs: &'a [PerFileIgnore], -) -> Result> { + pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, BTreeSet)], +) -> Result> { let (file_path, file_basename) = extract_path_names(path)?; Ok(pattern_code_pairs .iter() - .filter(|pattern_code_pair| { - is_excluded( - file_path, - file_basename, - [&pattern_code_pair.pattern].into_iter(), - ) + .filter(|(absolute, basename, _)| { + basename.is_match(file_basename) || absolute.is_match(file_path) }) - .flat_map(|pattern_code_pair| &pattern_code_pair.codes) + .flat_map(|(_, _, codes)| codes) .collect()) } @@ -171,9 +131,10 @@ pub(crate) fn read_file(path: &Path) -> Result { #[cfg(test)] mod tests { - use std::path::Path; + use std::path::{Path, PathBuf}; use anyhow::Result; + use globset::GlobSet; use path_absolutize::Absolutize; use crate::fs::{extract_path_names, is_excluded, is_included}; @@ -194,73 +155,86 @@ mod tests { assert!(!is_included(&path)); } + fn make_exclusion(file_pattern: FilePattern, project_root: Option<&PathBuf>) -> GlobSet { + let mut builder = globset::GlobSetBuilder::new(); + file_pattern.add_to(&mut builder, project_root).unwrap(); + builder.build().unwrap() + } + #[test] fn exclusions() -> Result<()> { let project_root = Path::new("/tmp/"); let path = Path::new("foo").absolutize_from(project_root).unwrap(); - let exclude = vec![FilePattern::from_user( - "foo", - Some(&project_root.to_path_buf()), - )?]; + let exclude = FilePattern::User("foo".to_string()); let (file_path, file_basename) = extract_path_names(&path)?; - assert!(is_excluded(file_path, file_basename, exclude.iter())); + assert!(is_excluded( + file_path, + file_basename, + &make_exclusion(exclude, Some(&project_root.to_path_buf())) + )); let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); - let exclude = vec![FilePattern::from_user( - "bar", - Some(&project_root.to_path_buf()), - )?]; + let exclude = FilePattern::User("bar".to_string()); let (file_path, file_basename) = extract_path_names(&path)?; - assert!(is_excluded(file_path, file_basename, exclude.iter())); + assert!(is_excluded( + file_path, + file_basename, + &make_exclusion(exclude, Some(&project_root.to_path_buf())) + )); let path = Path::new("foo/bar/baz.py") .absolutize_from(project_root) .unwrap(); - let exclude = vec![FilePattern::from_user( - "baz.py", - Some(&project_root.to_path_buf()), - )?]; + let exclude = FilePattern::User("baz.py".to_string()); let (file_path, file_basename) = extract_path_names(&path)?; - assert!(is_excluded(file_path, file_basename, exclude.iter())); + assert!(is_excluded( + file_path, + file_basename, + &make_exclusion(exclude, Some(&project_root.to_path_buf())) + )); let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); - let exclude = vec![FilePattern::from_user( - "foo/bar", - Some(&project_root.to_path_buf()), - )?]; + let exclude = FilePattern::User("foo/bar".to_string()); let (file_path, file_basename) = extract_path_names(&path)?; - assert!(is_excluded(file_path, file_basename, exclude.iter())); + assert!(is_excluded( + file_path, + file_basename, + &make_exclusion(exclude, Some(&project_root.to_path_buf())) + )); let path = Path::new("foo/bar/baz.py") .absolutize_from(project_root) .unwrap(); - let exclude = vec![FilePattern::from_user( - "foo/bar/baz.py", - Some(&project_root.to_path_buf()), - )?]; + let exclude = FilePattern::User("foo/bar/baz.py".to_string()); let (file_path, file_basename) = extract_path_names(&path)?; - assert!(is_excluded(file_path, file_basename, exclude.iter())); + assert!(is_excluded( + file_path, + file_basename, + &make_exclusion(exclude, Some(&project_root.to_path_buf())) + )); let path = Path::new("foo/bar/baz.py") .absolutize_from(project_root) .unwrap(); - let exclude = vec![FilePattern::from_user( - "foo/bar/*.py", - Some(&project_root.to_path_buf()), - )?]; + let exclude = FilePattern::User("foo/bar/*.py".to_string()); let (file_path, file_basename) = extract_path_names(&path)?; - assert!(is_excluded(file_path, file_basename, exclude.iter())); + assert!(is_excluded( + file_path, + file_basename, + &make_exclusion(exclude, Some(&project_root.to_path_buf())) + )); let path = Path::new("foo/bar/baz.py") .absolutize_from(project_root) .unwrap(); - let exclude = vec![FilePattern::from_user( - "baz", - Some(&project_root.to_path_buf()), - )?]; + let exclude = FilePattern::User("baz".to_string()); let (file_path, file_basename) = extract_path_names(&path)?; - assert!(!is_excluded(file_path, file_basename, exclude.iter())); + assert!(!is_excluded( + file_path, + file_basename, + &make_exclusion(exclude, Some(&project_root.to_path_buf())) + )); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 849d44e84e..2120a1d9c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,10 +87,10 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result> { None => debug!("Unable to find pyproject.toml; using default settings..."), }; - let settings = Settings::from_configuration(Configuration::from_pyproject( - pyproject.as_ref(), + let settings = Settings::from_configuration( + Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?, project_root.as_ref(), - )?); + )?; // Tokenize once. let tokens: Vec = tokenize(contents); diff --git a/src/main.rs b/src/main.rs index c5408cc2d7..84ece31b6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,6 @@ use ::ruff::logging::{set_up_logging, LogLevel}; use ::ruff::message::Message; use ::ruff::printer::{Printer, SerializationFormat}; use ::ruff::settings::configuration::Configuration; -use ::ruff::settings::types::FilePattern; use ::ruff::settings::user::UserConfiguration; use ::ruff::settings::{pyproject, Settings}; #[cfg(feature = "update-informer")] @@ -266,28 +265,16 @@ fn inner_main() -> Result { }; // Reconcile configuration from pyproject.toml and command-line arguments. - let exclude: Vec = cli - .exclude - .iter() - .map(|path| FilePattern::from_user(path, project_root.as_ref())) - .collect::>()?; - let extend_exclude: Vec = cli - .extend_exclude - .iter() - .map(|path| FilePattern::from_user(path, project_root.as_ref())) - .collect::>()?; - let mut configuration = Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?; - if !exclude.is_empty() { - configuration.exclude = exclude; + if !cli.exclude.is_empty() { + configuration.exclude = cli.exclude; } - if !extend_exclude.is_empty() { - configuration.extend_exclude = extend_exclude; + if !cli.extend_exclude.is_empty() { + configuration.extend_exclude = cli.extend_exclude; } if !cli.per_file_ignores.is_empty() { - configuration.per_file_ignores = - collect_per_file_ignores(cli.per_file_ignores, project_root.as_ref())?; + configuration.per_file_ignores = collect_per_file_ignores(cli.per_file_ignores); } if !cli.select.is_empty() { configuration.select = cli.select; @@ -342,7 +329,7 @@ fn inner_main() -> Result { // Extract settings for internal use. let fix_enabled: bool = configuration.fix; - let settings = Settings::from_configuration(configuration); + let settings = Settings::from_configuration(configuration, project_root.as_ref())?; if cli.show_files { show_files(&cli.files, &settings); diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 6cdf1fdfd6..324ec8a93b 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -46,25 +46,25 @@ pub struct Configuration { 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"), + FilePattern::Builtin(".bzr"), + FilePattern::Builtin(".direnv"), + FilePattern::Builtin(".eggs"), + FilePattern::Builtin(".git"), + FilePattern::Builtin(".hg"), + FilePattern::Builtin(".mypy_cache"), + FilePattern::Builtin(".nox"), + FilePattern::Builtin(".pants.d"), + FilePattern::Builtin(".ruff_cache"), + FilePattern::Builtin(".svn"), + FilePattern::Builtin(".tox"), + FilePattern::Builtin(".venv"), + FilePattern::Builtin("__pypackages__"), + FilePattern::Builtin("_build"), + FilePattern::Builtin("buck-out"), + FilePattern::Builtin("build"), + FilePattern::Builtin("dist"), + FilePattern::Builtin("node_modules"), + FilePattern::Builtin("venv"), ] }); @@ -103,22 +103,14 @@ impl Configuration { }, ), 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() - }) - .transpose()? - .unwrap_or_else(|| DEFAULT_EXCLUDE.clone()), + exclude: options.exclude.map_or_else( + || DEFAULT_EXCLUDE.clone(), + |paths| paths.into_iter().map(FilePattern::User).collect(), + ), extend_exclude: options .extend_exclude - .unwrap_or_default() - .iter() - .map(|path| FilePattern::from_user(path, project_root)) - .collect::>()?, + .map(|paths| paths.into_iter().map(FilePattern::User).collect()) + .unwrap_or_default(), extend_ignore: options.extend_ignore.unwrap_or_default(), select: options .select @@ -154,13 +146,10 @@ impl Configuration { .per_file_ignores .map(|per_file_ignores| { per_file_ignores - .iter() - .map(|(pattern, prefixes)| { - PerFileIgnore::new(pattern, prefixes, project_root) - }) + .into_iter() + .map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes)) .collect() }) - .transpose()? .unwrap_or_default(), show_source: options.show_source.unwrap_or_default(), // Plugins diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 3be7149ce2..602762a2fc 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -2,9 +2,12 @@ //! 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 std::path::PathBuf; +use std::path::{Path, PathBuf}; +use anyhow::Result; +use globset::{Glob, GlobMatcher, GlobSet}; use path_absolutize::path_dedot; use regex::Regex; use rustc_hash::FxHashSet; @@ -14,7 +17,7 @@ use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity}; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; use crate::{ - flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, isort, mccabe, + flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, fs, isort, mccabe, pep8_naming, }; @@ -28,11 +31,11 @@ pub mod user; pub struct Settings { pub dummy_variable_rgx: Regex, pub enabled: FxHashSet, - pub exclude: Vec, - pub extend_exclude: Vec, + pub exclude: GlobSet, + pub extend_exclude: GlobSet, pub fixable: FxHashSet, pub line_length: usize, - pub per_file_ignores: Vec, + pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, BTreeSet)>, pub show_source: bool, pub src: Vec, pub target_version: PythonVersion, @@ -47,8 +50,11 @@ pub struct Settings { } impl Settings { - pub fn from_configuration(config: Configuration) -> Self { - Self { + pub fn from_configuration( + config: Configuration, + project_root: Option<&PathBuf>, + ) -> Result { + Ok(Self { dummy_variable_rgx: config.dummy_variable_rgx, enabled: resolve_codes( &config @@ -62,8 +68,8 @@ impl Settings { .chain(config.extend_ignore.into_iter()) .collect::>(), ), - exclude: config.exclude, - extend_exclude: config.extend_exclude, + exclude: resolve_globset(config.exclude, project_root)?, + extend_exclude: resolve_globset(config.extend_exclude, project_root)?, fixable: resolve_codes(&config.fixable, &config.unfixable), flake8_annotations: config.flake8_annotations, flake8_bugbear: config.flake8_bugbear, @@ -73,11 +79,11 @@ impl Settings { mccabe: config.mccabe, 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, project_root)?, src: config.src, target_version: config.target_version, show_source: config.show_source, - } + }) } pub fn for_rule(check_code: CheckCode) -> Self { @@ -85,8 +91,8 @@ impl Settings { dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(), enabled: FxHashSet::from_iter([check_code.clone()]), fixable: FxHashSet::from_iter([check_code]), - exclude: vec![], - extend_exclude: vec![], + exclude: GlobSet::empty(), + extend_exclude: GlobSet::empty(), line_length: 88, per_file_ignores: vec![], src: vec![path_dedot::CWD.clone()], @@ -107,8 +113,8 @@ impl Settings { dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(), enabled: FxHashSet::from_iter(check_codes.clone()), fixable: FxHashSet::from_iter(check_codes), - exclude: vec![], - extend_exclude: vec![], + exclude: GlobSet::empty(), + extend_exclude: GlobSet::empty(), line_length: 88, per_file_ignores: vec![], src: vec![path_dedot::CWD.clone()], @@ -136,8 +142,10 @@ impl Hash for Settings { value.hash(state); } self.line_length.hash(state); - for value in &self.per_file_ignores { - value.hash(state); + for (absolute, basename, codes) in &self.per_file_ignores { + absolute.glob().hash(state); + basename.glob().hash(state); + codes.hash(state); } self.show_source.hash(state); self.target_version.hash(state); @@ -152,6 +160,42 @@ impl Hash for Settings { } } +/// Given a list of patterns, create a `GlobSet`. +pub fn resolve_globset( + patterns: Vec, + project_root: Option<&PathBuf>, +) -> Result { + let mut builder = globset::GlobSetBuilder::new(); + for pattern in patterns { + pattern.add_to(&mut builder, project_root)?; + } + builder.build().map_err(std::convert::Into::into) +} + +/// Given a list of patterns, create a `GlobSet`. +pub fn resolve_per_file_ignores( + per_file_ignores: Vec, + project_root: Option<&PathBuf>, +) -> Result)>> { + per_file_ignores + .into_iter() + .map(|per_file_ignore| { + // Construct absolute path matcher. + let path = Path::new(&per_file_ignore.pattern); + let absolute_path = match project_root { + Some(project_root) => fs::normalize_path_to(path, project_root), + None => fs::normalize_path(path), + }; + let absolute = Glob::new(&absolute_path.to_string_lossy())?.compile_matcher(); + + // Construct basename matcher. + let basename = Glob::new(&per_file_ignore.pattern)?.compile_matcher(); + + Ok((absolute, basename, per_file_ignore.codes)) + }) + .collect() +} + /// Given a set of selected and ignored prefixes, resolve the set of enabled /// error codes. fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet { diff --git a/src/settings/types.rs b/src/settings/types.rs index c05d3ffa7a..c6b80ddbf5 100644 --- a/src/settings/types.rs +++ b/src/settings/types.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use anyhow::{anyhow, Result}; -use glob::Pattern; +use globset::{Glob, GlobSetBuilder}; use serde::{de, Deserialize, Deserializer, Serialize}; use crate::checks::CheckCode; @@ -44,46 +44,59 @@ impl FromStr for PythonVersion { } } -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone)] pub enum FilePattern { - Simple(&'static str), - Complex(Pattern, Option), + Builtin(&'static str), + User(String), } impl FilePattern { - pub fn from_user(pattern: &str, project_root: Option<&PathBuf>) -> Result { - 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), - }; + pub fn add_to( + self, + builder: &mut GlobSetBuilder, + project_root: Option<&PathBuf>, + ) -> Result<()> { + match self { + FilePattern::Builtin(pattern) => { + builder.add(Glob::from_str(pattern)?); + } + FilePattern::User(pattern) => { + // Add absolute path. + 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), + }; + builder.add(Glob::new(&absolute_path.to_string_lossy())?); - let absolute = Pattern::new(&absolute_path.to_string_lossy())?; - let basename = if pattern.contains(std::path::MAIN_SEPARATOR) { - None - } else { - Some(Pattern::new(pattern)?) - }; + // Add basename path. + if !pattern.contains(std::path::MAIN_SEPARATOR) { + builder.add(Glob::from_str(&pattern)?); + } + } + } + Ok(()) + } +} - Ok(FilePattern::Complex(absolute, basename)) +impl FromStr for FilePattern { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(Self::User(s.into())) } } #[derive(Debug, Clone, Hash)] pub struct PerFileIgnore { - pub pattern: FilePattern, + pub pattern: String, pub codes: BTreeSet, } impl PerFileIgnore { - pub fn new( - pattern: &str, - prefixes: &[CheckCodePrefix], - project_root: Option<&PathBuf>, - ) -> Result { - let pattern = FilePattern::from_user(pattern, project_root)?; + pub fn new(pattern: String, prefixes: &[CheckCodePrefix]) -> Self { let codes = prefixes.iter().flat_map(CheckCodePrefix::codes).collect(); - Ok(Self { pattern, codes }) + Self { pattern, codes } } } @@ -115,9 +128,9 @@ impl<'de> Deserialize<'de> for PatternPrefixPair { impl FromStr for PatternPrefixPair { type Err = anyhow::Error; - fn from_str(string: &str) -> Result { + fn from_str(s: &str) -> Result { let (pattern_str, code_string) = { - let tokens = string.split(':').collect::>(); + let tokens = s.split(':').collect::>(); if tokens.len() != 2 { return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN)); } diff --git a/src/settings/user.rs b/src/settings/user.rs index 7cf3446626..877373f8a0 100644 --- a/src/settings/user.rs +++ b/src/settings/user.rs @@ -4,49 +4,25 @@ use std::path::PathBuf; use regex::Regex; -use crate::checks::CheckCode; use crate::checks_gen::CheckCodePrefix; -use crate::settings::types::{FilePattern, PythonVersion}; +use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion}; use crate::{ flake8_annotations, flake8_quotes, flake8_tidy_imports, isort, pep8_naming, 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 exclude: Vec, + pub extend_exclude: Vec, pub extend_ignore: Vec, pub extend_select: Vec, pub fix: bool, pub fixable: Vec, pub ignore: Vec, pub line_length: usize, - pub per_file_ignores: Vec<(Exclusion, Vec)>, + pub per_file_ignores: Vec, pub select: Vec, pub show_source: bool, pub src: Vec, @@ -71,16 +47,8 @@ impl UserConfiguration { ) -> Self { Self { dummy_variable_rgx: configuration.dummy_variable_rgx, - exclude: configuration - .exclude - .into_iter() - .map(Exclusion::from_file_pattern) - .collect(), - extend_exclude: configuration - .extend_exclude - .into_iter() - .map(Exclusion::from_file_pattern) - .collect(), + exclude: configuration.exclude, + extend_exclude: configuration.extend_exclude, extend_ignore: configuration.extend_ignore, extend_select: configuration.extend_select, fix: configuration.fix, @@ -88,16 +56,7 @@ impl UserConfiguration { unfixable: configuration.unfixable, ignore: configuration.ignore, line_length: configuration.line_length, - per_file_ignores: configuration - .per_file_ignores - .into_iter() - .map(|per_file_ignore| { - ( - Exclusion::from_file_pattern(per_file_ignore.pattern), - Vec::from_iter(per_file_ignore.codes), - ) - }) - .collect(), + per_file_ignores: configuration.per_file_ignores, select: configuration.select, src: configuration.src, target_version: configuration.target_version,