[pyupgrade] Do not offer fix when at least one target is global/nonlocal (UP028) (#16451)

## Summary

Resolves #16445.

`UP028` is now no longer always fixable: it will not offer a fix when at
least one `ExprName` target is bound to either a `global` or a
`nonlocal` declaration.

## Test Plan

`cargo nextest run` and `cargo insta test`.
This commit is contained in:
InSync 2025-03-04 17:28:01 +07:00 committed by GitHub
parent d93ed293eb
commit a3ae76edc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 115 additions and 27 deletions

View file

@ -163,3 +163,26 @@ def f():
pass
except Exception as x:
pass
# https://github.com/astral-sh/ruff/issues/15540
def f():
for a in 1,:
yield a
SOME_GLOBAL = None
def f(iterable):
global SOME_GLOBAL
for SOME_GLOBAL in iterable:
yield SOME_GLOBAL
some_non_local = None
def g():
nonlocal some_non_local
for some_non_local in iterable:
yield some_non_local

View file

@ -123,7 +123,20 @@ def f():
yield x, y, x + y
# https://github.com/astral-sh/ruff/issues/15540
def f():
for a in 1,:
yield a
global some_global
for element in iterable:
some_global = element
yield some_global
def f():
some_nonlocal = 1
def g():
nonlocal some_nonlocal
for element in iterable:
some_nonlocal = element
yield some_nonlocal

View file

@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Stmt};
@ -17,11 +17,19 @@ use crate::checkers::ast::Checker;
/// ```python
/// for x in foo:
/// yield x
///
/// global y
/// for y in foo:
/// yield y
/// ```
///
/// Use instead:
/// ```python
/// yield from foo
///
/// for _element in foo:
/// y = _element
/// yield y
/// ```
///
/// ## Fix safety
@ -31,6 +39,9 @@ use crate::checkers::ast::Checker;
/// to a `yield from` could lead to an attribute error if the underlying
/// generator does not implement the `send` method.
///
/// Additionally, if at least one target is `global` or `nonlocal`,
/// no fix will be offered.
///
/// In most cases, however, the fix is safe, and such a modification should have
/// no effect on the behavior of the program.
///
@ -40,14 +51,16 @@ use crate::checkers::ast::Checker;
#[derive(ViolationMetadata)]
pub(crate) struct YieldInForLoop;
impl AlwaysFixableViolation for YieldInForLoop {
impl Violation for YieldInForLoop {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"Replace `yield` over `for` loop with `yield from`".to_string()
}
fn fix_title(&self) -> String {
"Replace with `yield from`".to_string()
fn fix_title(&self) -> Option<String> {
Some("Replace with `yield from`".to_string())
}
}
@ -130,10 +143,22 @@ pub(crate) fn yield_in_for_loop(checker: &Checker, stmt_for: &ast::StmtFor) {
format!("yield from {contents}")
};
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents,
stmt_for.range(),
)));
if !collect_names(value).any(|name| {
let semantic = checker.semantic();
let mut bindings = semantic.current_scope().get_all(name.id.as_str());
bindings.any(|id| {
let binding = semantic.binding(id);
binding.is_global() || binding.is_nonlocal()
})
}) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents,
stmt_for.range(),
)));
}
checker.report_diagnostic(diagnostic);
}

View file

@ -380,3 +380,46 @@ UP028_0.py:134:5: UP028 [*] Replace `yield` over `for` loop with `yield from`
136 135 | # Shadowing with multiple `except` blocks
137 136 | try:
138 137 | pass
UP028_0.py:170:5: UP028 [*] Replace `yield` over `for` loop with `yield from`
|
168 | # https://github.com/astral-sh/ruff/issues/15540
169 | def f():
170 | / for a in 1,:
171 | | yield a
| |_______________^ UP028
|
= help: Replace with `yield from`
Unsafe fix
167 167 |
168 168 | # https://github.com/astral-sh/ruff/issues/15540
169 169 | def f():
170 |- for a in 1,:
171 |- yield a
170 |+ yield from (1,)
172 171 |
173 172 |
174 173 | SOME_GLOBAL = None
UP028_0.py:179:5: UP028 Replace `yield` over `for` loop with `yield from`
|
177 | global SOME_GLOBAL
178 |
179 | / for SOME_GLOBAL in iterable:
180 | | yield SOME_GLOBAL
| |_________________________^ UP028
181 |
182 | some_non_local = None
|
= help: Replace with `yield from`
UP028_0.py:187:9: UP028 Replace `yield` over `for` loop with `yield from`
|
185 | nonlocal some_non_local
186 |
187 | / for some_non_local in iterable:
188 | | yield some_non_local
| |________________________________^ UP028
|
= help: Replace with `yield from`

View file

@ -1,20 +1,4 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP028_1.py:128:5: UP028 [*] Replace `yield` over `for` loop with `yield from`
|
126 | # https://github.com/astral-sh/ruff/issues/15540
127 | def f():
128 | / for a in 1,:
129 | | yield a
| |_______________^ UP028
|
= help: Replace with `yield from`
Unsafe fix
125 125 |
126 126 | # https://github.com/astral-sh/ruff/issues/15540
127 127 | def f():
128 |- for a in 1,:
129 |- yield a
128 |+ yield from (1,)