diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 870161e2fcc20..033fa6b755932 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4841,13 +4841,6 @@ impl<'a> Checker<'a> { return; } - // Allow "__module__" and "__qualname__" in class scopes. - if (id == "__module__" || id == "__qualname__") - && matches!(self.ctx.scope().kind, ScopeKind::Class(..)) - { - return; - } - // Avoid flagging if `NameError` is handled. if self .ctx diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 89afaefdd70d7..8fe0e3ca1938e 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -473,6 +473,16 @@ mod tests { "#, &[Rule::UndefinedName], ); + flakes( + r#" + def f(): + __qualname__ = 1 + + class Foo: + __qualname__ + "#, + &[Rule::UnusedVariable], + ); } #[test] @@ -1151,6 +1161,40 @@ mod tests { "#, &[], ); + flakes( + r#" + class Test(object): + print(__class__.__name__) + + def __init__(self): + self.x = 1 + + t = Test() + "#, + &[Rule::UndefinedName], + ); + flakes( + r#" + class Test(object): + X = [__class__ for _ in range(10)] + + def __init__(self): + self.x = 1 + + t = Test() + "#, + &[Rule::UndefinedName], + ); + flakes( + r#" + def f(self): + print(__class__.__name__) + self.x = 1 + + f() + "#, + &[Rule::UndefinedName], + ); } /// See: diff --git a/crates/ruff_python_semantic/src/context.rs b/crates/ruff_python_semantic/src/context.rs index bf2cec0756c0c..3f3f91c7ae90a 100644 --- a/crates/ruff_python_semantic/src/context.rs +++ b/crates/ruff_python_semantic/src/context.rs @@ -126,11 +126,19 @@ impl<'a> Context<'a> { } } + let mut seen_function = false; let mut import_starred = false; for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() { let scope = &self.scopes[scope_id]; if scope.kind.is_class() { - if symbol == "__class__" { + // Allow usages of `__class__` within methods, e.g.: + // + // ```python + // class Foo: + // def __init__(self): + // print(__class__) + // ``` + if seen_function && matches!(symbol, "__class__") { return ResolvedReference::ImplicitGlobal; } if index > 0 { @@ -162,6 +170,28 @@ impl<'a> Context<'a> { return ResolvedReference::Resolved(scope_id, *binding_id); } + // Allow usages of `__module__` and `__qualname__` within class scopes, e.g.: + // + // ```python + // class Foo: + // print(__qualname__) + // ``` + // + // Intentionally defer this check to _after_ the standard `scope.get` logic, so that + // we properly attribute reads to overridden class members, e.g.: + // + // ```python + // class Foo: + // __qualname__ = "Bar" + // print(__qualname__) + // ``` + if index == 0 && scope.kind.is_class() { + if matches!(symbol, "__module__" | "__qualname__") { + return ResolvedReference::ImplicitGlobal; + } + } + + seen_function |= scope.kind.is_function(); import_starred = import_starred || scope.uses_star_imports(); }