diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH201.py b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH201.py index 852e060f70..03f8fb78c6 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH201.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_use_pathlib/PTH201.py @@ -1,4 +1,4 @@ -from pathlib import Path, PurePath +from pathlib import Path, PurePath, PosixPath, PurePosixPath, WindowsPath, PureWindowsPath from pathlib import Path as pth @@ -68,3 +68,11 @@ Path(".", "folder") PurePath(".", "folder") Path() + +from importlib.metadata import PackagePath + +_ = PosixPath(".") +_ = PurePosixPath(".") +_ = WindowsPath(".") +_ = PureWindowsPath(".") +_ = PackagePath(".") diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs index 82f7492beb..b71a635493 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs @@ -20,6 +20,35 @@ pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool { }) } +/// Check if the given segments represent a pathlib Path subclass or `PackagePath` with preview mode support. +/// In stable mode, only checks for `Path` and `PurePath`. In preview mode, also checks for +/// `PosixPath`, `PurePosixPath`, `WindowsPath`, `PureWindowsPath`, and `PackagePath`. +pub(crate) fn is_pure_path_subclass_with_preview( + checker: &crate::checkers::ast::Checker, + segments: &[&str], +) -> bool { + let is_core_pathlib = matches!(segments, ["pathlib", "Path" | "PurePath"]); + + if is_core_pathlib { + return true; + } + + if checker.settings().preview.is_enabled() { + let is_expanded_pathlib = matches!( + segments, + [ + "pathlib", + "PosixPath" | "PurePosixPath" | "WindowsPath" | "PureWindowsPath" + ] + ); + let is_packagepath = matches!(segments, ["importlib", "metadata", "PackagePath"]); + + return is_expanded_pathlib || is_packagepath; + } + + false +} + /// We check functions that take only 1 argument, this does not apply to functions /// with `dir_fd` argument, because `dir_fd` is not supported by pathlib, /// so check if it's set to non-default values diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs index e682cf8ef6..6042c440e9 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs @@ -123,6 +123,7 @@ mod tests { Ok(()) } + #[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))] #[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))] #[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))] diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs index 5b448ebe96..c909c312ac 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs @@ -9,6 +9,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::edits::{Parentheses, remove_argument}; +use crate::rules::flake8_use_pathlib::helpers::is_pure_path_subclass_with_preview; use crate::{AlwaysFixableViolation, Applicability, Edit, Fix}; /// ## What it does @@ -69,7 +70,7 @@ pub(crate) fn path_constructor_current_directory( let arguments = &call.arguments; - if !matches!(segments, ["pathlib", "Path" | "PurePath"]) { + if !is_pure_path_subclass_with_preview(checker, segments) { return; } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH201_PTH201.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH201_PTH201.py.snap new file mode 100644 index 0000000000..cd95f5f7d3 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__preview__PTH201_PTH201.py.snap @@ -0,0 +1,423 @@ +--- +source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs +assertion_line: 144 +--- +PTH201.py:6:10: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +5 | # match +6 | _ = Path(".") + | ^^^ PTH201 +7 | _ = pth(".") +8 | _ = PurePath(".") + | + = help: Remove the current directory argument + +ℹ Safe fix +3 3 | +4 4 | +5 5 | # match +6 |-_ = Path(".") + 6 |+_ = Path() +7 7 | _ = pth(".") +8 8 | _ = PurePath(".") +9 9 | _ = Path("") + +PTH201.py:7:9: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +5 | # match +6 | _ = Path(".") +7 | _ = pth(".") + | ^^^ PTH201 +8 | _ = PurePath(".") +9 | _ = Path("") + | + = help: Remove the current directory argument + +ℹ Safe fix +4 4 | +5 5 | # match +6 6 | _ = Path(".") +7 |-_ = pth(".") + 7 |+_ = pth() +8 8 | _ = PurePath(".") +9 9 | _ = Path("") +10 10 | + +PTH201.py:8:14: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +6 | _ = Path(".") +7 | _ = pth(".") +8 | _ = PurePath(".") + | ^^^ PTH201 +9 | _ = Path("") + | + = help: Remove the current directory argument + +ℹ Safe fix +5 5 | # match +6 6 | _ = Path(".") +7 7 | _ = pth(".") +8 |-_ = PurePath(".") + 8 |+_ = PurePath() +9 9 | _ = Path("") +10 10 | +11 11 | Path('', ) + +PTH201.py:9:10: PTH201 [*] Do not pass the current directory explicitly to `Path` + | + 7 | _ = pth(".") + 8 | _ = PurePath(".") + 9 | _ = Path("") + | ^^ PTH201 +10 | +11 | Path('', ) + | + = help: Remove the current directory argument + +ℹ Safe fix +6 6 | _ = Path(".") +7 7 | _ = pth(".") +8 8 | _ = PurePath(".") +9 |-_ = Path("") + 9 |+_ = Path() +10 10 | +11 11 | Path('', ) +12 12 | + +PTH201.py:11:6: PTH201 [*] Do not pass the current directory explicitly to `Path` + | + 9 | _ = Path("") +10 | +11 | Path('', ) + | ^^ PTH201 +12 | +13 | Path( + | + = help: Remove the current directory argument + +ℹ Safe fix +8 8 | _ = PurePath(".") +9 9 | _ = Path("") +10 10 | +11 |-Path('', ) + 11 |+Path() +12 12 | +13 13 | Path( +14 14 | '', + +PTH201.py:14:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +13 | Path( +14 | '', + | ^^ PTH201 +15 | ) + | + = help: Remove the current directory argument + +ℹ Safe fix +10 10 | +11 11 | Path('', ) +12 12 | +13 |-Path( +14 |- '', +15 |-) + 13 |+Path() +16 14 | +17 15 | Path( # Comment before argument +18 16 | '', + +PTH201.py:18:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +17 | Path( # Comment before argument +18 | '', + | ^^ PTH201 +19 | ) + | + = help: Remove the current directory argument + +ℹ Unsafe fix +14 14 | '', +15 15 | ) +16 16 | +17 |-Path( # Comment before argument +18 |- '', +19 |-) + 17 |+Path() +20 18 | +21 19 | Path( +22 20 | '', # EOL comment + +PTH201.py:22:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +21 | Path( +22 | '', # EOL comment + | ^^ PTH201 +23 | ) + | + = help: Remove the current directory argument + +ℹ Unsafe fix +18 18 | '', +19 19 | ) +20 20 | +21 |-Path( +22 |- '', # EOL comment +23 |-) + 21 |+Path() +24 22 | +25 23 | Path( +26 24 | '' # Comment in the middle of implicitly concatenated string + +PTH201.py:26:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +25 | Path( +26 | / '' # Comment in the middle of implicitly concatenated string +27 | | ".", + | |_______^ PTH201 +28 | ) + | + = help: Remove the current directory argument + +ℹ Unsafe fix +22 22 | '', # EOL comment +23 23 | ) +24 24 | +25 |-Path( +26 |- '' # Comment in the middle of implicitly concatenated string +27 |- ".", +28 |-) + 25 |+Path() +29 26 | +30 27 | Path( +31 28 | '' # Comment before comma + +PTH201.py:31:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +30 | Path( +31 | '' # Comment before comma + | ^^ PTH201 +32 | , +33 | ) + | + = help: Remove the current directory argument + +ℹ Unsafe fix +27 27 | ".", +28 28 | ) +29 29 | +30 |-Path( +31 |- '' # Comment before comma +32 |- , +33 |-) + 30 |+Path() +34 31 | +35 32 | Path( +36 33 | '', + +PTH201.py:36:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +35 | Path( +36 | '', + | ^^ PTH201 +37 | ) / "bare" + | + = help: Remove the current directory argument + +ℹ Safe fix +33 33 | ) +34 34 | +35 35 | Path( +36 |- '', +37 |-) / "bare" + 36 |+ "bare", + 37 |+) +38 38 | +39 39 | Path( # Comment before argument +40 40 | '', + +PTH201.py:40:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +39 | Path( # Comment before argument +40 | '', + | ^^ PTH201 +41 | ) / ("parenthesized") + | + = help: Remove the current directory argument + +ℹ Unsafe fix +37 37 | ) / "bare" +38 38 | +39 39 | Path( # Comment before argument +40 |- '', +41 |-) / ("parenthesized") + 40 |+ ("parenthesized"), + 41 |+) +42 42 | +43 43 | Path( +44 44 | '', # EOL comment + +PTH201.py:44:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +43 | Path( +44 | '', # EOL comment + | ^^ PTH201 +45 | ) / ( ("double parenthesized" ) ) + | + = help: Remove the current directory argument + +ℹ Unsafe fix +41 41 | ) / ("parenthesized") +42 42 | +43 43 | Path( +44 |- '', # EOL comment +45 |-) / ( ("double parenthesized" ) ) + 44 |+ ( ("double parenthesized" ) ), # EOL comment + 45 |+) +46 46 | +47 47 | ( Path( +48 48 | '' # Comment in the middle of implicitly concatenated string + +PTH201.py:48:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +47 | ( Path( +48 | / '' # Comment in the middle of implicitly concatenated string +49 | | ".", + | |_______^ PTH201 +50 | ) )/ (("parenthesized path call") +51 | # Comment between closing parentheses + | + = help: Remove the current directory argument + +ℹ Unsafe fix +44 44 | '', # EOL comment +45 45 | ) / ( ("double parenthesized" ) ) +46 46 | +47 |-( Path( +48 |- '' # Comment in the middle of implicitly concatenated string +49 |- ".", +50 |-) )/ (("parenthesized path call") + 47 |+Path( + 48 |+ (("parenthesized path call") +51 49 | # Comment between closing parentheses + 50 |+), +52 51 | ) +53 52 | +54 53 | Path( + +PTH201.py:55:5: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +54 | Path( +55 | '' # Comment before comma + | ^^ PTH201 +56 | , +57 | ) / "multiple" / ( + | + = help: Remove the current directory argument + +ℹ Unsafe fix +52 52 | ) +53 53 | +54 54 | Path( +55 |- '' # Comment before comma + 55 |+ "multiple" # Comment before comma +56 56 | , +57 |-) / "multiple" / ( + 57 |+) / ( +58 58 | "frag" # Comment +59 59 | 'ment' +60 60 | ) + +PTH201.py:74:15: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +72 | from importlib.metadata import PackagePath +73 | +74 | _ = PosixPath(".") + | ^^^ PTH201 +75 | _ = PurePosixPath(".") +76 | _ = WindowsPath(".") + | + = help: Remove the current directory argument + +ℹ Safe fix +71 71 | +72 72 | from importlib.metadata import PackagePath +73 73 | +74 |-_ = PosixPath(".") + 74 |+_ = PosixPath() +75 75 | _ = PurePosixPath(".") +76 76 | _ = WindowsPath(".") +77 77 | _ = PureWindowsPath(".") + +PTH201.py:75:19: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +74 | _ = PosixPath(".") +75 | _ = PurePosixPath(".") + | ^^^ PTH201 +76 | _ = WindowsPath(".") +77 | _ = PureWindowsPath(".") + | + = help: Remove the current directory argument + +ℹ Safe fix +72 72 | from importlib.metadata import PackagePath +73 73 | +74 74 | _ = PosixPath(".") +75 |-_ = PurePosixPath(".") + 75 |+_ = PurePosixPath() +76 76 | _ = WindowsPath(".") +77 77 | _ = PureWindowsPath(".") +78 78 | _ = PackagePath(".") + +PTH201.py:76:17: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +74 | _ = PosixPath(".") +75 | _ = PurePosixPath(".") +76 | _ = WindowsPath(".") + | ^^^ PTH201 +77 | _ = PureWindowsPath(".") +78 | _ = PackagePath(".") + | + = help: Remove the current directory argument + +ℹ Safe fix +73 73 | +74 74 | _ = PosixPath(".") +75 75 | _ = PurePosixPath(".") +76 |-_ = WindowsPath(".") + 76 |+_ = WindowsPath() +77 77 | _ = PureWindowsPath(".") +78 78 | _ = PackagePath(".") + +PTH201.py:77:21: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +75 | _ = PurePosixPath(".") +76 | _ = WindowsPath(".") +77 | _ = PureWindowsPath(".") + | ^^^ PTH201 +78 | _ = PackagePath(".") + | + = help: Remove the current directory argument + +ℹ Safe fix +74 74 | _ = PosixPath(".") +75 75 | _ = PurePosixPath(".") +76 76 | _ = WindowsPath(".") +77 |-_ = PureWindowsPath(".") + 77 |+_ = PureWindowsPath() +78 78 | _ = PackagePath(".") + +PTH201.py:78:17: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +76 | _ = WindowsPath(".") +77 | _ = PureWindowsPath(".") +78 | _ = PackagePath(".") + | ^^^ PTH201 + | + = help: Remove the current directory argument + +ℹ Safe fix +75 75 | _ = PurePosixPath(".") +76 76 | _ = WindowsPath(".") +77 77 | _ = PureWindowsPath(".") +78 |-_ = PackagePath(".") + 78 |+_ = PackagePath()