Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class name_1[name_2: name_2[0]]:
def name_4(name_3: name_2, /):
if name_3:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class name_1[name_2: (name_2[0], int)]:
def name_4(name_3: name_2, /):
if name_3:
pass
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,39 @@ def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3:
# error: [unsupported-bool-conversion]
10 and get() and True
```

## Constrained TypeVar has >=1 constraint that doesn't support boolean conversion

```toml
[environment]
python-version = "3.12"
```

```py
class NotBoolable:
__bool__ = None

def f[T: (int, NotBoolable)](x: T) -> T:
# error: [unsupported-bool-conversion]
if x:
pass
return x
```

## TypeVar has an upper bound that doesn't support boolean conversion

```toml
[environment]
python-version = "3.12"
```

```py
class NotBoolable:
__bool__ = None

def f[T: NotBoolable](x: T) -> T:
# error: [unsupported-bool-conversion]
if x:
pass
return x
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - Constrained TypeVar has >=1 constraint that doesn't support boolean conversion
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md
---

# Python source files

## mdtest_snippet.py

```
1 | class NotBoolable:
2 | __bool__ = None
3 |
4 | def f[T: (int, NotBoolable)](x: T) -> T:
5 | # error: [unsupported-bool-conversion]
6 | if x:
7 | pass
8 | return x
```

# Diagnostics

```
error[unsupported-bool-conversion]: Boolean conversion is unsupported for type variable `T@f` because constraint `NotBoolable` doesn't implement `__bool__` correctly
--> src/mdtest_snippet.py:6:8
|
4 | def f[T: (int, NotBoolable)](x: T) -> T:
5 | # error: [unsupported-bool-conversion]
6 | if x:
| ^
7 | pass
8 | return x
|
info: rule `unsupported-bool-conversion` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: unsupported_bool_conversion.md - Different ways that `unsupported-bool-conversion` can occur - TypeVar has an upper bound that doesn't support boolean conversion
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_bool_conversion.md
---

# Python source files

## mdtest_snippet.py

```
1 | class NotBoolable:
2 | __bool__ = None
3 |
4 | def f[T: NotBoolable](x: T) -> T:
5 | # error: [unsupported-bool-conversion]
6 | if x:
7 | pass
8 | return x
```

# Diagnostics

```
error[unsupported-bool-conversion]: Boolean conversion is unsupported for type variable `T@f` because upper bound `NotBoolable` doesn't implement `__bool__` correctly
--> src/mdtest_snippet.py:6:8
|
4 | def f[T: NotBoolable](x: T) -> T:
5 | # error: [unsupported-bool-conversion]
6 | if x:
| ^
7 | pass
8 | return x
|
info: rule `unsupported-bool-conversion` is enabled by default

