[`pyflakes`] Fix false positive for `__class__` in lambda expressions within class definitions (`F821`) (#20564)

## Summary

Fixes #20562

---------

Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
This commit is contained in:
Dan Parizher 2025-10-24 10:07:19 -04:00 committed by GitHub
parent 7576669297
commit 3e8685d2ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 1 deletions

View File

@ -0,0 +1,21 @@
class C:
f = lambda self: __class__
print(C().f().__name__)
# Test: nested lambda
class D:
g = lambda self: (lambda: __class__)
print(D().g()().__name__)
# Test: lambda outside class (should still fail)
h = lambda: __class__
# Test: lambda referencing module-level variable (should not be flagged as F821)
import uuid
class E:
uuid = lambda: str(uuid.uuid4())

View File

@ -2116,7 +2116,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
| Expr::DictComp(_) | Expr::DictComp(_)
| Expr::SetComp(_) => { | Expr::SetComp(_) => {
self.analyze.scopes.push(self.semantic.scope_id); self.analyze.scopes.push(self.semantic.scope_id);
self.semantic.pop_scope(); self.semantic.pop_scope(); // Lambda/Generator/Comprehension scope
} }
_ => {} _ => {}
} }
@ -3041,7 +3041,35 @@ impl<'a> Checker<'a> {
if let Some(parameters) = parameters { if let Some(parameters) = parameters {
self.visit_parameters(parameters); self.visit_parameters(parameters);
} }
// Here we add the implicit scope surrounding a lambda which allows code in the
// lambda to access `__class__` at runtime when the lambda is defined within a class.
// See the `ScopeKind::DunderClassCell` docs for more information.
let added_dunder_class_scope = if self
.semantic
.current_scopes()
.any(|scope| scope.kind.is_class())
{
self.semantic.push_scope(ScopeKind::DunderClassCell);
let binding_id = self.semantic.push_binding(
TextRange::default(),
BindingKind::DunderClassCell,
BindingFlags::empty(),
);
self.semantic
.current_scope_mut()
.add("__class__", binding_id);
true
} else {
false
};
self.visit_expr(body); self.visit_expr(body);
// Pop the DunderClassCell scope if it was added
if added_dunder_class_scope {
self.semantic.pop_scope();
}
} }
} }
self.semantic.restore(snapshot); self.semantic.restore(snapshot);

View File

@ -166,6 +166,7 @@ mod tests {
#[test_case(Rule::UndefinedName, Path::new("F821_30.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_30.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_31.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_31.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_32.pyi"))] #[test_case(Rule::UndefinedName, Path::new("F821_32.pyi"))]
#[test_case(Rule::UndefinedName, Path::new("F821_33.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]

View File

@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F821 Undefined name `__class__`
--> F821_33.py:15:13
|
14 | # Test: lambda outside class (should still fail)
15 | h = lambda: __class__
| ^^^^^^^^^
16 |
17 | # Test: lambda referencing module-level variable (should not be flagged as F821)
|