[red-knot] Avoid undeclared path when raising conflicting declarations (#14958)

## Summary

This PR updates the logic when raising conflicting declarations
diagnostic to avoid the undeclared path if present.

The conflicting declaration diagnostics is added when there are two or
more declarations in the control flow path of a definition whose type
isn't equivalent to each other. This can be seen in the following
example:

```py
if flag:
	x: int
x = 1  # conflicting-declarations: Unknown, int
```

After this PR, we'd avoid considering "Unknown" as part of the
conflicting declarations. This means we'd still flag it for the
following case:

```py
if flag:
	x: int
else:
	x: str
x = 1  # conflicting-declarations: int, str
```

A solution that's local to the exception control flow was also explored
which required updating the logic for merging the flow snapshot to avoid
considering declarations using a flag. This is preserved here:
https://github.com/astral-sh/ruff/compare/dhruv/control-flow-no-declarations?expand=1.

The main motivation to avoid that is we don't really understand what the
user experience is w.r.t. the Unknown type and the
conflicting-declaration diagnostics. This makes us unsure on what the
right semantics are as to whether that diagnostics should be raised or
not and when to raise them. For now, we've decided to move forward with
this PR and could decide to adopt another solution or remove the
conflicting-declaration diagnostics in the future.

Closes: #13966 

## Test Plan

Update the existing mdtest case. Add an additional case specific to
exception control flow to verify that the diagnostic is not being raised
now.
This commit is contained in:
Dhruv Manilawala
2024-12-17 09:49:39 +05:30
committed by GitHub
parent 4ddf9228f6
commit dcdc6e7c64
4 changed files with 64 additions and 31 deletions

View File

@@ -67,6 +67,6 @@ def _(flag: bool):
def __call__(self) -> int: ...
a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
# error: "Object of type `Literal[__call__] | Literal[1]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: int | Unknown
```

View File

@@ -19,14 +19,17 @@ def _(flag: bool):
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
```
## Partial declarations
## Incompatible declarations for 2 (out of 3) types
```py
def _(flag: bool):
if flag:
def _(flag1: bool, flag2: bool):
if flag1:
x: str
elif flag2:
x: int
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: Unknown, int"
# Here, the declared type for `x` is `int | str | Unknown`.
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
```
## Incompatible declarations with bad assignment
@@ -42,3 +45,31 @@ def _(flag: bool):
# error: [invalid-assignment]
x = b"foo"
```
## No errors
Currently, we avoid raising the conflicting-declarations for the following cases:
### Partial declarations
```py
def _(flag: bool):
if flag:
x: int
x = 1
```
### Partial declarations in try-except
Refer to <https://github.com/astral-sh/ruff/issues/13966>
```py
def _():
try:
x: int = 1
except:
x = 2
x = 3
```