red-knot: support narrowing for bool(E) (#14668)

Resolves https://github.com/astral-sh/ruff/issues/14547 by delegating
narrowing to `E` for `bool(E)` where `E` is some expression.

This change does not include other builtin class constructors which
should also work in this position, like `int(..)` or `float(..)`, as the
original issue does not mention these. It should be easy enough to add
checks for these as well if we want to.

I don't see a lot of markdown tests for malformed input, maybe there's a
better place for the no args and too many args cases to go?

I did see after the fact that it looks like this task was intended for a
new hire.. my apologies. I got here from
https://github.com/astral-sh/ruff/issues/13694, which is marked
help-wanted.

---------

Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
Connor Skees 2024-12-02 22:04:59 -05:00 committed by GitHub
parent 91e2d9a139
commit 3e702e12f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 77 additions and 33 deletions

View file

@ -0,0 +1,32 @@
## Narrowing for `bool(..)` checks
```py
def flag() -> bool: ...
x = 1 if flag() else None
# valid invocation, positive
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None):
reveal_type(x) # revealed: Literal[1]
# valid invocation, negative
reveal_type(x) # revealed: Literal[1] | None
if not bool(x is not None):
reveal_type(x) # revealed: None
# no args/narrowing
reveal_type(x) # revealed: Literal[1] | None
if not bool():
reveal_type(x) # revealed: Literal[1] | None
# invalid invocation, too many positional args
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, 5): # TODO diagnostic
reveal_type(x) # revealed: Literal[1] | None
# invalid invocation, too many kwargs
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, y=5): # TODO diagnostic
reveal_type(x) # revealed: Literal[1] | None
```

View file

@ -388,18 +388,23 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
let scope = self.scope(); let scope = self.scope();
let inference = infer_expression_types(self.db, expression); let inference = infer_expression_types(self.db, expression);
let callable_ty =
inference.expression_ty(expr_call.func.scoped_expression_id(self.db, scope));
// TODO: add support for PEP 604 union types on the right hand side of `isinstance` // TODO: add support for PEP 604 union types on the right hand side of `isinstance`
// and `issubclass`, for example `isinstance(x, str | (int | float))`. // and `issubclass`, for example `isinstance(x, str | (int | float))`.
match inference match callable_ty {
.expression_ty(expr_call.func.scoped_expression_id(self.db, scope)) Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => {
.into_function_literal() let function = function_type
.and_then(|f| f.known(self.db)) .known(self.db)
.and_then(KnownFunction::constraint_function) .and_then(KnownFunction::constraint_function)?;
{
Some(function) if expr_call.arguments.keywords.is_empty() => { let [ast::Expr::Name(ast::ExprName { id, .. }), class_info] =
if let [ast::Expr::Name(ast::ExprName { id, .. }), class_info] =
&*expr_call.arguments.args &*expr_call.arguments.args
{ else {
return None;
};
let symbol = self.symbols().symbol_id_by_name(id).unwrap(); let symbol = self.symbols().symbol_id_by_name(id).unwrap();
let class_info_ty = let class_info_ty =
@ -407,9 +412,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
let to_constraint = match function { let to_constraint = match function {
KnownConstraintFunction::IsInstance => { KnownConstraintFunction::IsInstance => {
|class_literal: ClassLiteralType<'db>| { |class_literal: ClassLiteralType<'db>| Type::instance(class_literal.class)
Type::instance(class_literal.class)
}
} }
KnownConstraintFunction::IsSubclass => { KnownConstraintFunction::IsSubclass => {
|class_literal: ClassLiteralType<'db>| { |class_literal: ClassLiteralType<'db>| {
@ -425,9 +428,18 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
constraints constraints
}, },
) )
} else {
None
} }
// for the expression `bool(E)`, we further narrow the type based on `E`
Type::ClassLiteral(class_type)
if expr_call.arguments.args.len() == 1
&& expr_call.arguments.keywords.is_empty()
&& class_type.class.is_known(self.db, KnownClass::Bool) =>
{
self.evaluate_expression_node_constraint(
&expr_call.arguments.args[0],
expression,
is_positive,
)
} }
_ => None, _ => None,
} }