mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 12:05:57 +00:00 
			
		
		
		
	 b01003f81d
			
		
	
	
		b01003f81d
		
			
		
	
	
	
	
		
			
			## Summary
This PR includes a behavioral change to how we infer types for public
uses of symbols within a module. Where we would previously use the type
that a use at the end of the scope would see, we now consider all
reachable bindings and union the results:
```py
x = None
def f():
    reveal_type(x)  # previously `Unknown | Literal[1]`, now `Unknown | None | Literal[1]`
f()
x = 1
f()
```
This helps especially in cases where the the end of the scope is not
reachable:
```py
def outer(x: int):
    def inner():
        reveal_type(x)  # previously `Unknown`, now `int`
    raise ValueError
```
This PR also proposes to skip the boundness analysis of public uses.
This is consistent with the "all reachable bindings" strategy, because
the implicit `x = <unbound>` binding is also always reachable, and we
would have to emit "possibly-unresolved" diagnostics for every public
use otherwise. Changing this behavior allows common use-cases like the
following to type check without any errors:
```py
def outer(flag: bool):
    if flag:
        x = 1
        def inner():
            print(x)  # previously: possibly-unresolved-reference, now: no error
```
closes https://github.com/astral-sh/ty/issues/210
closes https://github.com/astral-sh/ty/issues/607
closes https://github.com/astral-sh/ty/issues/699
## Follow up
It is now possible to resolve the following TODO, but I would like to do
that as a follow-up, because it requires some changes to how we treat
implicit attribute assignments, which could result in ecosystem changes
that I'd like to see separately.
315fb0f3da/crates/ty_python_semantic/src/semantic_index/builder.rs (L1095-L1117)
## Ecosystem analysis
[**Full report**](https://shark.fish/diff-public-types.html)
* This change obviously removes a lot of `possibly-unresolved-reference`
diagnostics (7818) because we do not analyze boundness for public uses
of symbols inside modules anymore.
* As the primary goal here, this change also removes a lot of
false-positive `unresolved-reference` diagnostics (231) in scenarios
like this:
    ```py
    def _(flag: bool):
        if flag:
            x = 1
    
            def inner():
                x
    
            raise
    ```
* This change also introduces some new false positives for cases like:
    ```py
    def _():
        x = None
    
        x = "test"
    
        def inner():
x.upper() # Attribute `upper` on type `Unknown | None | Literal["test"]`
is possibly unbound
    ```
We have test cases for these situations and it's plausible that we can
improve this in a follow-up.
## Test Plan
New Markdown tests
		
	
			
		
			
				
	
	
	
	
		
			1,019 B
		
	
	
	
	
	
	
	
			
		
		
	
	
			1,019 B
		
	
	
	
	
	
	
	
Builtin scope
Conditional local override of builtin
If a builtin name is conditionally shadowed by a local variable, a name lookup should union the builtin type with the conditionally-defined type:
def _(flag: bool) -> None:
    if flag:
        abs = 1
        chr: int = 1
    reveal_type(abs)  # revealed: Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T)
    reveal_type(chr)  # revealed: Literal[1] | (def chr(i: SupportsIndex, /) -> str)
Conditionally global override of builtin
If a builtin name is conditionally shadowed by a global variable, a name lookup should union the builtin type with the conditionally-defined type:
def flag() -> bool:
    return True
if flag():
    abs = 1
    chr: int = 1
def _():
    # TODO: Should ideally be `Unknown | Literal[1] | (def abs(x: SupportsAbs[_T], /) -> _T)`
    reveal_type(abs)  # revealed: Unknown | Literal[1]
    # TODO: Should ideally be `int | (def chr(i: SupportsIndex, /) -> str)`
    reveal_type(chr)  # revealed: int