[pylint] Detect indirect pathlib.Path usages for unspecified-encoding (PLW1514) (#19304)
Some checks are pending
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 (linux, release) (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 / Fuzz for new ty panics (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 / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary

Fixes #19294

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
Dan Parizher 2025-07-16 12:57:31 -04:00 committed by GitHub
parent 291699b375
commit b2501b45e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 70 additions and 10 deletions

View file

@ -91,9 +91,16 @@ Path("foo.txt").write_text(text, encoding="utf-8")
Path("foo.txt").write_text(text, *args)
Path("foo.txt").write_text(text, **kwargs)
# Violation but not detectable
# https://github.com/astral-sh/ruff/issues/19294
x = Path("foo.txt")
x.open()
# https://github.com/astral-sh/ruff/issues/18107
codecs.open("plw1514.py", "r", "utf-8").close() # this is fine
# function argument annotated as Path
from pathlib import Path
def format_file(file: Path):
with file.open() as f:
contents = f.read()

View file

@ -4,6 +4,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::analyze::typing;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@ -111,20 +112,34 @@ enum Callee<'a> {
}
impl<'a> Callee<'a> {
fn is_pathlib_path_call(expr: &Expr, semantic: &SemanticModel) -> bool {
if let Expr::Call(ast::ExprCall { func, .. }) = expr {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["pathlib", "Path"])
})
} else {
false
}
}
fn try_from_call_expression(
call: &'a ast::ExprCall,
semantic: &'a SemanticModel,
) -> Option<Self> {
if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = call.func.as_ref() {
// Check for `pathlib.Path(...).open(...)` or equivalent
if let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() {
if semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["pathlib", "Path"])
})
{
return Some(Callee::Pathlib(attr));
// Direct: Path(...).open()
if Self::is_pathlib_path_call(value, semantic) {
return Some(Callee::Pathlib(attr));
}
// Indirect: x.open() where x = Path(...)
else if let Expr::Name(name) = value.as_ref() {
if let Some(binding_id) = semantic.only_binding(name) {
let binding = semantic.binding(binding_id);
if typing::is_pathlib_path(binding, semantic) {
return Some(Callee::Pathlib(attr));
}
}
}
}

View file

@ -435,3 +435,41 @@ unspecified_encoding.py:80:1: PLW1514 [*] `pathlib.Path(...).write_text` without
81 81 |
82 82 | # Non-errors.
83 83 | Path("foo.txt").open(encoding="utf-8")
unspecified_encoding.py:96:1: PLW1514 [*] `pathlib.Path(...).open` in text mode without explicit `encoding` argument
|
94 | # https://github.com/astral-sh/ruff/issues/19294
95 | x = Path("foo.txt")
96 | x.open()
| ^^^^^^ PLW1514
97 |
98 | # https://github.com/astral-sh/ruff/issues/18107
|
= help: Add explicit `encoding` argument
Unsafe fix
93 93 |
94 94 | # https://github.com/astral-sh/ruff/issues/19294
95 95 | x = Path("foo.txt")
96 |-x.open()
96 |+x.open(encoding="utf-8")
97 97 |
98 98 | # https://github.com/astral-sh/ruff/issues/18107
99 99 | codecs.open("plw1514.py", "r", "utf-8").close() # this is fine
unspecified_encoding.py:105:10: PLW1514 [*] `pathlib.Path(...).open` in text mode without explicit `encoding` argument
|
104 | def format_file(file: Path):
105 | with file.open() as f:
| ^^^^^^^^^ PLW1514
106 | contents = f.read()
|
= help: Add explicit `encoding` argument
Unsafe fix
102 102 | from pathlib import Path
103 103 |
104 104 | def format_file(file: Path):
105 |- with file.open() as f:
105 |+ with file.open(encoding="utf-8") as f:
106 106 | contents = f.read()