Skip to content

Commit b2501b4

Browse files
danparizherntBre
andauthored
[pylint] Detect indirect pathlib.Path usages for unspecified-encoding (PLW1514) (#19304)
## Summary Fixes #19294 --------- Co-authored-by: Brent Westbrook <[email protected]>
1 parent 291699b commit b2501b4

File tree

3 files changed

+70
-10
lines changed

3 files changed

+70
-10
lines changed

crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,16 @@ def func(*args, **kwargs):
9191
Path("foo.txt").write_text(text, *args)
9292
Path("foo.txt").write_text(text, **kwargs)
9393

94-
# Violation but not detectable
94+
# https://github.com/astral-sh/ruff/issues/19294
9595
x = Path("foo.txt")
9696
x.open()
9797

9898
# https://github.com/astral-sh/ruff/issues/18107
9999
codecs.open("plw1514.py", "r", "utf-8").close() # this is fine
100+
101+
# function argument annotated as Path
102+
from pathlib import Path
103+
104+
def format_file(file: Path):
105+
with file.open() as f:
106+
contents = f.read()

crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
44
use ruff_python_ast::name::QualifiedName;
55
use ruff_python_ast::{self as ast, Expr};
66
use ruff_python_semantic::SemanticModel;
7+
use ruff_python_semantic::analyze::typing;
78
use ruff_text_size::{Ranged, TextRange};
89

910
use crate::checkers::ast::Checker;
@@ -111,20 +112,34 @@ enum Callee<'a> {
111112
}
112113

113114
impl<'a> Callee<'a> {
115+
fn is_pathlib_path_call(expr: &Expr, semantic: &SemanticModel) -> bool {
116+
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
117+
semantic
118+
.resolve_qualified_name(func)
119+
.is_some_and(|qualified_name| {
120+
matches!(qualified_name.segments(), ["pathlib", "Path"])
121+
})
122+
} else {
123+
false
124+
}
125+
}
126+
114127
fn try_from_call_expression(
115128
call: &'a ast::ExprCall,
116129
semantic: &'a SemanticModel,
117130
) -> Option<Self> {
118131
if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = call.func.as_ref() {
119-
// Check for `pathlib.Path(...).open(...)` or equivalent
120-
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
121-
if semantic
122-
.resolve_qualified_name(func)
123-
.is_some_and(|qualified_name| {
124-
matches!(qualified_name.segments(), ["pathlib", "Path"])
125-
})
126-
{
127-
return Some(Callee::Pathlib(attr));
132+
// Direct: Path(...).open()
133+
if Self::is_pathlib_path_call(value, semantic) {
134+
return Some(Callee::Pathlib(attr));
135+
}
136+
// Indirect: x.open() where x = Path(...)
137+
else if let Expr::Name(name) = value.as_ref() {
138+
if let Some(binding_id) = semantic.only_binding(name) {
139+
let binding = semantic.binding(binding_id);
140+
if typing::is_pathlib_path(binding, semantic) {
141+
return Some(Callee::Pathlib(attr));
142+
}
128143
}
129144
}
130145
}

crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1514_unspecified_encoding.py.snap

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,3 +435,41 @@ unspecified_encoding.py:80:1: PLW1514 [*] `pathlib.Path(...).write_text` without
435435
81 81 |
436436
82 82 | # Non-errors.
437437
83 83 | Path("foo.txt").open(encoding="utf-8")
438+
439+
unspecified_encoding.py:96:1: PLW1514 [*] `pathlib.Path(...).open` in text mode without explicit `encoding` argument
440+
|
441+
94 | # https://github.com/astral-sh/ruff/issues/19294
442+
95 | x = Path("foo.txt")
443+
96 | x.open()
444+
| ^^^^^^ PLW1514
445+
97 |
446+
98 | # https://github.com/astral-sh/ruff/issues/18107
447+
|
448+
= help: Add explicit `encoding` argument
449+
450+
Unsafe fix
451+
93 93 |
452+
94 94 | # https://github.com/astral-sh/ruff/issues/19294
453+
95 95 | x = Path("foo.txt")
454+
96 |-x.open()
455+
96 |+x.open(encoding="utf-8")
456+
97 97 |
457+
98 98 | # https://github.com/astral-sh/ruff/issues/18107
458+
99 99 | codecs.open("plw1514.py", "r", "utf-8").close() # this is fine
459+
460+
unspecified_encoding.py:105:10: PLW1514 [*] `pathlib.Path(...).open` in text mode without explicit `encoding` argument
461+
|
462+
104 | def format_file(file: Path):
463+
105 | with file.open() as f:
464+
| ^^^^^^^^^ PLW1514
465+
106 | contents = f.read()
466+
|
467+
= help: Add explicit `encoding` argument
468+
469+
Unsafe fix
470+
102 102 | from pathlib import Path
471+
103 103 |
472+
104 104 | def format_file(file: Path):
473+
105 |- with file.open() as f:
474+
105 |+ with file.open(encoding="utf-8") as f:
475+
106 106 | contents = f.read()

0 commit comments

Comments
 (0)