ruff
ruff copied to clipboard
[red-knot] model that class/comprehension bodies run immediately
Currently, we have a general strategy for name lookups in enclosing scopes: they use the type as seen from end of the scope where it's defined. For example:
x = 1
def f():
# This is correct for any call to `f` that would happen after this module has
# fully executed. It could be wrong if `f` is called during execution of the
# module body.
reveal_type(x) # revealed: Literal["foo"]
x = "foo"
For nested functions, this is a pretty reasonable default behavior. Another option would be to consider all definitions of x (or at least all definitions of x visible from the point where f is defined, forwards). This would result in revealing Literal[1] | Literal["foo"] for x in the inner scope of f, instead. This would be safer, but also more annoying for the more common case where f is not called from the module body. It would also mean we would always consider x possibly-undefined inside f, if f is defined before x, which could be really annoying.
We could try to be smarter by detecting, for example, that the function f is (or might be) called from module level, and only consider earlier definitions if we observe this possibility. But this will always be best-effort, since reliably detecting all possible paths (other functions, even possibly in other modules) through which f could be called from module level isn't feasible.
But there are cases where we can be quite sure that the nested scope will run before the end of the outer scope. For example, class definitions:
x = 1
class C:
y = x
x = "foo"
# This is currently what we reveal, but it's clearly wrong: should be `Literal[1]`
reveal_type(C.y) # revealed: Literal["foo"]
The class body always runs immediately, so it should resolve types in the enclosing scope from the class definition's point in control flow, not from end-of-scope.
Should we do the same for list/dict/set comprehensions? (Generators expressions obviously do not run immediately, unlike list/dict/set comprehensions.)
Yes!
See https://github.com/astral-sh/ruff/issues/13879 for discussion of generator expressions as it relates to this task.
https://github.com/astral-sh/ruff/pull/16079
@dcreager is this complete now?
I think it is.
I'm looking at how we're representing the bindings snapshots next, to see if there's any performance we can squeeze out of it. But I'm :+1: to closing this since the functionality is there and working.