diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py b/crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py index db89cbf4e2b7b..07bba3bea8339 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/unspecified_encoding.py @@ -91,9 +91,16 @@ def func(*args, **kwargs): 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() diff --git a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs index cc9ff1cb29c13..e6d9e43cc0ab2 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/unspecified_encoding.rs @@ -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 { 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)); + } } } } diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1514_unspecified_encoding.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1514_unspecified_encoding.py.snap index cc91be3fd94ae..c67477849c495 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1514_unspecified_encoding.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW1514_unspecified_encoding.py.snap @@ -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()