[`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 pass
except Exception as x: except Exception as x:
pass 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 yield x, y, x + y
# https://github.com/astral-sh/ruff/issues/15540
def f(): def f():
for a in 1,: global some_global
yield a
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_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::parenthesize::parenthesized_range; use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Expr, Stmt}; use ruff_python_ast::{self as ast, Expr, Stmt};
@ -17,11 +17,19 @@ use crate::checkers::ast::Checker;
/// ```python /// ```python
/// for x in foo: /// for x in foo:
/// yield x /// yield x
///
/// global y
/// for y in foo:
/// yield y
/// ``` /// ```
/// ///
/// Use instead: /// Use instead:
/// ```python /// ```python
/// yield from foo /// yield from foo
///
/// for _element in foo:
/// y = _element
/// yield y
/// ``` /// ```
/// ///
/// ## Fix safety /// ## Fix safety
@ -31,6 +39,9 @@ use crate::checkers::ast::Checker;
/// to a `yield from` could lead to an attribute error if the underlying /// to a `yield from` could lead to an attribute error if the underlying
/// generator does not implement the `send` method. /// 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 /// In most cases, however, the fix is safe, and such a modification should have
/// no effect on the behavior of the program. /// no effect on the behavior of the program.
/// ///
@ -40,14 +51,16 @@ use crate::checkers::ast::Checker;
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
pub(crate) struct YieldInForLoop; pub(crate) struct YieldInForLoop;
impl AlwaysFixableViolation for YieldInForLoop { impl Violation for YieldInForLoop {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
"Replace `yield` over `for` loop with `yield from`".to_string() "Replace `yield` over `for` loop with `yield from`".to_string()
} }
fn fix_title(&self) -> String { fn fix_title(&self) -> Option<String> {
"Replace with `yield from`".to_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}") format!("yield from {contents}")
}; };
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( diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents, contents,
stmt_for.range(), stmt_for.range(),
))); )));
}
checker.report_diagnostic(diagnostic); 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 136 135 | # Shadowing with multiple `except` blocks
137 136 | try: 137 136 | try:
138 137 | pass 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 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,)