mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-15 16:10:17 +00:00
[ty] Delay computation of 'unbound' visibility for implicit instance attributes (#18669)
## Summary Consider the following example, which leads to a excessively large runtime on `main`. The reason for this is the following. When inferring types for `self.a`, we look up the `a` attribute on `C`. While looking for implicit instance attributes, we go through every method and check for `self.a = …` assignments. There are no such assignments here, but we always have an implicit `self.a = <unbound>` binding at the beginning over every method. This binding accumulates a complex visibility constraint in `C.f`, due to the `isinstance` checks. While evaluating that constraint, we need to infer the type of `self.b`. There's no binding for `self.b` either, but there's also an implicit `self.b = <unbound>` binding with the same complex visibility constraint (involving `self.b` recursively). This leads to a combinatorial explosion: ```py class C: def f(self: "C"): if isinstance(self.a, str): return if isinstance(self.b, str): return if isinstance(self.b, str): return if isinstance(self.b, str): return # repeat 20 times ``` (note that the `self` parameter here is annotated explicitly because we currently still infer `Unknown` for `self` otherwise) The fix proposed here is rather simple: when there are no `self.name = …` attribute assignments in a given method, we skip evaluating the visibility constraint of the implicit `self.name = <unbound>` binding. This should also generally help with performance, because that's a very common case. This is *not* a fix for cases where there *are* actual bindings in the method. When we add `self.a = 1; self.b = 1` to that example above, we still see that combinatorial explosion of runtime. I still think it's worth to make this optimization, as it fixes the problems with `pandas` and `sqlalchemy` reported by users. I will open a ticket to track that separately. closes https://github.com/astral-sh/ty/issues/627 closes https://github.com/astral-sh/ty/issues/641 ## Test Plan * Made sure that `ty` finishes quickly on the MREs in https://github.com/astral-sh/ty/issues/627 * Made sure that `ty` finishes quickly on `pandas` * Made sure that `ty` finishes quickly on `sqlalchemy`
This commit is contained in:
parent
1889a5e6eb
commit
89d915a1e3
2 changed files with 18 additions and 15 deletions
|
@ -109,10 +109,6 @@ impl<'db> DefinitionState<'db> {
|
|||
|| matches!(self, DefinitionState::Defined(def) if f(def))
|
||||
}
|
||||
|
||||
pub(crate) fn is_undefined(self) -> bool {
|
||||
matches!(self, DefinitionState::Undefined)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn definition(self) -> Option<Definition<'db>> {
|
||||
match self {
|
||||
|
|
|
@ -1640,19 +1640,21 @@ impl<'db> ClassLiteral<'db> {
|
|||
continue;
|
||||
}
|
||||
|
||||
let mut attribute_assignments = attribute_assignments.peekable();
|
||||
let unbound_visibility = attribute_assignments
|
||||
.peek()
|
||||
.map(|attribute_assignment| {
|
||||
if attribute_assignment.binding.is_undefined() {
|
||||
method_map.is_binding_visible(db, attribute_assignment)
|
||||
} else {
|
||||
Truthiness::AlwaysFalse
|
||||
}
|
||||
})
|
||||
.unwrap_or(Truthiness::AlwaysFalse);
|
||||
// Storage for the implicit `DefinitionState::Undefined` binding. If present, it
|
||||
// will be the first binding in the `attribute_assignments` iterator.
|
||||
let mut unbound_binding = None;
|
||||
|
||||
for attribute_assignment in attribute_assignments {
|
||||
if let DefinitionState::Undefined = attribute_assignment.binding {
|
||||
// Store the implicit unbound binding here so that we can delay the
|
||||
// computation of `unbound_visibility` to the point when we actually
|
||||
// need it. This is an optimization for the common case where the
|
||||
// `unbound` binding is the only binding of the `name` attribute,
|
||||
// i.e. if there is no `self.name = …` assignment in this method.
|
||||
unbound_binding = Some(attribute_assignment);
|
||||
continue;
|
||||
}
|
||||
|
||||
let DefinitionState::Defined(binding) = attribute_assignment.binding else {
|
||||
continue;
|
||||
};
|
||||
|
@ -1676,6 +1678,11 @@ impl<'db> ClassLiteral<'db> {
|
|||
// There is at least one attribute assignment that may be visible,
|
||||
// so if `unbound_visibility` is always false then this attribute is considered bound.
|
||||
// TODO: this is incomplete logic since the attributes bound after termination are considered visible.
|
||||
let unbound_visibility = unbound_binding
|
||||
.as_ref()
|
||||
.map(|binding| method_map.is_binding_visible(db, binding))
|
||||
.unwrap_or(Truthiness::AlwaysFalse);
|
||||
|
||||
if unbound_visibility
|
||||
.negate()
|
||||
.and(is_method_visible)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue