Repository: astral-sh/ruff
File: crates/ty_python_semantic/src/types/narrow.rs (Rust)

Code before:
            {
                let return_ty = inference.expression_type(expr_call);

                let (guarded_ty, place) = match return_ty {
                    // TODO: TypeGuard
                    Type::TypeIs(type_is) => {
                        let (_, place) = type_is.place_info(self.db)?;
                        (type_is.return_type(self.db), place)
                    }
                    _ => return None,
                };

                Some(NarrowingConstraints::from_iter([(
                    place,
                    guarded_ty.negate_if(self.db, !is_positive),
                )]))
            }
            // for the expression `len(E)`, we narrow the type based on whether len(E) is truthy
            // (i.e., whether E is non-empty)
            Type::FunctionLiteral(function_type)
                if expr_call.arguments.args.len() == 1
                    && expr_call.arguments.keywords.is_empty()
                    && function_type.known(self.db) == Some(KnownFunction::Len) =>
            {
                self.evaluate_simple_expr(&expr_call.arguments.args[0], is_positive)
            }
            Type::FunctionLiteral(function_type) if expr_call.arguments.keywords.is_empty() => {
                let [first_arg, second_arg] = &*expr_call.arguments.args else {
                    return None;
                };
                let first_arg = place_expr(first_arg)?;
                let function = function_type.known(self.db)?;
                let place = self.expect_place(&first_arg);

                if function == KnownFunction::HasAttr {
                    let attr = inference
                        .expression_type(second_arg)
                        .as_string_literal()?
                        .value(self.db);

                    if !is_identifier(attr) {
                        return None;
                    }

                    // Since `hasattr` only checks if an attribute is readable,
                    // the type of the protocol member should be a read-only property that returns `object`.
                    let constraint =
                        Type::protocol_with_readonly_members(self.db, [(attr, Type::object())]);

                    return Some(NarrowingConstraints::from_iter([(
                        place,

Review comment (bug):
this introduces an unsoundness: it's possible that a user class could have a `__bool__` method that "disagrees" with its `__len__` method, which would mean that we would apply incorrect narrowing here. For example:

```py
from typing import Literal

class Tricksy:
    def __bool__(self) -> Literal[True]:
        return True

    def __len__(self):
        return 0

def f(x: Tricksy | Literal[""]):
    if len(x):
        reveal_type(x)  # `Tricksy` on your PR, but unreachable at runtime
    else:
        reveal_type(x)  # `Literal[""]` on your PR, but reachable at runtime
                        # even if an instance of `Tricksy` was passed in
```

Code after:
                        let place = self.expect_place(&left);
                        constraints.insert(place, ty);
                    }
                }
                ast::Expr::Call(ast::ExprCall {
                    range: _,
                    node_index: _,
                    func: callable,
                    arguments:
                        ast::Arguments {
                            args,
                            keywords,
                            range: _,
                            node_index: _,
                        },
                }) if keywords.is_empty() => {
                    let Type::ClassLiteral(rhs_class) = rhs_ty else {
                        continue;
                    };

                    let target = match &**args {
                        [first] => match place_expr(first) {
                            Some(target) => target,
                            None => continue,
                        },
                        _ => continue,
                    };

                    let is_positive = if is_positive {
                        op == &ast::CmpOp::Is
                    } else {
                        op == &ast::CmpOp::IsNot
                    };

                    // `else`-branch narrowing for `if type(x) is Y` can only be done
                    // if `Y` is a final class
                    if !rhs_class.is_final(self.db) && !is_positive {
                        continue;
                    }

                    let callable_type = inference.expression_type(&**callable);

                    if callable_type
                        .as_class_literal()
                        .is_some_and(|c| c.is_known(self.db, KnownClass::Type))
                    {
                        let place = self.expect_place(&target);
                        constraints.insert(
                            place,
                            Type::instance(self.db, rhs_class.unknown_specialization(self.db))
                                .negate_if(self.db, !is_positive),