mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:35:58 +00:00
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:
parent
91e2d9a139
commit
3e702e12f7
2 changed files with 77 additions and 33 deletions
|
@ -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
|
||||||
|
```
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue