Skip to content

Commit 901b7dd

Browse files
[flake8-use-pathlib] Catch redundant joins in PTH201 and avoid syntax errors (#15177)
## Summary Resolves #10453, resolves #15165. ## Test Plan `cargo nextest run` and `cargo insta test`.
1 parent d349217 commit 901b7dd

File tree

4 files changed

+440
-69
lines changed

4 files changed

+440
-69
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,70 @@
11
from pathlib import Path, PurePath
22
from pathlib import Path as pth
33

4+
45
# match
56
_ = Path(".")
67
_ = pth(".")
78
_ = PurePath(".")
89
_ = Path("")
910

11+
Path('', )
12+
13+
Path(
14+
'',
15+
)
16+
17+
Path( # Comment before argument
18+
'',
19+
)
20+
21+
Path(
22+
'', # EOL comment
23+
)
24+
25+
Path(
26+
'' # Comment in the middle of implicitly concatenated string
27+
".",
28+
)
29+
30+
Path(
31+
'' # Comment before comma
32+
,
33+
)
34+
35+
Path(
36+
'',
37+
) / "bare"
38+
39+
Path( # Comment before argument
40+
'',
41+
) / ("parenthesized")
42+
43+
Path(
44+
'', # EOL comment
45+
) / ( ("double parenthesized" ) )
46+
47+
( Path(
48+
'' # Comment in the middle of implicitly concatenated string
49+
".",
50+
) )/ (("parenthesized path call")
51+
# Comment between closing parentheses
52+
)
53+
54+
Path(
55+
'' # Comment before comma
56+
,
57+
) / "multiple" / (
58+
"frag" # Comment
59+
'ment'
60+
)
61+
62+
1063
# no match
1164
_ = Path()
1265
print(".")
1366
Path("file.txt")
1467
Path(".", "folder")
1568
PurePath(".", "folder")
69+
70+
Path()

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
983983
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
984984
}
985985
if checker.enabled(Rule::PathConstructorCurrentDirectory) {
986-
flake8_use_pathlib::rules::path_constructor_current_directory(checker, expr, func);
986+
flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
987987
}
988988
if checker.enabled(Rule::OsSepSplit) {
989989
flake8_use_pathlib::rules::os_sep_split(checker, call);

crates/ruff_linter/src/rules/flake8_use_pathlib/rules/path_constructor_current_directory.rs

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
1+
use std::ops::Range;
2+
3+
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
24
use ruff_macros::{derive_message_formats, ViolationMetadata};
3-
use ruff_python_ast::{self as ast, Expr, ExprCall};
5+
use ruff_python_ast::parenthesize::parenthesized_range;
6+
use ruff_python_ast::{AstNode, Expr, ExprBinOp, ExprCall, Operator};
7+
use ruff_python_semantic::SemanticModel;
8+
use ruff_python_trivia::CommentRanges;
9+
use ruff_text_size::{Ranged, TextRange};
410

511
use crate::checkers::ast::Checker;
12+
use crate::fix::edits::{remove_argument, Parentheses};
613

714
/// ## What it does
815
/// Checks for `pathlib.Path` objects that are initialized with the current
@@ -43,7 +50,17 @@ impl AlwaysFixableViolation for PathConstructorCurrentDirectory {
4350
}
4451

4552
/// PTH201
46-
pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &Expr, func: &Expr) {
53+
pub(crate) fn path_constructor_current_directory(checker: &mut Checker, call: &ExprCall) {
54+
let applicability = |range| {
55+
if checker.comment_ranges().intersects(range) {
56+
Applicability::Unsafe
57+
} else {
58+
Applicability::Safe
59+
}
60+
};
61+
62+
let (func, arguments) = (&call.func, &call.arguments);
63+
4764
if !checker
4865
.semantic()
4966
.resolve_qualified_name(func)
@@ -54,21 +71,75 @@ pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &E
5471
return;
5572
}
5673

57-
let Expr::Call(ExprCall { arguments, .. }) = expr else {
74+
if !arguments.keywords.is_empty() {
75+
return;
76+
}
77+
78+
let [Expr::StringLiteral(arg)] = &*arguments.args else {
5879
return;
5980
};
6081

61-
if !arguments.keywords.is_empty() {
82+
if !matches!(arg.value.to_str(), "" | ".") {
6283
return;
6384
}
6485

65-
let [Expr::StringLiteral(ast::ExprStringLiteral { value, range })] = &*arguments.args else {
66-
return;
86+
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, arg.range());
87+
88+
match parent_and_next_path_fragment_range(
89+
checker.semantic(),
90+
checker.comment_ranges(),
91+
checker.source(),
92+
) {
93+
Some((parent_range, next_fragment_range)) => {
94+
let next_fragment_expr = checker.locator().slice(next_fragment_range);
95+
let call_expr = checker.locator().slice(call.range());
96+
97+
let relative_argument_range: Range<usize> = {
98+
let range = arg.range() - call.start();
99+
range.start().into()..range.end().into()
100+
};
101+
102+
let mut new_call_expr = call_expr.to_string();
103+
new_call_expr.replace_range(relative_argument_range, next_fragment_expr);
104+
105+
let edit = Edit::range_replacement(new_call_expr, parent_range);
106+
107+
diagnostic.set_fix(Fix::applicable_edit(edit, applicability(parent_range)));
108+
}
109+
None => diagnostic.try_set_fix(|| {
110+
let edit = remove_argument(arg, arguments, Parentheses::Preserve, checker.source())?;
111+
Ok(Fix::applicable_edit(edit, applicability(call.range())))
112+
}),
67113
};
68114

69-
if matches!(value.to_str(), "" | ".") {
70-
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, *range);
71-
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(*range)));
72-
checker.diagnostics.push(diagnostic);
115+
checker.diagnostics.push(diagnostic);
116+
}
117+
118+
fn parent_and_next_path_fragment_range(
119+
semantic: &SemanticModel,
120+
comment_ranges: &CommentRanges,
121+
source: &str,
122+
) -> Option<(TextRange, TextRange)> {
123+
let parent = semantic.current_expression_parent()?;
124+
125+
let Expr::BinOp(parent @ ExprBinOp { op, right, .. }) = parent else {
126+
return None;
127+
};
128+
129+
let range = right.range();
130+
131+
if !matches!(op, Operator::Div) {
132+
return None;
73133
}
134+
135+
Some((
136+
parent.range(),
137+
parenthesized_range(
138+
right.into(),
139+
parent.as_any_node_ref(),
140+
comment_ranges,
141+
source,
142+
)
143+
.unwrap_or(range),
144+
))
74145
}

0 commit comments

Comments
 (0)