diff --git a/crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py b/crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py new file mode 100644 index 0000000000..055f2c1721 --- /dev/null +++ b/crates/red_knot_project/resources/test/corpus/88_regression_generic_method_with_nested_function.py @@ -0,0 +1,9 @@ +# Regression test for an issue that came up while working +# on https://github.com/astral-sh/ruff/pull/17769 + +class C: + def method[T](self, x: T) -> T: + def inner(): + self.attr = 1 + +C().attr diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 5077b0eb8f..17079b86b8 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1949,6 +1949,27 @@ reveal_type(C.a_type) # revealed: type reveal_type(C.a_none) # revealed: None ``` +### Generic methods + +We also detect implicit instance attributes on methods that are themselves generic. We have an extra +test for this because generic functions have an extra type-params scope in between the function body +scope and the outer scope, so we need to make sure that our implementation can still recognize `f` +as a method of `C` here: + +```toml +[environment] +python-version = "3.12" +``` + +```py +class C: + def f[T](self, t: T) -> T: + self.x: int = 1 + return t + +reveal_type(C().x) # revealed: int +``` + ## Enum classes Enums are not supported yet; attribute access on an enum class is inferred as `Todo`. diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/red_knot_python_semantic/src/semantic_index.rs index 619172faef..11137f3eeb 100644 --- a/crates/red_knot_python_semantic/src/semantic_index.rs +++ b/crates/red_knot_python_semantic/src/semantic_index.rs @@ -18,7 +18,8 @@ use crate::semantic_index::builder::SemanticIndexBuilder; use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions}; use crate::semantic_index::expression::Expression; use crate::semantic_index::symbol::{ - FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolTable, + FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId, + SymbolTable, }; use crate::semantic_index::use_def::{EagerBindingsKey, ScopedEagerBindingsId, UseDefMap}; use crate::Db; @@ -109,12 +110,26 @@ pub(crate) fn attribute_assignments<'db, 's>( let index = semantic_index(db, file); let class_scope_id = class_body_scope.file_scope_id(db); - ChildrenIter::new(index, class_scope_id).filter_map(|(file_scope_id, maybe_method)| { - maybe_method.node().as_function()?; - let attribute_table = index.instance_attribute_table(file_scope_id); + ChildrenIter::new(index, class_scope_id).filter_map(|(child_scope_id, scope)| { + let (function_scope_id, function_scope) = + if scope.node().scope_kind() == ScopeKind::Annotation { + // This could be a generic method with a type-params scope. + // Go one level deeper to find the function scope. The first + // descendant is the (potential) function scope. + let function_scope_id = scope.descendants().start; + (function_scope_id, index.scope(function_scope_id)) + } else { + (child_scope_id, scope) + }; + + function_scope.node().as_function()?; + let attribute_table = index.instance_attribute_table(function_scope_id); let symbol = attribute_table.symbol_id_by_name(name)?; - let use_def = &index.use_def_maps[file_scope_id]; - Some((use_def.instance_attribute_bindings(symbol), file_scope_id)) + let use_def = &index.use_def_maps[function_scope_id]; + Some(( + use_def.instance_attribute_bindings(symbol), + function_scope_id, + )) }) } diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index 75c7494b43..6426e5cc0f 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -183,13 +183,26 @@ impl<'db> SemanticIndexBuilder<'db> { fn is_method_of_class(&self) -> Option { let mut scopes_rev = self.scope_stack.iter().rev(); let current = scopes_rev.next()?; + + if self.scopes[current.file_scope_id].kind() != ScopeKind::Function { + return None; + } + let parent = scopes_rev.next()?; - match ( - self.scopes[current.file_scope_id].kind(), - self.scopes[parent.file_scope_id].kind(), - ) { - (ScopeKind::Function, ScopeKind::Class) => Some(parent.file_scope_id), + match self.scopes[parent.file_scope_id].kind() { + ScopeKind::Class => Some(parent.file_scope_id), + ScopeKind::Annotation => { + // If the function is generic, the parent scope is an annotation scope. + // In this case, we need to go up one level higher to find the class scope. + let grandparent = scopes_rev.next()?; + + if self.scopes[grandparent.file_scope_id].kind() == ScopeKind::Class { + Some(grandparent.file_scope_id) + } else { + None + } + } _ => None, } }