From 047c2118379a9946a41857019e7a3781beef3028 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 28 Jul 2023 17:06:37 -0500 Subject: [PATCH] Add semantic analysis of type aliases and parameters (#6109) Requires https://github.com/astral-sh/RustPython-Parser/pull/42 Related https://github.com/PyCQA/pyflakes/pull/778 [PEP-695](https://peps.python.org/pep-0695) Part of #5062 ## Summary Adds a scope for type parameters, a type parameter binding kind, and checker visitation of type parameters in type alias statements, function definitions, and class definitions. A few changes were necessary to ensure correctness following the insertion of a new scope between function and class scopes and their parent. ## Test Plan Undefined name snapshots. Unused type parameter rule will be added as follow-up. --- .../test/fixtures/pyflakes/F821_17.py | 103 ++++++++++ crates/ruff/src/checkers/ast/deferred.rs | 6 +- crates/ruff/src/checkers/ast/mod.rs | 79 +++++++- crates/ruff/src/renamer.rs | 1 + .../rules/unused_arguments.rs | 5 +- crates/ruff/src/rules/pyflakes/mod.rs | 1 + ...les__pyflakes__tests__F821_F821_17.py.snap | 178 ++++++++++++++++++ crates/ruff/src/rules/pylint/helpers.rs | 2 +- crates/ruff_python_semantic/src/binding.rs | 12 ++ crates/ruff_python_semantic/src/model.rs | 35 +++- crates/ruff_python_semantic/src/scope.rs | 1 + 11 files changed, 405 insertions(+), 18 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pyflakes/F821_17.py create mode 100644 crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F821_F821_17.py.snap diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F821_17.py b/crates/ruff/resources/test/fixtures/pyflakes/F821_17.py new file mode 100644 index 0000000000..0daa3bdceb --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyflakes/F821_17.py @@ -0,0 +1,103 @@ +"""Test type parameters and aliases""" + +# Type parameters in type alias statements + +from some_module import Bar + +type Foo[T] = T # OK +type Foo[T] = list[T] # OK +type Foo[T: ForwardA] = T # OK +type Foo[*Ts] = Bar[Ts] # OK +type Foo[**P] = Bar[P] # OK +class ForwardA: ... + +# Types used in aliased assignment must exist + +type Foo = DoesNotExist # F821: Undefined name `DoesNotExist` +type Foo = list[DoesNotExist] # F821: Undefined name `DoesNotExist` + +# Type parameters do not escape alias scopes + +type Foo[T] = T +T # F821: Undefined name `T` - not accessible afterward alias scope + +# Type parameters in functions + +def foo[T](t: T) -> T: return t # OK +async def afoo[T](t: T) -> T: return t # OK +def with_forward_ref[T: ForwardB](t: T) -> T: return t # OK +def can_access_inside[T](t: T) -> T: # OK + print(T) # OK + return t # OK +class ForwardB: ... + + +# Type parameters do not escape function scopes + +from some_library import some_decorator + +@some_decorator(T) # F821: Undefined name `T` - not accessible in decorators + +def foo[T](t: T) -> None: ... +T # F821: Undefined name `T` - not accessible afterward function scope + + +# Type parameters in classes + +class Foo[T](list[T]): ... # OK +class UsesForward[T: ForwardC](list[T]): ... # OK +class ForwardC: ... +class WithinBody[T](list[T]): # OK + t = T # OK + x: T # OK + + def foo(self, x: T) -> T: # OK + return x + + def foo(self): + T # OK + + +# Type parameters do not escape class scopes + +from some_library import some_decorator +@some_decorator(T) # F821: Undefined name `T` - not accessible in decorators + +class Foo[T](list[T]): ... +T # F821: Undefined name `T` - not accessible after class scope + +# Types specified in bounds should exist + +type Foo[T: DoesNotExist] = T # F821: Undefined name `DoesNotExist` +def foo[T: DoesNotExist](t: T) -> T: return t # F821: Undefined name `DoesNotExist` +class Foo[T: DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist` + +type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +def foo[T: (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + +# Type parameters in nested classes + +class Parent[T]: + t = T # OK + + def can_use_class_variable(self, x: t) -> t: # OK + return x + + class Child: + def can_access_parent_type_parameter(self, x: T) -> T: # OK + T # OK + return x + + def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T` + t # F821: Undefined name `t` + return x + +# Type parameters in nested functions + +def can_access_inside_nested[T](t: T) -> T: # OK + def bar(x: T) -> T: # OK + T # OK + return x + + bar(t) diff --git a/crates/ruff/src/checkers/ast/deferred.rs b/crates/ruff/src/checkers/ast/deferred.rs index b44480b00d..85c900d1b0 100644 --- a/crates/ruff/src/checkers/ast/deferred.rs +++ b/crates/ruff/src/checkers/ast/deferred.rs @@ -1,7 +1,6 @@ -use ruff_python_ast::Expr; -use ruff_text_size::TextRange; - +use ruff_python_ast::{Expr, TypeParam}; use ruff_python_semantic::{ScopeId, Snapshot}; +use ruff_text_size::TextRange; /// A collection of AST nodes that are deferred for later analysis. /// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all @@ -11,6 +10,7 @@ pub(crate) struct Deferred<'a> { pub(crate) scopes: Vec, pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>, pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>, + pub(crate) type_param_definitions: Vec<(&'a TypeParam, Snapshot)>, pub(crate) functions: Vec, pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>, pub(crate) for_loops: Vec, diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index a538da7287..442000a3f2 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -453,12 +453,14 @@ where args, decorator_list, returns, + type_params, .. }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, args, decorator_list, + type_params, returns, .. }) => { @@ -472,6 +474,12 @@ where // are enabled. let runtime_annotation = !self.semantic.future_annotations(); + self.semantic.push_scope(ScopeKind::Type); + + for type_param in type_params { + self.visit_type_param(type_param); + } + for arg_with_default in args .posonlyargs .iter() @@ -542,18 +550,25 @@ where bases, keywords, decorator_list, + type_params, .. }, ) => { + for decorator in decorator_list { + self.visit_decorator(decorator); + } + + self.semantic.push_scope(ScopeKind::Type); + + for type_param in type_params { + self.visit_type_param(type_param); + } for expr in bases { self.visit_expr(expr); } for keyword in keywords { self.visit_keyword(keyword); } - for decorator in decorator_list { - self.visit_decorator(decorator); - } let definition = docstrings::extraction::extract_definition( ExtractionTarget::Class, @@ -562,7 +577,6 @@ where &self.semantic.definitions, ); self.semantic.push_definition(definition); - self.semantic.push_scope(ScopeKind::Class(class_def)); // Extract any global bindings from the class body. @@ -572,6 +586,20 @@ where self.visit_body(body); } + Stmt::TypeAlias(ast::StmtTypeAlias { + range: _range, + name, + type_params, + value, + }) => { + self.semantic.push_scope(ScopeKind::Type); + for type_param in type_params { + self.visit_type_param(type_param); + } + self.visit_expr(value); + self.semantic.pop_scope(); + self.visit_expr(name); + } Stmt::Try(ast::StmtTry { body, handlers, @@ -715,8 +743,9 @@ where | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => { let scope_id = self.semantic.scope_id; self.deferred.scopes.push(scope_id); - self.semantic.pop_scope(); + self.semantic.pop_scope(); // Function scope self.semantic.pop_definition(); + self.semantic.pop_scope(); // Type parameter scope self.add_binding( name, stmt.identifier(), @@ -727,8 +756,9 @@ where Stmt::ClassDef(ast::StmtClassDef { name, .. }) => { let scope_id = self.semantic.scope_id; self.deferred.scopes.push(scope_id); - self.semantic.pop_scope(); + self.semantic.pop_scope(); // Class scope self.semantic.pop_definition(); + self.semantic.pop_scope(); // Type parameter scope self.add_binding( name, stmt.identifier(), @@ -1331,6 +1361,26 @@ where self.visit_stmt(stmt); } } + + fn visit_type_param(&mut self, type_param: &'b ast::TypeParam) { + // Step 1: Binding + match type_param { + ast::TypeParam::TypeVar(ast::TypeParamTypeVar { name, range, .. }) + | ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { name, range }) + | ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { name, range }) => { + self.add_binding( + name.as_str(), + *range, + BindingKind::TypeParam, + BindingFlags::empty(), + ); + } + } + // Step 2: Traversal + self.deferred + .type_param_definitions + .push((type_param, self.semantic.snapshot())); + } } impl<'a> Checker<'a> { @@ -1693,6 +1743,22 @@ impl<'a> Checker<'a> { } } + fn visit_deferred_type_param_definitions(&mut self) { + while !self.deferred.type_param_definitions.is_empty() { + let type_params = std::mem::take(&mut self.deferred.type_param_definitions); + for (type_param, snapshot) in type_params { + self.semantic.restore(snapshot); + + if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar { + bound: Some(bound), .. + }) = type_param + { + self.visit_expr(bound); + } + } + } + } + fn visit_deferred_string_type_definitions(&mut self, allocator: &'a typed_arena::Arena) { while !self.deferred.string_type_definitions.is_empty() { let type_definitions = std::mem::take(&mut self.deferred.string_type_definitions); @@ -1882,6 +1948,7 @@ pub(crate) fn check_ast( checker.visit_deferred_functions(); checker.visit_deferred_lambdas(); checker.visit_deferred_future_type_definitions(); + checker.visit_deferred_type_param_definitions(); let allocator = typed_arena::Arena::new(); checker.visit_deferred_string_type_definitions(&allocator); checker.visit_exports(); diff --git a/crates/ruff/src/renamer.rs b/crates/ruff/src/renamer.rs index 9274018a79..84f4cfbe76 100644 --- a/crates/ruff/src/renamer.rs +++ b/crates/ruff/src/renamer.rs @@ -242,6 +242,7 @@ impl Renamer { // By default, replace the binding's name with the target name. BindingKind::Annotation | BindingKind::Argument + | BindingKind::TypeParam | BindingKind::NamedExprAssignment | BindingKind::UnpackedAssignment | BindingKind::Assignment diff --git a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs index 78ad38e2b4..0a7b06a051 100644 --- a/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs +++ b/crates/ruff/src/rules/flake8_unused_arguments/rules/unused_arguments.rs @@ -317,10 +317,7 @@ pub(crate) fn unused_arguments( scope: &Scope, diagnostics: &mut Vec, ) { - let Some(parent) = scope - .parent - .map(|scope_id| &checker.semantic().scopes[scope_id]) - else { + let Some(parent) = &checker.semantic().first_non_type_parent_scope(scope) else { return; }; diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 08170bb519..7eb0434f31 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -128,6 +128,7 @@ mod tests { #[test_case(Rule::UndefinedName, Path::new("F821_14.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_15.py"))] #[test_case(Rule::UndefinedName, Path::new("F821_16.py"))] + #[test_case(Rule::UndefinedName, Path::new("F821_17.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))] #[test_case(Rule::UndefinedExport, Path::new("F822_2.py"))] diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F821_F821_17.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F821_F821_17.py.snap new file mode 100644 index 0000000000..1cec326ee5 --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F821_F821_17.py.snap @@ -0,0 +1,178 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- +F821_17.py:16:12: F821 Undefined name `DoesNotExist` + | +14 | # Types used in aliased assignment must exist +15 | +16 | type Foo = DoesNotExist # F821: Undefined name `DoesNotExist` + | ^^^^^^^^^^^^ F821 +17 | type Foo = list[DoesNotExist] # F821: Undefined name `DoesNotExist` + | + +F821_17.py:17:17: F821 Undefined name `DoesNotExist` + | +16 | type Foo = DoesNotExist # F821: Undefined name `DoesNotExist` +17 | type Foo = list[DoesNotExist] # F821: Undefined name `DoesNotExist` + | ^^^^^^^^^^^^ F821 +18 | +19 | # Type parameters do not escape alias scopes + | + +F821_17.py:22:1: F821 Undefined name `T` + | +21 | type Foo[T] = T +22 | T # F821: Undefined name `T` - not accessible afterward alias scope + | ^ F821 +23 | +24 | # Type parameters in functions + | + +F821_17.py:39:17: F821 Undefined name `T` + | +37 | from some_library import some_decorator +38 | +39 | @some_decorator(T) # F821: Undefined name `T` - not accessible in decorators + | ^ F821 +40 | +41 | def foo[T](t: T) -> None: ... + | + +F821_17.py:42:1: F821 Undefined name `T` + | +41 | def foo[T](t: T) -> None: ... +42 | T # F821: Undefined name `T` - not accessible afterward function scope + | ^ F821 + | + +F821_17.py:64:17: F821 Undefined name `T` + | +63 | from some_library import some_decorator +64 | @some_decorator(T) # F821: Undefined name `T` - not accessible in decorators + | ^ F821 +65 | +66 | class Foo[T](list[T]): ... + | + +F821_17.py:67:1: F821 Undefined name `T` + | +66 | class Foo[T](list[T]): ... +67 | T # F821: Undefined name `T` - not accessible after class scope + | ^ F821 +68 | +69 | # Types specified in bounds should exist + | + +F821_17.py:71:13: F821 Undefined name `DoesNotExist` + | +69 | # Types specified in bounds should exist +70 | +71 | type Foo[T: DoesNotExist] = T # F821: Undefined name `DoesNotExist` + | ^^^^^^^^^^^^ F821 +72 | def foo[T: DoesNotExist](t: T) -> T: return t # F821: Undefined name `DoesNotExist` +73 | class Foo[T: DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist` + | + +F821_17.py:72:12: F821 Undefined name `DoesNotExist` + | +71 | type Foo[T: DoesNotExist] = T # F821: Undefined name `DoesNotExist` +72 | def foo[T: DoesNotExist](t: T) -> T: return t # F821: Undefined name `DoesNotExist` + | ^^^^^^^^^^^^ F821 +73 | class Foo[T: DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist` + | + +F821_17.py:73:14: F821 Undefined name `DoesNotExist` + | +71 | type Foo[T: DoesNotExist] = T # F821: Undefined name `DoesNotExist` +72 | def foo[T: DoesNotExist](t: T) -> T: return t # F821: Undefined name `DoesNotExist` +73 | class Foo[T: DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist` + | ^^^^^^^^^^^^ F821 +74 | +75 | type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | + +F821_17.py:75:14: F821 Undefined name `DoesNotExist1` + | +73 | class Foo[T: DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist` +74 | +75 | type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | ^^^^^^^^^^^^^ F821 +76 | def foo[T: (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +77 | class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | + +F821_17.py:75:29: F821 Undefined name `DoesNotExist2` + | +73 | class Foo[T: DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist` +74 | +75 | type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | ^^^^^^^^^^^^^ F821 +76 | def foo[T: (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +77 | class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | + +F821_17.py:76:13: F821 Undefined name `DoesNotExist1` + | +75 | type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +76 | def foo[T: (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | ^^^^^^^^^^^^^ F821 +77 | class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | + +F821_17.py:76:28: F821 Undefined name `DoesNotExist2` + | +75 | type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +76 | def foo[T: (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | ^^^^^^^^^^^^^ F821 +77 | class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | + +F821_17.py:77:15: F821 Undefined name `DoesNotExist1` + | +75 | type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +76 | def foo[T: (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +77 | class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | ^^^^^^^^^^^^^ F821 +78 | +79 | # Type parameters in nested classes + | + +F821_17.py:77:30: F821 Undefined name `DoesNotExist2` + | +75 | type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +76 | def foo[T: (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` +77 | class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2` + | ^^^^^^^^^^^^^ F821 +78 | +79 | # Type parameters in nested classes + | + +F821_17.py:92:52: F821 Undefined name `t` + | +90 | return x +91 | +92 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T` + | ^ F821 +93 | t # F821: Undefined name `t` +94 | return x + | + +F821_17.py:92:58: F821 Undefined name `t` + | +90 | return x +91 | +92 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T` + | ^ F821 +93 | t # F821: Undefined name `t` +94 | return x + | + +F821_17.py:93:17: F821 Undefined name `t` + | +92 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T` +93 | t # F821: Undefined name `t` + | ^ F821 +94 | return x + | + + diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index 1b04390398..139485c872 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -44,7 +44,7 @@ pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &Settings) -> b if name != "__init__" { return false; } - let Some(parent) = scope.parent.map(|scope_id| &semantic.scopes[scope_id]) else { + let Some(parent) = semantic.first_non_type_parent_scope(scope) else { return false; }; diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index 6a6e712f58..7dd9056dd6 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -413,6 +413,18 @@ pub enum BindingKind<'a> { /// ``` Assignment, + /// A binding for a generic type parameter, like `X` in: + /// ```python + /// def foo[X](x: X): + /// ... + /// + /// class Foo[X](x: X): + /// ... + /// + /// type Foo[X] = ... + /// ``` + TypeParam, + /// A binding for a for-loop variable, like `x` in: /// ```python /// for x in range(10): diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index ee338cd8c1..75810ef142 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -308,6 +308,7 @@ impl<'a> SemanticModel<'a> { let mut seen_function = false; let mut import_starred = false; + let mut class_variables_visible = true; for (index, scope_id) in self.scopes.ancestor_ids(self.scope_id).enumerate() { let scope = &self.scopes[scope_id]; if scope.kind.is_class() { @@ -321,7 +322,8 @@ impl<'a> SemanticModel<'a> { if seen_function && matches!(name.id.as_str(), "__class__") { return ReadResult::ImplicitGlobal; } - // Do not allow usages of class symbols unless it is the immediate parent, e.g.: + // Do not allow usages of class symbols unless it is the immediate parent + // (excluding type scopes), e.g.: // // ```python // class Foo: @@ -334,12 +336,16 @@ impl<'a> SemanticModel<'a> { // def d(self): // print(a) # not allowed // ``` - // - if index > 0 { + if !class_variables_visible { continue; } } + // Allow class variables to be visible for an additional scope level + // when a type scope is seen — this covers the type scope present between + // function and class definitions and their parent class scope. + class_variables_visible = scope.kind.is_type() && index == 0; + if let Some(binding_id) = scope.get(name.id.as_str()) { // Mark the binding as used. let reference_id = @@ -500,17 +506,20 @@ impl<'a> SemanticModel<'a> { } let mut seen_function = false; + let mut class_variables_visible = true; 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 seen_function && matches!(symbol, "__class__") { return None; } - if index > 0 { + if !class_variables_visible { continue; } } + class_variables_visible = scope.kind.is_type() && index == 0; + if let Some(binding_id) = scope.get(symbol) { match self.bindings[binding_id].kind { BindingKind::Annotation => continue, @@ -856,6 +865,24 @@ impl<'a> SemanticModel<'a> { &self.scopes[self.scope_id] } + /// Returns the parent of the given scope, if any. + pub fn parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> { + scope.parent.map(|scope_id| &self.scopes[scope_id]) + } + + /// Returns the first parent of the given scope that is not a [`ScopeKind::Type`] scope, if any. + pub fn first_non_type_parent_scope(&self, scope: &Scope) -> Option<&Scope<'a>> { + let mut current_scope = scope; + while let Some(parent) = self.parent_scope(current_scope) { + if parent.kind.is_type() { + current_scope = parent; + } else { + return Some(parent); + } + } + None + } + /// Returns a mutable reference to the current top most scope. pub fn scope_mut(&mut self) -> &mut Scope<'a> { &mut self.scopes[self.scope_id] diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index b263436b09..b80d612784 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -181,6 +181,7 @@ pub enum ScopeKind<'a> { AsyncFunction(&'a ast::StmtAsyncFunctionDef), Generator, Module, + Type, Lambda(&'a ast::ExprLambda), }