[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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 64 additions and 31 deletions

View file

@ -67,6 +67,6 @@ def _(flag: bool):
def __call__(self) -> int: ... def __call__(self) -> int: ...
a = NonCallable() a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)" # error: "Object of type `Literal[__call__] | Literal[1]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int 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" x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
``` ```
## Partial declarations ## Incompatible declarations for 2 (out of 3) types
```py ```py
def _(flag: bool): def _(flag1: bool, flag2: bool):
if flag: if flag1:
x: str
elif flag2:
x: int 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 ## Incompatible declarations with bad assignment
@ -42,3 +45,31 @@ def _(flag: bool):
# error: [invalid-assignment] # error: [invalid-assignment]
x = b"foo" 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
```

View file

@ -292,8 +292,8 @@ type DeclaredTypeResult<'db> = Result<Type<'db>, (Type<'db>, Box<[Type<'db>]>)>;
/// `Ok(declared_type)`. If there are conflicting declarations, returns /// `Ok(declared_type)`. If there are conflicting declarations, returns
/// `Err((union_of_declared_types, conflicting_declared_types))`. /// `Err((union_of_declared_types, conflicting_declared_types))`.
/// ///
/// If undeclared is a possibility, `undeclared_ty` type will be part of the return type (and may /// If undeclared is a possibility, `undeclared_ty` type will be part of the return type but it
/// conflict with other declarations.) /// will not be considered to be conflicting with any other types.
/// ///
/// # Panics /// # Panics
/// Will panic if there are no declarations and no `undeclared_ty` is provided. This is a logic /// Will panic if there are no declarations and no `undeclared_ty` is provided. This is a logic
@ -304,27 +304,31 @@ fn declarations_ty<'db>(
declarations: DeclarationsIterator<'_, 'db>, declarations: DeclarationsIterator<'_, 'db>,
undeclared_ty: Option<Type<'db>>, undeclared_ty: Option<Type<'db>>,
) -> DeclaredTypeResult<'db> { ) -> DeclaredTypeResult<'db> {
let decl_types = declarations.map(|declaration| declaration_ty(db, declaration)); let mut declaration_types = declarations.map(|declaration| declaration_ty(db, declaration));
let mut all_types = undeclared_ty.into_iter().chain(decl_types); let Some(first) = declaration_types.next() else {
if let Some(undeclared_ty) = undeclared_ty {
let first = all_types.next().expect( // Short-circuit to return the undeclared type if there are no declarations.
"declarations_ty must not be called with zero declarations and no may-be-undeclared", return Ok(undeclared_ty);
); }
panic!("declarations_ty must not be called with zero declarations and no undeclared_ty");
};
let mut conflicting: Vec<Type<'db>> = vec![]; let mut conflicting: Vec<Type<'db>> = vec![];
let declared_ty = if let Some(second) = all_types.next() {
let mut builder = UnionBuilder::new(db).add(first); let mut builder = UnionBuilder::new(db).add(first);
for other in [second].into_iter().chain(all_types) { for other in declaration_types {
if !first.is_equivalent_to(db, other) { if !first.is_equivalent_to(db, other) {
conflicting.push(other); conflicting.push(other);
} }
builder = builder.add(other); builder = builder.add(other);
} }
builder.build() // Avoid considering the undeclared type for the conflicting declaration diagnostics. It
} else { // should still be part of the declared type.
first if let Some(undeclared_ty) = undeclared_ty {
}; builder = builder.add(undeclared_ty);
}
let declared_ty = builder.build();
if conflicting.is_empty() { if conflicting.is_empty() {
Ok(declared_ty) Ok(declared_ty)
} else { } else {
@ -447,6 +451,10 @@ pub enum Type<'db> {
} }
impl<'db> Type<'db> { impl<'db> Type<'db> {
pub const fn is_unknown(&self) -> bool {
matches!(self, Type::Unknown)
}
pub const fn is_never(&self) -> bool { pub const fn is_never(&self) -> bool {
matches!(self, Type::Never) matches!(self, Type::Never)
} }

View file

@ -31,15 +31,10 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:364:9 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:381:13 Conflicting declared types for `char`: Unknown, str | None",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:395:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
@ -47,7 +42,6 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
"error[lint:conflicting-declarations] /src/tomllib/_parser.py:590:9 Conflicting declared types for `char`: Unknown, str | None",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
]; ];