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 

<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
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

<!-- How was it tested? -->
Undefined name snapshots.

Unused type parameter rule will be added as follow-up.
This commit is contained in:
Zanie Blue 2023-07-28 17:06:37 -05:00 committed by GitHub
parent 134d447d4c
commit 047c211837
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 405 additions and 18 deletions

View file

@ -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]