diff --git a/Cargo.lock b/Cargo.lock index 11b8123aaf..05fedf6504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -906,6 +906,12 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "gloo-timers" version = "0.2.4" @@ -1751,6 +1757,7 @@ dependencies = [ "dirs 4.0.0", "fern", "filetime", + "glob", "itertools", "log", "notify", diff --git a/Cargo.toml b/Cargo.toml index 3f59f21eb0..d287e6e9c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ common-path = { version = "1.0.0" } dirs = { version = "4.0.0" } fern = { version = "0.6.1" } filetime = { version = "0.2.17" } +glob = "0.3.0" itertools = "0.10.3" log = { version = "0.4.17" } notify = { version = "4.0.17" } diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index 24bc24acc0..4a853c2e3c 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -1,6 +1,6 @@ [tool.ruff] line-length = 88 -extend-exclude = ["excluded\\.py", "migrations"] +extend-exclude = ["excluded.py", "migrations"] select = [ "E402", "E501", diff --git a/src/fs.rs b/src/fs.rs index 2bbbf7353a..b4d39185c9 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -3,40 +3,61 @@ use std::io::{BufReader, Read}; use std::path::{Path, PathBuf}; use anyhow::Result; +use glob::Pattern; use log::debug; -use regex::Regex; use walkdir::{DirEntry, WalkDir}; -fn is_excluded(entry: &DirEntry, exclude: &[Regex]) -> bool { - entry - .path() - .to_str() - .map(|path| exclude.iter().any(|pattern| pattern.is_match(path))) - .unwrap_or(true) +fn is_excluded(path: &Path, exclude: &[Pattern]) -> bool { + if let Some(file_name) = path.file_name() { + if let Some(file_name) = file_name.to_str() { + for pattern in exclude { + if pattern.matches(file_name) { + return true; + } + } + false + } else { + false + } + } else { + false + } } -fn is_included(entry: &DirEntry) -> bool { - let path = entry.path().to_string_lossy(); - path.ends_with(".py") || path.ends_with(".pyi") +fn is_included(path: &Path) -> bool { + let file_name = path.to_string_lossy(); + file_name.ends_with(".py") || file_name.ends_with(".pyi") } pub fn iter_python_files<'a>( path: &'a PathBuf, - exclude: &'a [Regex], + exclude: &'a [Pattern], + extend_exclude: &'a [Pattern], ) -> impl Iterator + 'a { WalkDir::new(path) .follow_links(true) .into_iter() .filter_entry(|entry| { - if is_excluded(entry, exclude) { - debug!("Ignored path: {}", entry.path().to_string_lossy()); + if exclude.is_empty() && extend_exclude.is_empty() { + return true; + } + + let path = entry.path(); + if is_excluded(path, exclude) { + debug!("Ignored path via `exclude`: {:?}", path); + false + } else if is_excluded(path, extend_exclude) { + debug!("Ignored path via `extend-exclude`: {:?}", path); false } else { true } }) .filter_map(|entry| entry.ok()) - .filter(is_included) + .filter(|entry| { + let path = entry.path(); + is_included(path) + }) } pub fn read_file(path: &Path) -> Result { @@ -46,3 +67,46 @@ pub fn read_file(path: &Path) -> Result { buf_reader.read_to_string(&mut contents)?; Ok(contents) } + +#[cfg(test)] +mod tests { + use std::path::Path; + + use glob::Pattern; + + use crate::fs::{is_excluded, is_included}; + + #[test] + fn inclusions() { + let path = Path::new("foo/bar/baz.py"); + assert!(is_included(path)); + + let path = Path::new("foo/bar/baz.pyi"); + assert!(is_included(path)); + + let path = Path::new("foo/bar/baz.js"); + assert!(!is_included(path)); + + let path = Path::new("foo/bar/baz"); + assert!(!is_included(path)); + } + + #[test] + fn exclusions() { + let path = Path::new("foo"); + let exclude = vec![Pattern::new("foo").unwrap()]; + assert!(is_excluded(path, &exclude)); + + let path = Path::new("foo/bar"); + let exclude = vec![Pattern::new("bar").unwrap()]; + assert!(is_excluded(path, &exclude)); + + let path = Path::new("foo/bar/baz.py"); + let exclude = vec![Pattern::new("baz.py").unwrap()]; + assert!(is_excluded(path, &exclude)); + + let path = Path::new("foo/bar/baz.py"); + let exclude = vec![Pattern::new("baz").unwrap()]; + assert!(!is_excluded(path, &exclude)); + } +} diff --git a/src/linter.rs b/src/linter.rs index cd2f447882..4ffe8c8b56 100644 --- a/src/linter.rs +++ b/src/linter.rs @@ -97,6 +97,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E402]), }, &fixer::Mode::Generate, @@ -122,6 +123,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E501]), }, &fixer::Mode::Generate, @@ -147,6 +149,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E711]), }, &fixer::Mode::Generate, @@ -179,6 +182,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E712]), }, &fixer::Mode::Generate, @@ -222,6 +226,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E713]), }, &fixer::Mode::Generate, @@ -247,6 +252,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E721]), }, &fixer::Mode::Generate, @@ -349,6 +355,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E722]), }, &fixer::Mode::Generate, @@ -374,6 +381,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E714]), }, &fixer::Mode::Generate, @@ -399,6 +407,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E731]), }, &fixer::Mode::Generate, @@ -432,6 +441,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E741]), }, &fixer::Mode::Generate, @@ -580,6 +590,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E742]), }, &fixer::Mode::Generate, @@ -618,6 +629,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::E743]), }, &fixer::Mode::Generate, @@ -656,6 +668,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F401]), }, &fixer::Mode::Generate, @@ -693,6 +706,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F403]), }, &fixer::Mode::Generate, @@ -725,6 +739,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F404]), }, &fixer::Mode::Generate, @@ -750,6 +765,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F406]), }, &fixer::Mode::Generate, @@ -782,6 +798,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F407]), }, &fixer::Mode::Generate, @@ -807,6 +824,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F541]), }, &fixer::Mode::Generate, @@ -844,6 +862,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F601]), }, &fixer::Mode::Generate, @@ -880,6 +899,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F602]), }, &fixer::Mode::Generate, @@ -904,6 +924,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F622]), }, &fixer::Mode::Generate, @@ -928,6 +949,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F631]), }, &fixer::Mode::Generate, @@ -960,6 +982,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F632]), }, &fixer::Mode::Generate, @@ -992,6 +1015,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F633]), }, &fixer::Mode::Generate, @@ -1017,6 +1041,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F634]), }, &fixer::Mode::Generate, @@ -1049,6 +1074,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F701]), }, &fixer::Mode::Generate, @@ -1091,6 +1117,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F702]), }, &fixer::Mode::Generate, @@ -1133,6 +1160,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F704]), }, &fixer::Mode::Generate, @@ -1170,6 +1198,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F706]), }, &fixer::Mode::Generate, @@ -1202,6 +1231,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F707]), }, &fixer::Mode::Generate, @@ -1239,6 +1269,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F722]), }, &fixer::Mode::Generate, @@ -1264,6 +1295,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F821]), }, &fixer::Mode::Generate, @@ -1311,6 +1343,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F822]), }, &fixer::Mode::Generate, @@ -1336,6 +1369,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F823]), }, &fixer::Mode::Generate, @@ -1361,6 +1395,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F831]), }, &fixer::Mode::Generate, @@ -1398,6 +1433,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F841]), }, &fixer::Mode::Generate, @@ -1445,6 +1481,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F901]), }, &fixer::Mode::Generate, @@ -1477,6 +1514,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::R001]), }, &fixer::Mode::Generate, @@ -1699,6 +1737,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::R002]), }, &fixer::Mode::Generate, @@ -1741,6 +1780,7 @@ mod tests { &settings::Settings { line_length: 88, exclude: vec![], + extend_exclude: vec![], select: BTreeSet::from([CheckCode::F401, CheckCode::F821]), }, &fixer::Mode::Generate, diff --git a/src/main.rs b/src/main.rs index 22bbb722fa..dfa94cd205 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,10 @@ use std::time::Instant; use anyhow::Result; use clap::{Parser, ValueHint}; use colored::Colorize; +use glob::Pattern; use log::{debug, error}; use notify::{raw_watcher, RecursiveMode, Watcher}; use rayon::prelude::*; -use regex::Regex; use walkdir::DirEntry; use ::ruff::checks::CheckCode; @@ -54,12 +54,12 @@ struct Cli { /// List of error codes to ignore. #[clap(long, multiple = true)] ignore: Vec, - /// List of regular expressions, used to exclude files and/or directories from checks. + /// List of paths, used to exclude files and/or directories from checks. #[clap(long, multiple = true)] - exclude: Vec, + exclude: Vec, /// Like --exclude, but adds additional files and directories on top of the excluded ones. #[clap(long, multiple = true)] - extend_exclude: Vec, + extend_exclude: Vec, } #[cfg(feature = "update-informer")] @@ -96,7 +96,7 @@ fn run_once( let start = Instant::now(); let paths: Vec = files .iter() - .flat_map(|path| iter_python_files(path, &settings.exclude)) + .flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude)) .collect(); let duration = start.elapsed(); debug!("Identified files to lint in: {:?}", duration); @@ -194,10 +194,10 @@ fn inner_main() -> Result { settings.ignore(&cli.ignore); } if !cli.exclude.is_empty() { - settings.exclude(cli.exclude); + settings.exclude = cli.exclude; } if !cli.extend_exclude.is_empty() { - settings.extend_exclude(cli.extend_exclude); + settings.extend_exclude = cli.extend_exclude; } if cli.watch { diff --git a/src/pyproject.rs b/src/pyproject.rs index 415968b2d9..25b1845f26 100644 --- a/src/pyproject.rs +++ b/src/pyproject.rs @@ -12,7 +12,7 @@ pub fn load_config(paths: &[PathBuf]) -> Config { match find_project_root(paths) { Some(project_root) => match find_pyproject_toml(&project_root) { Some(path) => { - debug!("Found pyproject.toml at: {}", path.to_string_lossy()); + debug!("Found pyproject.toml at: {:?}", path); match parse_pyproject_toml(&path) { Ok(pyproject) => pyproject .tool @@ -260,7 +260,7 @@ other-attribute = 1 line_length: Some(88), exclude: None, extend_exclude: Some(vec![ - Path::new("excluded\\.py").to_path_buf(), + Path::new("excluded.py").to_path_buf(), Path::new("migrations").to_path_buf() ]), select: Some(vec![ diff --git a/src/settings.rs b/src/settings.rs index 2ef21d96aa..ee565532d3 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,8 +1,9 @@ use std::collections::BTreeSet; use std::hash::{Hash, Hasher}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -use regex::Regex; +use glob::Pattern; +use once_cell::sync::Lazy; use crate::checks::CheckCode; use crate::pyproject::load_config; @@ -10,7 +11,8 @@ use crate::pyproject::load_config; #[derive(Debug)] pub struct Settings { pub line_length: usize, - pub exclude: Vec, + pub exclude: Vec, + pub extend_exclude: Vec, pub select: BTreeSet, } @@ -22,6 +24,28 @@ impl Hash for Settings { } } } +static DEFAULT_EXCLUDE: Lazy> = Lazy::new(|| { + vec![ + Pattern::new(".bzr").unwrap(), + Pattern::new(".direnv").unwrap(), + Pattern::new(".eggs").unwrap(), + Pattern::new(".git").unwrap(), + Pattern::new(".hg").unwrap(), + Pattern::new(".mypy_cache").unwrap(), + Pattern::new(".nox").unwrap(), + Pattern::new(".pants.d").unwrap(), + Pattern::new(".svn").unwrap(), + Pattern::new(".tox").unwrap(), + Pattern::new(".venv").unwrap(), + Pattern::new("__pypackages__").unwrap(), + Pattern::new("_build").unwrap(), + Pattern::new("buck-out").unwrap(), + Pattern::new("build").unwrap(), + Pattern::new("dist").unwrap(), + Pattern::new("node_modules").unwrap(), + Pattern::new("venv").unwrap(), + ] +}); impl Settings { pub fn from_paths(paths: &[PathBuf]) -> Self { @@ -30,28 +54,26 @@ impl Settings { line_length: config.line_length.unwrap_or(88), exclude: config .exclude - .unwrap_or_else(|| { - vec![ - Path::new("\\.direnv").to_path_buf(), - Path::new("\\.eggs").to_path_buf(), - Path::new("\\.git").to_path_buf(), - Path::new("\\.hg").to_path_buf(), - Path::new("\\.mypy_cache").to_path_buf(), - Path::new("\\.nox").to_path_buf(), - Path::new("\\.svn").to_path_buf(), - Path::new("\\.tox").to_path_buf(), - Path::new("\\.venv").to_path_buf(), - Path::new("__pypackages__").to_path_buf(), - Path::new("_build").to_path_buf(), - Path::new("buck-out").to_path_buf(), - Path::new("build").to_path_buf(), - Path::new("dist").to_path_buf(), - Path::new("venv").to_path_buf(), - ] + .map(|paths| { + paths + .into_iter() + .map(|path| { + Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.") + }) + .collect() }) - .into_iter() - .map(|path| Regex::new(&path.to_string_lossy()).expect("Invalid pattern.")) - .collect(), + .unwrap_or_else(|| DEFAULT_EXCLUDE.clone()), + extend_exclude: config + .extend_exclude + .map(|paths| { + paths + .into_iter() + .map(|path| { + Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.") + }) + .collect() + }) + .unwrap_or_default(), select: BTreeSet::from_iter(config.select.unwrap_or_else(|| { vec![ CheckCode::E402, @@ -116,12 +138,4 @@ impl Settings { self.select.remove(code); } } - - pub fn exclude(&mut self, exclude: Vec) { - self.exclude = exclude; - } - - pub fn extend_exclude(&mut self, exclude: Vec) { - self.exclude.extend(exclude); - } }