diff --git a/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py new file mode 100644 index 0000000000..04a3726cf6 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyflakes/F821_33.py @@ -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()) diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 7750d29f34..2321cfbb7c 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -2116,7 +2116,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 } _ => {} } @@ -3041,7 +3041,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); diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index 3dd7edde65..5f41a19f2a 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -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"))] diff --git a/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_33.py.snap b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_33.py.snap new file mode 100644 index 0000000000..04e1cd30dd --- /dev/null +++ b/crates/ruff_linter/src/rules/pyflakes/snapshots/ruff_linter__rules__pyflakes__tests__F821_F821_33.py.snap @@ -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) + |