Better model runtime in isinstance and type checks (#20675)

We now more accurately model reachability for impossible checks. We also
avoid allowing tuples and X | Y syntax in situations where they don't
work at runtime.
diff --git a/mypy/checker.py b/mypy/checker.py
index 2fdfdf7..d371f40 100644
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -6804,18 +6804,20 @@
                     continue
                 expr = operands[j]
 
-                current_type_range = self.get_isinstance_type(expr)
-                if current_type_range is not None:
-                    target_type = make_simplified_union([tr.item for tr in current_type_range])
-                    if isinstance(target_type, AnyType):
-                        # Avoid widening to Any for checks like `type(x) is type(y: Any)`.
-                        # We patch this here because it is desirable to widen to any for cases like
-                        # isinstance(x, (y: Any))
-                        continue
+                current_type_range = self.get_type_range_of_type(operand_types[j])
+                if current_type_range is None:
+                    continue
+                if isinstance(get_proper_type(current_type_range.item), AnyType):
+                    # Avoid widening to Any for checks like `type(x) is type(y: Any)`.
+                    # We patch this here because it is desirable to widen to any for cases like
+                    # isinstance(x, (y: Any))
+                    continue
                 if_map, else_map = conditional_types_to_typemaps(
                     expr_in_type_expr,
                     *self.conditional_types_with_intersection(
-                        self.lookup_type(expr_in_type_expr), current_type_range, expr_in_type_expr
+                        self.lookup_type(expr_in_type_expr),
+                        [current_type_range],
+                        expr_in_type_expr,
                     ),
                 )
 
@@ -7899,51 +7901,87 @@
             return first_item.var.is_settable_property
         return False
 
-    def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None:
+    def get_isinstance_type(
+        self, expr: Expression, flatten_tuples: bool = True
+    ) -> list[TypeRange] | None:
         """Get the type(s) resulting from an isinstance check.
 
         Returns an empty list for isinstance(x, ()).
         """
         if isinstance(expr, OpExpr) and expr.op == "|":
-            left = self.get_isinstance_type(expr.left)
-            if left is None and is_literal_none(expr.left):
+            left: list[TypeRange] | None
+            right: list[TypeRange] | None
+            if is_literal_none(expr.left):
                 left = [TypeRange(NoneType(), is_upper_bound=False)]
-            right = self.get_isinstance_type(expr.right)
-            if right is None and is_literal_none(expr.right):
+            else:
+                left = self.get_isinstance_type(expr.left, flatten_tuples=False)
+            if is_literal_none(expr.right):
                 right = [TypeRange(NoneType(), is_upper_bound=False)]
+            else:
+                right = self.get_isinstance_type(expr.right, flatten_tuples=False)
             if left is None or right is None:
                 return None
             return left + right
-        all_types = get_proper_types(flatten_types(self.lookup_type(expr)))
-        types: list[TypeRange] = []
-        for typ in all_types:
-            if isinstance(typ, FunctionLike) and typ.is_type_obj():
-                # If a type is generic, `isinstance` can only narrow its variables to Any.
-                any_parameterized = fill_typevars_with_any(typ.type_object())
-                # Tuples may have unattended type variables among their items
-                if isinstance(any_parameterized, TupleType):
-                    erased_type = erase_typevars(any_parameterized)
-                else:
-                    erased_type = any_parameterized
-                types.append(TypeRange(erased_type, is_upper_bound=False))
-            elif isinstance(typ, TypeType):
-                # Type[A] means "any type that is a subtype of A" rather than "precisely type A"
-                # we indicate this by setting is_upper_bound flag
-                is_upper_bound = True
-                if isinstance(typ.item, NoneType):
-                    # except for Type[None], because "'NoneType' is not an acceptable base type"
-                    is_upper_bound = False
-                types.append(TypeRange(typ.item, is_upper_bound=is_upper_bound))
-            elif isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
-                object_type = Instance(typ.type.mro[-1], [])
-                types.append(TypeRange(object_type, is_upper_bound=True))
-            elif isinstance(typ, Instance) and typ.type.fullname == "types.UnionType" and typ.args:
-                types.append(TypeRange(UnionType(typ.args), is_upper_bound=False))
-            elif isinstance(typ, AnyType):
-                types.append(TypeRange(typ, is_upper_bound=False))
-            else:  # we didn't see an actual type, but rather a variable with unknown value
+
+        if flatten_tuples:
+            type_ranges = []
+            for typ in flatten_types_if_tuple(self.lookup_type(expr)):
+                type_range = self.get_type_range_of_type(typ)
+                if type_range is None:
+                    return None
+                type_ranges.append(type_range)
+            return type_ranges
+
+        else:
+            type_range = self.get_type_range_of_type(self.lookup_type(expr))
+            if type_range is None:
                 return None
-        return types
+            return [type_range]
+
+    def get_type_range_of_type(self, typ: Type) -> TypeRange | None:
+        typ = get_proper_type(typ)
+        if isinstance(typ, TypeVarType):
+            typ = get_proper_type(typ.upper_bound)
+
+        if isinstance(typ, UnionType):
+            type_ranges = [self.get_type_range_of_type(item) for item in typ.items]
+            is_upper_bound = any(t.is_upper_bound for t in type_ranges if t is not None)
+            item = make_simplified_union([t.item for t in type_ranges if t is not None])
+            return TypeRange(item, is_upper_bound=is_upper_bound)
+        if isinstance(typ, FunctionLike) and typ.is_type_obj():
+            # If a type is generic, `isinstance` can only narrow its variables to Any.
+            any_parameterized = fill_typevars_with_any(typ.type_object())
+            # Tuples may have unattended type variables among their items
+            if isinstance(any_parameterized, TupleType):
+                erased_type = erase_typevars(any_parameterized)
+            else:
+                erased_type = any_parameterized
+            return TypeRange(erased_type, is_upper_bound=False)
+        if isinstance(typ, TypeType):
+            # Type[A] means "any type that is a subtype of A" rather than "precisely type A"
+            # we indicate this by setting is_upper_bound flag
+            is_upper_bound = True
+            if isinstance(typ.item, NoneType):
+                # except for Type[None], because "'NoneType' is not an acceptable base type"
+                is_upper_bound = False
+            return TypeRange(typ.item, is_upper_bound=is_upper_bound)
+        if isinstance(typ, AnyType):
+            return TypeRange(typ, is_upper_bound=False)
+        if isinstance(typ, Instance) and typ.type.fullname == "builtins.type":
+            object_type = Instance(typ.type.mro[-1], [])
+            return TypeRange(object_type, is_upper_bound=True)
+        if isinstance(typ, Instance) and typ.type.fullname == "types.UnionType" and typ.args:
+            return TypeRange(UnionType(typ.args), is_upper_bound=False)
+        if isinstance(typ, Instance) and typ.type.fullname == "typing._SpecialForm":
+            # This is probably an alias to a Union object. We don't have the args here so we can't
+            # conclude anything
+            return None
+        if not is_subtype(self.named_type("builtins.type"), typ):
+            # We saw something, but it couldn't possibly be valid
+            return TypeRange(UninhabitedType(), is_upper_bound=False)
+
+        # This is e.g. a variable of type object, so we can't conclude anything
+        return None
 
     def is_literal_enum(self, n: Expression) -> bool:
         """Returns true if this expression (with the given type context) is an Enum literal.
@@ -8642,13 +8680,13 @@
         return [t]
 
 
-def flatten_types(t: Type) -> list[Type]:
+def flatten_types_if_tuple(t: Type) -> list[Type]:
     """Flatten a nested sequence of tuples into one list of nodes."""
     t = get_proper_type(t)
     if isinstance(t, UnionType):
-        return [b for a in t.items for b in flatten_types(a)]
+        return [b for a in t.items for b in flatten_types_if_tuple(a)]
     if isinstance(t, TupleType):
-        return [b for a in t.items for b in flatten_types(a)]
+        return [b for a in t.items for b in flatten_types_if_tuple(a)]
     elif is_named_instance(t, "builtins.tuple"):
         return [t.args[0]]
     return [t]
diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test
index f27d905..c386555 100644
--- a/test-data/unit/check-isinstance.test
+++ b/test-data/unit/check-isinstance.test
@@ -1748,6 +1748,21 @@
         cls()[0] + 1
 [builtins fixtures/isinstancelist.pyi]
 
+[case testIssubclassTypeVar]
+# flags: --strict-equality --warn-unreachable
+from __future__ import annotations
+from typing import TypeVar
+
+ClassT = TypeVar("ClassT", bound=type)
+
+def directed_meet(cls0: ClassT, cls1: ClassT) -> ClassT | None:
+    if issubclass(cls1, cls0):
+        return cls1
+    if issubclass(cls0, cls1):
+        return cls0
+    return None
+[builtins fixtures/isinstancelist.pyi]
+
 [case testIsinstanceTypeArgs]
 from typing import Iterable, TypeVar
 x = 1
@@ -2932,6 +2947,7 @@
 [builtins fixtures/isinstance.pyi]
 
 [case testIsInstanceTypeAny]
+# flags: --strict-equality --warn-unreachable
 from typing import Any
 
 def foo(x: object, t: type[Any]):
@@ -2939,10 +2955,36 @@
         reveal_type(x)  # N: Revealed type is "Any"
 [builtins fixtures/isinstance.pyi]
 
+[case testIsInstanceObject]
+# flags: --strict-equality --warn-unreachable
+from typing import Any
+
+def foo(x: object, t: object):
+    if isinstance(x, t):  # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type[object] | tuple[type[object], ...]"
+        reveal_type(x)  # N: Revealed type is "builtins.object"
+[builtins fixtures/isinstance.pyi]
+
+[case testIsInstanceOrExprInTuple]
+# flags: --strict-equality --warn-unreachable
+from typing import Any
+
+def f1(x: object):
+    if isinstance(x, str | (int, dict)):  # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type | tuple[Any, ...]"
+        reveal_type(x)  # N: Revealed type is "builtins.str"
+    if type(x) == str | (int, dict):
+        reveal_type(x)  # N: Revealed type is "builtins.object"
+
+def f2(x: Any):
+    if isinstance(x, str | (int, dict)):  # E: Argument 2 to "isinstance" has incompatible type "object"; expected "type | tuple[Any, ...]"
+        reveal_type(x)  # N: Revealed type is "builtins.str"
+    if type(x) == str | (int, dict):
+        reveal_type(x)  # N: Revealed type is "Any"
+[builtins fixtures/primitives.pyi]
+
 [case testIsInstanceUnionOfTuples]
 # flags: --strict-equality --warn-unreachable
 from __future__ import annotations
-from typing import TypeVar, Iterator
+from typing import TypeVar, Iterator, final
 
 T1 = TypeVar("T1")
 T2 = TypeVar("T2")
@@ -2960,4 +3002,53 @@
         reveal_type(values)  # N: Revealed type is "T1`-1 | T2`-2 | T3`-3"
         yield values
     raise
+
+class A: ...
+class B: ...
+
+def f1(x: A | B, t: tuple[type[A]] | tuple[type[B]]):
+    if isinstance(x, t):
+        reveal_type(x)  # N: Revealed type is "__main__.A | __main__.B"
+    else:
+        reveal_type(x)  # N: Revealed type is "__main__.A | __main__.B"
+
+def f2(x: object, t: tuple[type[A]] | tuple[type[B]]):
+    if isinstance(x, t):
+        reveal_type(x)  # N: Revealed type is "__main__.A | __main__.B"
+    else:
+        reveal_type(x)  # N: Revealed type is "builtins.object"
+
+@final
+class FA: ...
+@final
+class FB: ...
+
+def g1(x: FA | FB, t: tuple[type[FA]] | tuple[type[FB]]):
+    if isinstance(x, t):
+        reveal_type(x)  # N: Revealed type is "__main__.FA | __main__.FB"
+    else:
+        reveal_type(x)  # N: Revealed type is "__main__.FA | __main__.FB"
+
+def g2(x: object, t: tuple[type[FA]] | tuple[type[FB]]):
+    if isinstance(x, t):
+        reveal_type(x)  # N: Revealed type is "__main__.FA | __main__.FB"
+    else:
+        reveal_type(x)  # N: Revealed type is "builtins.object"
+[builtins fixtures/primitives.pyi]
+
+
+[case testIsInstanceTypeVarBoundToType]
+# flags: --strict-equality --warn-unreachable
+from __future__ import annotations
+from typing import TypeVar, Protocol
+
+class A(Protocol):
+    x: int
+
+T = TypeVar("T", bound=type[A])
+
+def foo(x: object, t: T):
+    if isinstance(x, t):
+        reveal_type(x)  # N: Revealed type is "__main__.A"
+        reveal_type(x.x)  # N: Revealed type is "builtins.int"
 [builtins fixtures/primitives.pyi]
diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test
index 368ce81..b9bb0c1 100644
--- a/test-data/unit/check-narrowing.test
+++ b/test-data/unit/check-narrowing.test
@@ -3180,13 +3180,22 @@
     reveal_type(y) # N: Revealed type is "collections.defaultdict[Any, Any]"
     reveal_type(z) # N: Revealed type is "collections.defaultdict[Any, Any]"
 
-[case testUnionTypeEquality-xfail]
+[case testTypeEqualsTypeObjectUnion]
+# flags: --strict-equality --warn-unreachable
+from __future__ import annotations
+def f(x: object, y: type[int] | type[str]):
+    if type(x) == y:
+        reveal_type(x)  # N: Revealed type is "builtins.int | builtins.str"
+        reveal_type(y)  # N: Revealed type is "type[builtins.int] | type[builtins.str]"
+[builtins fixtures/primitives.pyi]
+
+[case testUnionTypeEquality]
 # flags: --strict-equality --warn-unreachable
 from typing import Any, reveal_type
 
-x: Any = ()
-if type(x) == (int, str):
-    reveal_type(x) # E: Statement is unreachable
+def f(x: Any):
+    if type(x) == (int, str):  # E: Non-overlapping equality check (left operand type: "type[Any]", right operand type: "tuple[type[int], type[str]]")
+        reveal_type(x) # E: Statement is unreachable
 [builtins fixtures/tuple.pyi]
 
 [case testTypeIntersectionWithConcreteTypes]