diff --git a/crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH201.py b/crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH201.py new file mode 100644 index 00000000000000..78ebdf261f28d1 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_use_pathlib/PTH201.py @@ -0,0 +1,14 @@ +from pathlib import Path, PurePath +from pathlib import Path as pth + +# match +_ = Path(".") +_ = pth(".") +_ = PurePath(".") + +# no match +_ = Path() +print(".") +Path("file.txt") +Path(".", "folder") +PurePath(".", "folder") diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 35e0fed7dcafd4..1a855094df6d31 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -3027,6 +3027,9 @@ where ]) { flake8_use_pathlib::rules::replaceable_by_pathlib(self, func); } + if self.enabled(Rule::PathConstructorCurrentDirectory) { + flake8_use_pathlib::rules::path_constructor_current_directory(self, expr, func); + } if self.enabled(Rule::NumpyLegacyRandom) { numpy::rules::legacy_random(self, func); } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 101a2ea94389de..ec4476476230cf 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -748,6 +748,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8UsePathlib, "122") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::OsPathSplitext), (Flake8UsePathlib, "123") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::BuiltinOpen), (Flake8UsePathlib, "124") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::PyPath), + (Flake8UsePathlib, "201") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::PathConstructorCurrentDirectory), // flake8-logging-format (Flake8LoggingFormat, "001") => (RuleGroup::Unspecified, rules::flake8_logging_format::violations::LoggingStringFormat), diff --git a/crates/ruff/src/rules/flake8_use_pathlib/mod.rs b/crates/ruff/src/rules/flake8_use_pathlib/mod.rs index 6e72e583edffeb..590c94dee0b63f 100644 --- a/crates/ruff/src/rules/flake8_use_pathlib/mod.rs +++ b/crates/ruff/src/rules/flake8_use_pathlib/mod.rs @@ -56,6 +56,8 @@ mod tests { #[test_case(Rule::PyPath, Path::new("py_path_1.py"))] #[test_case(Rule::PyPath, Path::new("py_path_2.py"))] + #[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))] + fn rules_pypath(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs b/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs index dcf45f615c5e1c..d16eb63c72180d 100644 --- a/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_use_pathlib/rules/mod.rs @@ -1,3 +1,5 @@ +pub(crate) use path_constructor_current_directory::*; pub(crate) use replaceable_by_pathlib::*; +mod path_constructor_current_directory; mod replaceable_by_pathlib; diff --git a/crates/ruff/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs b/crates/ruff/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs new file mode 100644 index 00000000000000..a1d9c1104957cd --- /dev/null +++ b/crates/ruff/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs @@ -0,0 +1,85 @@ +use rustpython_parser::ast::{Constant, Expr, ExprCall, ExprConstant}; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for `pathlib.Path` objects that are initialized with the current +/// directory. +/// +/// ## Why is this bad? +/// The `Path()` constructor defaults to the current directory, so passing it +/// in explicitly (as `"."`) is unnecessary. +/// +/// ## Example +/// ```python +/// from pathlib import Path +/// +/// _ = Path(".") +/// ``` +/// +/// Use instead: +/// ```python +/// from pathlib import Path +/// +/// _ = Path() +/// ``` +/// +/// ## References +/// - [Python documentation: `Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) +#[violation] +pub struct PathConstructorCurrentDirectory; + +impl AlwaysAutofixableViolation for PathConstructorCurrentDirectory { + #[derive_message_formats] + fn message(&self) -> String { + format!("Do not pass the current directory explicitly to `Path`") + } + + fn autofix_title(&self) -> String { + "Remove the current directory argument".to_string() + } +} + +/// PTH201 +pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &Expr, func: &Expr) { + if !checker + .semantic() + .resolve_call_path(func) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["pathlib", "Path" | "PurePath"]) + }) + { + return; + } + + let Expr::Call(ExprCall { args, keywords, .. }) = expr else { + return; + }; + + if !keywords.is_empty() { + return; + } + + let [Expr::Constant(ExprConstant { + value: Constant::Str(value), + kind: _, + range + })] = args.as_slice() + else { + return; + }; + + if value != "." { + return; + } + + let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, *range); + if checker.patch(diagnostic.kind.rule()) { + diagnostic.set_fix(Fix::automatic(Edit::range_deletion(*range))); + } + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff/src/rules/flake8_use_pathlib/snapshots/ruff__rules__flake8_use_pathlib__tests__PTH201_PTH201.py.snap b/crates/ruff/src/rules/flake8_use_pathlib/snapshots/ruff__rules__flake8_use_pathlib__tests__PTH201_PTH201.py.snap new file mode 100644 index 00000000000000..2e215356fd8824 --- /dev/null +++ b/crates/ruff/src/rules/flake8_use_pathlib/snapshots/ruff__rules__flake8_use_pathlib__tests__PTH201_PTH201.py.snap @@ -0,0 +1,65 @@ +--- +source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs +--- +PTH201.py:5:10: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +4 | # match +5 | _ = Path(".") + | ^^^ PTH201 +6 | _ = pth(".") +7 | _ = PurePath(".") + | + = help: Remove the current directory argument + +ℹ Fix +2 2 | from pathlib import Path as pth +3 3 | +4 4 | # match +5 |-_ = Path(".") + 5 |+_ = Path() +6 6 | _ = pth(".") +7 7 | _ = PurePath(".") +8 8 | + +PTH201.py:6:9: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +4 | # match +5 | _ = Path(".") +6 | _ = pth(".") + | ^^^ PTH201 +7 | _ = PurePath(".") + | + = help: Remove the current directory argument + +ℹ Fix +3 3 | +4 4 | # match +5 5 | _ = Path(".") +6 |-_ = pth(".") + 6 |+_ = pth() +7 7 | _ = PurePath(".") +8 8 | +9 9 | # no match + +PTH201.py:7:14: PTH201 [*] Do not pass the current directory explicitly to `Path` + | +5 | _ = Path(".") +6 | _ = pth(".") +7 | _ = PurePath(".") + | ^^^ PTH201 +8 | +9 | # no match + | + = help: Remove the current directory argument + +ℹ Fix +4 4 | # match +5 5 | _ = Path(".") +6 6 | _ = pth(".") +7 |-_ = PurePath(".") + 7 |+_ = PurePath() +8 8 | +9 9 | # no match +10 10 | _ = Path() + + diff --git a/ruff.schema.json b/ruff.schema.json index d64486a8724f1c..031265b57bd062 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2326,6 +2326,9 @@ "PTH122", "PTH123", "PTH124", + "PTH2", + "PTH20", + "PTH201", "PYI", "PYI0", "PYI00",