Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py
Original file line number Diff line number Diff line change
@@ -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())
30 changes: 29 additions & 1 deletion crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2052,7 +2052,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
| Expr::DictComp(_)
| Expr::SetComp(_) => {
self.analyze.scopes.push(self.semantic.scope_id);
self.semantic.pop_scope();
self.semantic.pop_scope(); // Lambda/Generator/Comprehension scope
}
_ => {}
}
Expand Down Expand Up @@ -2977,7 +2977,35 @@ impl<'a> Checker<'a> {
if let Some(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);

// Pop the DunderClassCell scope if it was added
if added_dunder_class_scope {
self.semantic.pop_scope();
}
}
}
self.semantic.restore(snapshot);
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ mod tests {
#[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_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.pyi"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
|