[flake8-use-pathlib] Fix PTH123 false positive when open is passed a file descriptor from a function call (#17705)

## Summary
Includes minor changes to the semantic type inference to help detect the
return type of function call.

Fixes #17691

## Test Plan

Snapshot tests
This commit is contained in:
Victor Hugo Gomes 2025-04-29 17:51:38 -03:00 committed by GitHub
parent 93d6a3567b
commit 8c68d30c3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 58 additions and 5 deletions

View file

@ -55,3 +55,16 @@ x = 2
open(x) open(x)
def foo(y: int): def foo(y: int):
open(y) open(y)
# https://github.com/astral-sh/ruff/issues/17691
def f() -> int:
return 1
open(f())
open(b"foo")
byte_str = b"bar"
open(byte_str)
def bytes_str_func() -> bytes:
return b"foo"
open(bytes_str_func())

View file

@ -126,10 +126,9 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
.arguments .arguments
.find_argument_value("opener", 7) .find_argument_value("opener", 7)
.is_some_and(|expr| !expr.is_none_literal_expr()) .is_some_and(|expr| !expr.is_none_literal_expr())
|| call || call.arguments.find_positional(0).is_some_and(|expr| {
.arguments is_file_descriptor_or_bytes_str(expr, checker.semantic())
.find_positional(0) })
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
{ {
return None; return None;
} }
@ -168,6 +167,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
} }
} }
fn is_file_descriptor_or_bytes_str(expr: &Expr, semantic: &SemanticModel) -> bool {
is_file_descriptor(expr, semantic) || is_bytes_string(expr, semantic)
}
/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer. /// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool { fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
if matches!( if matches!(
@ -180,7 +183,7 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
return true; return true;
} }
let Some(name) = expr.as_name_expr() else { let Some(name) = get_name_expr(expr) else {
return false; return false;
}; };
@ -190,3 +193,28 @@ fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
typing::is_int(binding, semantic) typing::is_int(binding, semantic)
} }
/// Returns `true` if the given expression is a bytes string.
fn is_bytes_string(expr: &Expr, semantic: &SemanticModel) -> bool {
if matches!(expr, Expr::BytesLiteral(_)) {
return true;
}
let Some(name) = get_name_expr(expr) else {
return false;
};
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
return false;
};
typing::is_bytes(binding, semantic)
}
fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
match expr {
Expr::Name(name) => Some(name),
Expr::Call(ast::ExprCall { func, .. }) => get_name_expr(func),
_ => None,
}
}

View file

@ -639,6 +639,18 @@ pub fn check_type<T: TypeChecker>(binding: &Binding, semantic: &SemanticModel) -
_ => false, _ => false,
}, },
BindingKind::FunctionDefinition(_) => match binding.statement(semantic) {
// ```python
// def foo() -> int:
// ...
// ```
Some(Stmt::FunctionDef(ast::StmtFunctionDef { returns, .. })) => returns
.as_ref()
.is_some_and(|return_ann| T::match_annotation(return_ann, semantic)),
_ => false,
},
_ => false, _ => false,
} }
} }