[ty] Skip eagerly evaluated scopes for attribute storing#20856
[ty] Skip eagerly evaluated scopes for attribute storing#20856carljm merged 8 commits intoastral-sh:mainfrom
Conversation
dc32d77 to
d263c50
Compare
Diagnostic diff on typing conformance testsNo changes detected when running ty on typing conformance tests ✅ |
|
d263c50 to
8d53802
Compare
| @@ -184,29 +184,34 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> { | |||
| self.current_scope_info().file_scope_id | |||
| } | |||
|
|
|||
| /// Returns the scope ID of the surrounding class body scope if the current scope | |||
| /// Returns the scope ID of the method scope if the current scope | |||
| /// is a method inside a class body. Returns `None` otherwise, e.g. if the current | |||
| /// scope is a function body outside of a class, or if the current scope is not a | |||
| /// function body. | |||
| fn is_method_of_class(&self) -> Option<FileScopeId> { | |||
There was a problem hiding this comment.
This function is also used at https://github.com/Glyphack/ruff/blob/8d53802bc7e9566a9087fd3e3a460345e40a7e6d/crates/ty_python_semantic/src/semantic_index/builder.rs#L1681 and I think this new behavior should be there as well.
I'm not happy with the name though, this now returns either method or an eager child scope of the class.
|
There is no ecosystem change detected by mypy primer? With the amount of custom logic added I expected to have more effect. Do you think I should simplify the logic even more since there's not much impact? Other solution could be only support children eager scopes and don't go into decendants. This would turn a lot of loops into if statements. |
I would expect the impact to be bigger when combined with type-of-
I don't really see a reason not to support that if you already implemented it correctly. |
|
Thank you for working on this? Could you please also add some tests with generic methods like described here? I'd like to see that we correctly skip the type params scope when detecting methods in classes. |
|
Thanks. I didn't think of the impact there, I was still hoping to reduce the false positives like this. So let's keep the current implementation. I went over the tests again added more examples of what is supported. |
7b7c842 to
aed27db
Compare
|
Just rebased this (and updated some tests) to get an updated ecosystem report. |
| If the comprehension is inside another scope like function then that attribute is not inferred. | ||
|
|
||
| ```py | ||
| class C: | ||
| def __init__(self): | ||
| def f(): | ||
| # error: [unresolved-attribute] | ||
| [... for self.a in [1]] | ||
|
|
||
| def g(): | ||
| # error: [unresolved-attribute] | ||
| [... for self.b in [1]] | ||
| g() | ||
|
|
||
| c_instance = C() | ||
|
|
||
| # This attribute is in the function f and is not reachable | ||
| # error: [unresolved-attribute] | ||
| reveal_type(c_instance.a) # revealed: Unknown | ||
|
|
||
| # TODO: Even though g method is called and is reachable we do not record this attribute assignment | ||
| # error: [unresolved-attribute] | ||
| reveal_type(c_instance.b) # revealed: Unknown | ||
| ``` |
There was a problem hiding this comment.
It's not totally clear to me if this is the right behavior or not. We can't guarantee that these attributes will ever be initialized. We can definitely say that they might be.
For reference, this behavior matches pyright. Mypy finds both attributes and gives them type Any. Pyrefly finds both attributes and gives them type int. No type checker attempts to distinguish these cases based on trying to determine whether the nested function is ever called; this is quite complex to do in general and I doubt we should attempt it either.
Overall I think the current behavior in this PR is fine and this case is probably rare (and it doesn't show up in the ecosystem report.) I don't think we need TODOs to revisit it; we'll revisit it only if there are real user reports.
There was a problem hiding this comment.
I haven't worked with call graph analysis and didn't think about it's complexity. If an issue was reported for this I would follow it up.
Co-authored-by: Carl Meyer <[email protected]>
Co-authored-by: Carl Meyer <[email protected]>
|
Thank you! |


Summary
Fix astral-sh/ty#664
This PR adds support for storing attributes in comprehension scopes (any eager scope.)
For example in the following code we infer type of
zcorrectly:The fix works by adjusting the following logics:
To identify if an attriute is an assignment to self or cls we need to check the scope is a method. To allow comprehension scopes here we skip any eager scope in the check.
Also at this stage the code checks if self or the first method argument is shadowed by another binding that eager scope to prevent this:
When determining scopes that attributes might be defined after collecting all the methods of the class the code also returns any decendant scope that is eager and only has eager parents until the method scope.
When checking reachability of a attribute definition if the attribute is defined in an eager scope we use the reachability of the first non eager scope which must be a method. This allows attributes to be marked as reachable and be seen.
There are also which I didn't add support for:
In the above example we will not even return the comprehension scope as an attribute scope because there is a non eager scope (
ffunction) between the comprehension and the__init__methodTest Plan