diff --git a/Cargo.lock b/Cargo.lock index 3bae92e138..bf61c8354b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1432,6 +1432,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "path-absolutize" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3de4b40bd9736640f14c438304c09538159802388febb02c8abaae0846c1f13" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-dedot" +version = "3.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d611d5291372b3738a34ebf0d1f849e58b1dcc1101032f76a346eaa1f8ddbb5b" +dependencies = [ + "once_cell", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1802,6 +1820,7 @@ dependencies = [ "log", "notify", "once_cell", + "path-absolutize", "rayon", "regex", "rustpython-parser", diff --git a/Cargo.toml b/Cargo.toml index 95dd78b4b6..98bb5a74cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ itertools = "0.10.3" log = { version = "0.4.17" } notify = { version = "4.0.17" } once_cell = { version = "1.13.1" } +path-absolutize = "3.0.13" rayon = { version = "1.5.3" } regex = { version = "1.6.0" } rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" } diff --git a/README.md b/README.md index 90fcab62a2..c56e49cf97 100644 --- a/README.md +++ b/README.md @@ -96,20 +96,42 @@ ARGS: ... OPTIONS: - -e, --exit-zero Exit with status code "0", even upon detecting errors - --exclude ... List of file and/or directory patterns to exclude from checks - -f, --fix Attempt to automatically fix lint errors - --format Output serialization format for error messages [default: text] [possible values: text, json] - -h, --help Print help information - --ignore ... List of error codes to ignore - -n, --no-cache Disable cache reads - -q, --quiet Disable all logging (but still exit with status code "1" upon - detecting errors) - --select ... + List of error codes to enable + -v, --verbose + Enable verbose logging + -w, --watch + Run in watch mode by re-running whenever files change ``` +Exclusions are based on globs, and can be either: + +- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the + tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching + `foo_*.py` ). +- Relative patterns, like `./directory/foo.py` (to exclude that specific file) or `./directory/*.py` + (to exclude any Python files in `./directory`). Note that these paths are relative to the + directory from which you execute `ruff`, and _not_ the directory of the `pyproject.toml`. + ### Compatibility with Black ruff is intended to be compatible with [Black](https://github.com/psf/black), and should be @@ -222,28 +244,28 @@ Add this `pyproject.toml` to the CPython directory: [tool.ruff] line-length = 88 exclude = [ - "Lib/lib2to3/tests/data/bom.py", - "Lib/lib2to3/tests/data/crlf.py", - "Lib/lib2to3/tests/data/different_encoding.py", - "Lib/lib2to3/tests/data/false_encoding.py", - "Lib/lib2to3/tests/data/py2_test_grammar.py", - "Lib/test/bad_coding2.py", - "Lib/test/badsyntax_3131.py", - "Lib/test/badsyntax_pep3120.py", - "Lib/test/encoded_modules/module_iso_8859_1.py", - "Lib/test/encoded_modules/module_koi8_r.py", - "Lib/test/test_fstring.py", - "Lib/test/test_grammar.py", - "Lib/test/test_importlib/test_util.py", - "Lib/test/test_named_expressions.py", - "Lib/test/test_patma.py", - "Lib/test/test_source_encoding.py", - "Tools/c-analyzer/c_parser/parser/_delim.py", - "Tools/i18n/pygettext.py", - "Tools/test2to3/maintest.py", - "Tools/test2to3/setup.py", - "Tools/test2to3/test/test_foo.py", - "Tools/test2to3/test2to3/hello.py", + "./resources/test/cpython/Lib/lib2to3/tests/data/bom.py", + "./resources/test/cpython/Lib/lib2to3/tests/data/crlf.py", + "./resources/test/cpython/Lib/lib2to3/tests/data/different_encoding.py", + "./resources/test/cpython/Lib/lib2to3/tests/data/false_encoding.py", + "./resources/test/cpython/Lib/lib2to3/tests/data/py2_test_grammar.py", + "./resources/test/cpython/Lib/test/bad_coding2.py", + "./resources/test/cpython/Lib/test/badsyntax_3131.py", + "./resources/test/cpython/Lib/test/badsyntax_pep3120.py", + "./resources/test/cpython/Lib/test/encoded_modules/module_iso_8859_1.py", + "./resources/test/cpython/Lib/test/encoded_modules/module_koi8_r.py", + "./resources/test/cpython/Lib/test/test_fstring.py", + "./resources/test/cpython/Lib/test/test_grammar.py", + "./resources/test/cpython/Lib/test/test_importlib/test_util.py", + "./resources/test/cpython/Lib/test/test_named_expressions.py", + "./resources/test/cpython/Lib/test/test_patma.py", + "./resources/test/cpython/Lib/test/test_source_encoding.py", + "./resources/test/cpython/Tools/c-analyzer/c_parser/parser/_delim.py", + "./resources/test/cpython/Tools/i18n/pygettext.py", + "./resources/test/cpython/Tools/test2to3/maintest.py", + "./resources/test/cpython/Tools/test2to3/setup.py", + "./resources/test/cpython/Tools/test2to3/test/test_foo.py", + "./resources/test/cpython/Tools/test2to3/test2to3/hello.py", ] ``` diff --git a/resources/test/fixtures/directory/also_excluded.py b/resources/test/fixtures/directory/also_excluded.py new file mode 100644 index 0000000000..c9b6c43d41 --- /dev/null +++ b/resources/test/fixtures/directory/also_excluded.py @@ -0,0 +1,9 @@ +a = "abc" +b = f"ghi{'jkl'}" + +c = f"def" +d = f"def" + "ghi" +e = ( + f"def" + + "ghi" +) diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index fb95d0686b..9ccbf2bfb1 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -1,6 +1,10 @@ [tool.ruff] line-length = 88 -extend-exclude = ["excluded.py", "migrations"] +extend-exclude = [ + "excluded.py", + "migrations", + "./resources/test/fixtures/directory/also_excluded.py", +] select = [ "E402", "E501", diff --git a/src/fs.rs b/src/fs.rs index b4d39185c9..fad56c3af4 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,3 +1,4 @@ +use std::env; use std::fs::File; use std::io::{BufReader, Read}; use std::path::{Path, PathBuf}; @@ -5,9 +6,11 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use glob::Pattern; use log::debug; +use path_absolutize::Absolutize; use walkdir::{DirEntry, WalkDir}; fn is_excluded(path: &Path, exclude: &[Pattern]) -> bool { + // Check the basename. if let Some(file_name) = path.file_name() { if let Some(file_name) = file_name.to_str() { for pattern in exclude { @@ -15,13 +18,19 @@ fn is_excluded(path: &Path, exclude: &[Pattern]) -> bool { return true; } } - false - } else { - false } - } else { - false } + + // Check the complete path. + if let Some(file_name) = path.to_str() { + for pattern in exclude { + if pattern.matches(file_name) { + return true; + } + } + } + + false } fn is_included(path: &Path) -> bool { @@ -34,7 +43,7 @@ pub fn iter_python_files<'a>( exclude: &'a [Pattern], extend_exclude: &'a [Pattern], ) -> impl Iterator + 'a { - WalkDir::new(path) + WalkDir::new(normalize_path(path)) .follow_links(true) .into_iter() .filter_entry(|entry| { @@ -60,6 +69,20 @@ pub fn iter_python_files<'a>( }) } +pub fn normalize_path(path: &PathBuf) -> PathBuf { + if path == Path::new(".") || path == Path::new("..") { + return path.clone(); + } + if let Ok(path) = path.absolutize() { + if let Ok(root) = env::current_dir() { + if let Ok(path) = path.strip_prefix(root) { + return Path::new(".").join(path); + } + } + } + path.clone() +} + pub fn read_file(path: &Path) -> Result { let file = File::open(path)?; let mut buf_reader = BufReader::new(file); @@ -105,6 +128,18 @@ mod tests { let exclude = vec![Pattern::new("baz.py").unwrap()]; assert!(is_excluded(path, &exclude)); + let path = Path::new("foo/bar"); + let exclude = vec![Pattern::new("foo/bar").unwrap()]; + assert!(is_excluded(path, &exclude)); + + let path = Path::new("foo/bar/baz.py"); + let exclude = vec![Pattern::new("foo/bar/baz.py").unwrap()]; + assert!(is_excluded(path, &exclude)); + + let path = Path::new("foo/bar/baz.py"); + let exclude = vec![Pattern::new("foo/bar/*.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/pyproject.rs b/src/pyproject.rs index 7297ba30e1..06fd00e4da 100644 --- a/src/pyproject.rs +++ b/src/pyproject.rs @@ -261,7 +261,8 @@ other-attribute = 1 exclude: None, extend_exclude: Some(vec![ Path::new("excluded.py").to_path_buf(), - Path::new("migrations").to_path_buf() + Path::new("migrations").to_path_buf(), + Path::new("./resources/test/fixtures/directory/also_excluded.py").to_path_buf() ]), select: Some(vec![ CheckCode::E402, diff --git a/src/settings.rs b/src/settings.rs index bd6489c4e0..1e482f7f59 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -24,6 +24,7 @@ impl Hash for Settings { } } } + static DEFAULT_EXCLUDE: Lazy> = Lazy::new(|| { vec![ Pattern::new(".bzr").unwrap(), @@ -57,7 +58,7 @@ impl Settings { .exclude .map(|paths| { paths - .into_iter() + .iter() .map(|path| { Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.") }) @@ -68,7 +69,7 @@ impl Settings { .extend_exclude .map(|paths| { paths - .into_iter() + .iter() .map(|path| { Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.") }) diff --git a/test/F823.py b/test/F823.py new file mode 100644 index 0000000000..38894ecd76 --- /dev/null +++ b/test/F823.py @@ -0,0 +1,27 @@ +my_dict = {} +my_var = 0 + + +def foo(): + my_var += 1 + + +def bar(): + global my_var + my_var += 1 + + +def baz(): + global my_var + global my_dict + my_dict[my_var] += 1 + + +def dec(x): + return x + + +@dec +def f(): + dec = 1 + return dec diff --git a/test/directory/F706.py b/test/directory/F706.py new file mode 100644 index 0000000000..f6ff0dbce4 --- /dev/null +++ b/test/directory/F706.py @@ -0,0 +1,9 @@ +def f() -> int: + return 1 + + +class Foo: + return 2 + + +return 3 diff --git a/test/directory/F707.py b/test/directory/F707.py new file mode 100644 index 0000000000..f6ff0dbce4 --- /dev/null +++ b/test/directory/F707.py @@ -0,0 +1,9 @@ +def f() -> int: + return 1 + + +class Foo: + return 2 + + +return 3 diff --git a/test/ignore.py b/test/ignore.py new file mode 100644 index 0000000000..f6ff0dbce4 --- /dev/null +++ b/test/ignore.py @@ -0,0 +1,9 @@ +def f() -> int: + return 1 + + +class Foo: + return 2 + + +return 3 diff --git a/test/pyproject.toml b/test/pyproject.toml new file mode 100644 index 0000000000..80a9af3cf0 --- /dev/null +++ b/test/pyproject.toml @@ -0,0 +1,3 @@ +[tool.ruff] +line-length = 88 +extend-exclude = ["./directory/*"]