From 3a542a80f65d66a15e22b6fe161e5af05ddffa83 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 8 Aug 2025 17:01:17 -0400 Subject: [PATCH] [ty] Handle cycles when finding implicit attributes (#19833) The [minimal reproduction](https://gist.github.com/dcreager/fc53c59b30d7ce71d478dcb2c1c56444) of https://github.com/astral-sh/ty/issues/948 is an example of a class with implicit attributes whose types end up depending on themselves. Our existing cycle detection for `infer_expression_types` is usually enough to handle this situation correctly, but when there are very many of these implicit attributes, we get a combinatorial explosion of running time and memory usage. Adding a separate cycle handler for `ClassLiteral::implicit_attribute` lets us catch and recover from this situation earlier. Closes https://github.com/astral-sh/ty/issues/948 --- crates/ty_python_semantic/src/types/class.rs | 43 +++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index f8a32dff66..3660398e70 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -89,6 +89,26 @@ fn inheritance_cycle_initial<'db>( None } +fn implicit_attribute_recover<'db>( + _db: &'db dyn Db, + _value: &PlaceAndQualifiers<'db>, + _count: u32, + _class_body_scope: ScopeId<'db>, + _name: String, + _target_method_decorator: MethodDecorator, +) -> salsa::CycleRecoveryAction> { + salsa::CycleRecoveryAction::Iterate +} + +fn implicit_attribute_initial<'db>( + _db: &'db dyn Db, + _class_body_scope: ScopeId<'db>, + _name: String, + _target_method_decorator: MethodDecorator, +) -> PlaceAndQualifiers<'db> { + Place::Unbound.into() +} + fn try_mro_cycle_recover<'db>( _db: &'db dyn Db, _value: &Result, MroError<'db>>, @@ -2359,6 +2379,25 @@ impl<'db> ClassLiteral<'db> { class_body_scope: ScopeId<'db>, name: &str, target_method_decorator: MethodDecorator, + ) -> PlaceAndQualifiers<'db> { + Self::implicit_attribute_inner( + db, + class_body_scope, + name.to_string(), + target_method_decorator, + ) + } + + #[salsa::tracked( + cycle_fn=implicit_attribute_recover, + cycle_initial=implicit_attribute_initial, + heap_size=ruff_memory_usage::heap_size, + )] + fn implicit_attribute_inner( + db: &'db dyn Db, + class_body_scope: ScopeId<'db>, + name: String, + target_method_decorator: MethodDecorator, ) -> PlaceAndQualifiers<'db> { // If we do not see any declarations of an attribute, neither in the class body nor in // any method, we build a union of `Unknown` with the inferred types of all bindings of @@ -2392,7 +2431,7 @@ impl<'db> ClassLiteral<'db> { // First check declarations for (attribute_declarations, method_scope_id) in - attribute_declarations(db, class_body_scope, name) + attribute_declarations(db, class_body_scope, &name) { let method_scope = method_scope_id.to_scope_id(db, file); if !is_valid_scope(method_scope) { @@ -2450,7 +2489,7 @@ impl<'db> ClassLiteral<'db> { } for (attribute_assignments, method_scope_id) in - attribute_assignments(db, class_body_scope, name) + attribute_assignments(db, class_body_scope, &name) { let method_scope = method_scope_id.to_scope_id(db, file); if !is_valid_scope(method_scope) {