[flake8-use-pathlib] Catch redundant joins in PTH201 and avoid syntax errors (#15177)
Some checks are pending
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions

## Summary

Resolves #10453, resolves #15165.

## Test Plan

`cargo nextest run` and `cargo insta test`.
This commit is contained in:
InSync 2024-12-30 10:31:35 +07:00 committed by GitHub
parent d3492178e1
commit 901b7dd8f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 437 additions and 66 deletions

View file

@ -1,15 +1,70 @@
from pathlib import Path, PurePath from pathlib import Path, PurePath
from pathlib import Path as pth from pathlib import Path as pth
# match # match
_ = Path(".") _ = Path(".")
_ = pth(".") _ = pth(".")
_ = PurePath(".") _ = PurePath(".")
_ = Path("") _ = Path("")
Path('', )
Path(
'',
)
Path( # Comment before argument
'',
)
Path(
'', # EOL comment
)
Path(
'' # Comment in the middle of implicitly concatenated string
".",
)
Path(
'' # Comment before comma
,
)
Path(
'',
) / "bare"
Path( # Comment before argument
'',
) / ("parenthesized")
Path(
'', # EOL comment
) / ( ("double parenthesized" ) )
( Path(
'' # Comment in the middle of implicitly concatenated string
".",
) )/ (("parenthesized path call")
# Comment between closing parentheses
)
Path(
'' # Comment before comma
,
) / "multiple" / (
"frag" # Comment
'ment'
)
# no match # no match
_ = Path() _ = Path()
print(".") print(".")
Path("file.txt") Path("file.txt")
Path(".", "folder") Path(".", "folder")
PurePath(".", "folder") PurePath(".", "folder")
Path()

View file

@ -983,7 +983,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call); flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
} }
if checker.enabled(Rule::PathConstructorCurrentDirectory) { if checker.enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, expr, func); flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
} }
if checker.enabled(Rule::OsSepSplit) { if checker.enabled(Rule::OsSepSplit) {
flake8_use_pathlib::rules::os_sep_split(checker, call); flake8_use_pathlib::rules::os_sep_split(checker, call);

View file

@ -1,8 +1,15 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix}; use std::ops::Range;
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{self as ast, Expr, ExprCall}; use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{AstNode, Expr, ExprBinOp, ExprCall, Operator};
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::fix::edits::{remove_argument, Parentheses};
/// ## What it does /// ## What it does
/// Checks for `pathlib.Path` objects that are initialized with the current /// Checks for `pathlib.Path` objects that are initialized with the current
@ -43,7 +50,17 @@ impl AlwaysFixableViolation for PathConstructorCurrentDirectory {
} }
/// PTH201 /// PTH201
pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &Expr, func: &Expr) { pub(crate) fn path_constructor_current_directory(checker: &mut Checker, call: &ExprCall) {
let applicability = |range| {
if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
}
};
let (func, arguments) = (&call.func, &call.arguments);
if !checker if !checker
.semantic() .semantic()
.resolve_qualified_name(func) .resolve_qualified_name(func)
@ -54,21 +71,75 @@ pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &E
return; return;
} }
let Expr::Call(ExprCall { arguments, .. }) = expr else {
return;
};
if !arguments.keywords.is_empty() { if !arguments.keywords.is_empty() {
return; return;
} }
let [Expr::StringLiteral(ast::ExprStringLiteral { value, range })] = &*arguments.args else { let [Expr::StringLiteral(arg)] = &*arguments.args else {
return; return;
}; };
if matches!(value.to_str(), "" | ".") { if !matches!(arg.value.to_str(), "" | ".") {
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, *range); return;
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(*range)));
checker.diagnostics.push(diagnostic);
} }
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, arg.range());
match parent_and_next_path_fragment_range(
checker.semantic(),
checker.comment_ranges(),
checker.source(),
) {
Some((parent_range, next_fragment_range)) => {
let next_fragment_expr = checker.locator().slice(next_fragment_range);
let call_expr = checker.locator().slice(call.range());
let relative_argument_range: Range<usize> = {
let range = arg.range() - call.start();
range.start().into()..range.end().into()
};
let mut new_call_expr = call_expr.to_string();
new_call_expr.replace_range(relative_argument_range, next_fragment_expr);
let edit = Edit::range_replacement(new_call_expr, parent_range);
diagnostic.set_fix(Fix::applicable_edit(edit, applicability(parent_range)));
}
None => diagnostic.try_set_fix(|| {
let edit = remove_argument(arg, arguments, Parentheses::Preserve, checker.source())?;
Ok(Fix::applicable_edit(edit, applicability(call.range())))
}),
};
checker.diagnostics.push(diagnostic);
}
fn parent_and_next_path_fragment_range(
semantic: &SemanticModel,
comment_ranges: &CommentRanges,
source: &str,
) -> Option<(TextRange, TextRange)> {
let parent = semantic.current_expression_parent()?;
let Expr::BinOp(parent @ ExprBinOp { op, right, .. }) = parent else {
return None;
};
let range = right.range();
if !matches!(op, Operator::Div) {
return None;
}
Some((
parent.range(),
parenthesized_range(
right.into(),
parent.as_any_node_ref(),
comment_ranges,
source,
)
.unwrap_or(range),
))
} }