```
89 changes: 73 additions & 16 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5307,12 +5307,19 @@ impl<'db> Type<'db> {
}
};

let try_union = |union: UnionType<'db>| {
let try_union = |union_like: Either<UnionType<'db>, BoundTypeVarInstance<'db>>| {
let mut truthiness = None;
let mut all_not_callable = true;
let mut has_errors = false;
let union_elements = match union_like {
Either::Left(union) => union.elements(db),
Either::Right(tvar) => tvar
.typevar(db)
.constraints(db)
.expect("Should only call `try_union` on a constrained TypeVar"),
};

for element in union.elements(db) {
for element in union_elements {
let element_truthiness =
match element.try_bool_impl(db, allow_short_circuit, visitor) {
Ok(truthiness) => truthiness,
Expand Down Expand Up @@ -5340,9 +5347,13 @@ impl<'db> Type<'db> {
not_boolable_type: *self,
});
}
return Err(BoolError::Union {
union,
truthiness: truthiness.unwrap_or(Truthiness::Ambiguous),
let truthiness = truthiness.unwrap_or(Truthiness::Ambiguous);
return Err(match union_like {
Either::Left(union) => BoolError::Union { union, truthiness },
Either::Right(typevar) => BoolError::ConstrainedTypevar {
typevar,
truthiness,
},
});
}
Ok(truthiness.unwrap_or(Truthiness::Ambiguous))
Expand Down Expand Up @@ -5404,12 +5415,17 @@ impl<'db> Type<'db> {
Type::TypeVar(bound_typevar) => {
match bound_typevar.typevar(db).bound_or_constraints(db) {
None => Truthiness::Ambiguous,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.try_bool_impl(db, allow_short_circuit, visitor)?
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => visitor
.visit(*self, || {
bound.try_bool_impl(db, allow_short_circuit, visitor)
})
.map_err(|err| BoolError::BoundTypeVar {
typevar: *bound_typevar,
truthiness: err.fallback_truthiness(),
})?,
Some(TypeVarBoundOrConstraints::Constraints(_)) => {
visitor.visit(*self, || try_union(Either::Right(*bound_typevar)))?
}
Comment on lines +5426 to 5428
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I first naively tried doing this

Suggested change
Some(TypeVarBoundOrConstraints::Constraints(_)) => {
visitor.visit(*self, || try_union(Either::Right(*bound_typevar)))?
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
visitor.visit(*self, || try_union(constraints.as_type(db)))?
}

but that's no good -- the TypeVarConstraints::as_type() call itself causes a stack overflow if one of the TypeVar constraints is recursive. So I had to refactor try_union so that it could take a UnionType or a TypeVar.

Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.as_type(db)
.try_bool_impl(db, allow_short_circuit, visitor)?,
}
}

Expand All @@ -5421,7 +5437,7 @@ impl<'db> Type<'db> {

Type::ProtocolInstance(_) => try_dunders()?,

Type::Union(union) => try_union(*union)?,
Type::Union(union) => try_union(Either::Left(*union))?,

Type::Intersection(_) => {
// TODO
Expand Down Expand Up @@ -10320,11 +10336,7 @@ fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>(

impl<'db> TypeVarConstraints<'db> {
fn as_type(self, db: &'db dyn Db) -> Type<'db> {
let mut builder = UnionBuilder::new(db);
for ty in self.elements(db) {
builder = builder.add(*ty);
}
builder.build()
UnionType::from_elements(db, self.elements(db))
}

fn to_instance(self, db: &'db dyn Db) -> Option<TypeVarConstraints<'db>> {
Expand Down Expand Up @@ -11451,6 +11463,18 @@ pub(super) enum BoolError<'db> {
truthiness: Truthiness,
},

/// A typevar has a bound that doesn't implement `__bool__` correctly.
BoundTypeVar {
typevar: BoundTypeVarInstance<'db>,
truthiness: Truthiness,
},

/// A typevar has constraints, and >=1 constraint doesn't implement `__bool__` correctly.
ConstrainedTypevar {
typevar: BoundTypeVarInstance<'db>,
truthiness: Truthiness,
},

/// Any other reason why the type can't be converted to a bool.
/// E.g. because calling `__bool__` returns in a union type and not all variants support `__bool__` or
/// because `__bool__` points to a type that has a possibly missing `__call__` method.
Expand All @@ -11464,6 +11488,8 @@ impl<'db> BoolError<'db> {
| BoolError::IncorrectReturnType { .. }
| BoolError::Other { .. } => Truthiness::Ambiguous,
BoolError::IncorrectArguments { truthiness, .. }
| BoolError::ConstrainedTypevar { truthiness, .. }
| BoolError::BoundTypeVar { truthiness, .. }
| BoolError::Union { truthiness, .. } => *truthiness,
}
}
Expand All @@ -11481,6 +11507,8 @@ impl<'db> BoolError<'db> {
not_boolable_type, ..
} => *not_boolable_type,
BoolError::Union { union, .. } => Type::Union(*union),
BoolError::ConstrainedTypevar { typevar, .. }
| BoolError::BoundTypeVar { typevar, .. } => Type::TypeVar(*typevar),
}
}

Expand Down Expand Up @@ -11577,6 +11605,35 @@ impl<'db> BoolError<'db> {
first_error.not_boolable_type().display(context.db()),
));
}
Self::ConstrainedTypevar { typevar, .. } => {
let first_error = typevar
.typevar(context.db())
.constraints(context.db())
.into_iter()
.flatten()
.find_map(|constraint| constraint.try_bool(context.db()).err())
.unwrap();

builder.into_diagnostic(format_args!(
"Boolean conversion is unsupported for type variable `{}` \
because constraint `{}` doesn't implement `__bool__` correctly",
Type::TypeVar(*typevar).display(context.db()),
first_error.not_boolable_type().display(context.db()),
));
}
Self::BoundTypeVar { typevar, .. } => {
let bound = typevar
.typevar(context.db())
.upper_bound(context.db())
.unwrap();

builder.into_diagnostic(format_args!(
"Boolean conversion is unsupported for type variable `{}` \
because upper bound `{}` doesn't implement `__bool__` correctly",
Type::TypeVar(*typevar).display(context.db()),
bound.display(context.db()),
));
}

Self::Other { not_boolable_type } => {
builder.into_diagnostic(format_args!(
Expand Down
Loading