View file

@ -2,84 +2,329 @@
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
snapshot_kind: text snapshot_kind: text
--- ---
PTH201.py:5:10: PTH201 [*] Do not pass the current directory explicitly to `Path` PTH201.py:6:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
| |
4 | # match 5 | # match
5 | _ = Path(".") 6 | _ = Path(".")
| ^^^ PTH201 | ^^^ PTH201
6 | _ = pth(".") 7 | _ = pth(".")
7 | _ = PurePath(".") 8 | _ = PurePath(".")
| |
= help: Remove the current directory argument = help: Remove the current directory argument
Safe fix Safe fix
2 2 | from pathlib import Path as pth
3 3 | 3 3 |
4 4 | # match 4 4 |
5 |-_ = Path(".") 5 5 | # match
5 |+_ = Path() 6 |-_ = Path(".")
6 6 | _ = pth(".") 6 |+_ = Path()
7 7 | _ = PurePath(".") 7 7 | _ = pth(".")
8 8 | _ = Path("") 8 8 | _ = PurePath(".")
9 9 | _ = Path("")
PTH201.py:6:9: PTH201 [*] Do not pass the current directory explicitly to `Path` PTH201.py:7:9: PTH201 [*] Do not pass the current directory explicitly to `Path`
| |
4 | # match 5 | # match
5 | _ = Path(".") 6 | _ = Path(".")
6 | _ = pth(".") 7 | _ = pth(".")
| ^^^ PTH201 | ^^^ PTH201
7 | _ = PurePath(".") 8 | _ = PurePath(".")
8 | _ = Path("") 9 | _ = Path("")
| |
= help: Remove the current directory argument = help: Remove the current directory argument
Safe fix Safe fix
3 3 | 4 4 |
4 4 | # match 5 5 | # match
5 5 | _ = Path(".") 6 6 | _ = Path(".")
6 |-_ = pth(".") 7 |-_ = pth(".")
6 |+_ = pth() 7 |+_ = pth()
7 7 | _ = PurePath(".") 8 8 | _ = PurePath(".")
8 8 | _ = Path("") 9 9 | _ = Path("")
9 9 | 10 10 |
PTH201.py:7:14: PTH201 [*] Do not pass the current directory explicitly to `Path` PTH201.py:8:14: PTH201 [*] Do not pass the current directory explicitly to `Path`
| |
5 | _ = Path(".") 6 | _ = Path(".")
6 | _ = pth(".") 7 | _ = pth(".")
7 | _ = PurePath(".") 8 | _ = PurePath(".")
| ^^^ PTH201 | ^^^ PTH201
8 | _ = Path("") 9 | _ = Path("")
| |
= help: Remove the current directory argument = help: Remove the current directory argument
Safe fix Safe fix
4 4 | # match 5 5 | # match
5 5 | _ = Path(".") 6 6 | _ = Path(".")
6 6 | _ = pth(".") 7 7 | _ = pth(".")
7 |-_ = PurePath(".") 8 |-_ = PurePath(".")
7 |+_ = PurePath() 8 |+_ = PurePath()
8 8 | _ = Path("") 9 9 | _ = Path("")
9 9 | 10 10 |
10 10 | # no match 11 11 | Path('', )
PTH201.py:8:10: PTH201 [*] Do not pass the current directory explicitly to `Path` PTH201.py:9:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
| |
6 | _ = pth(".") 7 | _ = pth(".")
7 | _ = PurePath(".") 8 | _ = PurePath(".")
8 | _ = Path("") 9 | _ = Path("")
| ^^ PTH201 | ^^ PTH201
9 | 10 |
10 | # no match 11 | Path('', )
| |
= help: Remove the current directory argument = help: Remove the current directory argument
Safe fix Safe fix
5 5 | _ = Path(".") 6 6 | _ = Path(".")
6 6 | _ = pth(".") 7 7 | _ = pth(".")
7 7 | _ = PurePath(".") 8 8 | _ = PurePath(".")
8 |-_ = Path("") 9 |-_ = Path("")
8 |+_ = Path() 9 |+_ = Path()
9 9 | 10 10 |
10 10 | # no match 11 11 | Path('', )
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 | )