diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index b7415de8b4..f32c007c1c 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -152,7 +152,8 @@ s = SubclassOfA() reveal_type(isinstance(s, SubclassOfA)) # revealed: Literal[True] reveal_type(isinstance(s, A)) # revealed: Literal[True] -def _(x: A | B): +def _(x: A | B, y: list[int]): + reveal_type(isinstance(y, list)) # revealed: Literal[True] reveal_type(isinstance(x, A)) # revealed: bool if isinstance(x, A): diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 713cd05b2d..360938cd13 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -238,3 +238,103 @@ def match_non_exhaustive(x: A | B | C): # this diagnostic is correct: the inferred type of `x` is `B & ~A & ~C` assert_never(x) # error: [type-assertion-failure] ``` + +## `isinstance` checks with generics + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import assert_never + +class A[T]: ... +class ASub[T](A[T]): ... +class B[T]: ... +class C[T]: ... +class D: ... +class E: ... +class F: ... + +def if_else_exhaustive(x: A[D] | B[E] | C[F]): + if isinstance(x, A): + pass + elif isinstance(x, B): + pass + elif isinstance(x, C): + pass + else: + # TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456) + no_diagnostic_here # error: [unresolved-reference] + assert_never(x) # error: [type-assertion-failure] + +# TODO: false-positive diagnostic (https://github.com/astral-sh/ty/issues/456) +def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type] + if isinstance(x, A): + return 0 + elif isinstance(x, B): + return 1 + elif isinstance(x, C): + return 2 + +def if_else_non_exhaustive(x: A[D] | B[E] | C[F]): + if isinstance(x, A): + pass + elif isinstance(x, C): + pass + else: + this_should_be_an_error # error: [unresolved-reference] + + # this diagnostic is correct: the inferred type of `x` is `B[E] & ~A[D] & ~C[F]` + assert_never(x) # error: [type-assertion-failure] + +def match_exhaustive(x: A[D] | B[E] | C[F]): + match x: + case A(): + pass + case B(): + pass + case C(): + pass + case _: + # TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456) + no_diagnostic_here # error: [unresolved-reference] + assert_never(x) # error: [type-assertion-failure] + +# TODO: false-positive diagnostic (https://github.com/astral-sh/ty/issues/456) +def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type] + match x: + case A(): + return 0 + case B(): + return 1 + case C(): + return 2 + +def match_non_exhaustive(x: A[D] | B[E] | C[F]): + match x: + case A(): + pass + case C(): + pass + case _: + this_should_be_an_error # error: [unresolved-reference] + + # this diagnostic is correct: the inferred type of `x` is `B[E] & ~A[D] & ~C[F]` + assert_never(x) # error: [type-assertion-failure] + +# This function might seem a bit silly, but it's a pattern that exists in real-world code! +# see https://github.com/bokeh/bokeh/blob/adef0157284696ce86961b2089c75fddda53c15c/src/bokeh/core/property/container.py#L130-L140 +def no_invalid_return_diagnostic_here_either[T](x: A[T]) -> ASub[T]: + if isinstance(x, A): + if isinstance(x, ASub): + return x + else: + return ASub() + else: + # We *would* emit a diagnostic here complaining that it's an invalid `return` statement + # ...except that we (correctly) infer that this branch is unreachable, so the complaint + # is null and void (and therefore we don't emit a diagnostic) + return x +``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 2b284830c9..2c13e03083 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -76,9 +76,9 @@ use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; use crate::types::{ - BoundMethodType, CallableType, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, - KnownClass, Truthiness, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, - UnionBuilder, all_members, walk_type_mapping, + BoundMethodType, CallableType, ClassBase, ClassLiteral, ClassType, DeprecatedInstance, + DynamicType, KnownClass, Truthiness, Type, TypeMapping, TypeRelation, TypeTransformer, + TypeVarInstance, UnionBuilder, all_members, walk_type_mapping, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -901,7 +901,12 @@ fn is_instance_truthiness<'db>( if let Type::NominalInstance(instance) = ty { if instance .class - .is_subclass_of(db, ClassType::NonGeneric(class)) + .iter_mro(db) + .filter_map(ClassBase::into_class) + .any(|c| match c { + ClassType::Generic(c) => c.origin(db) == class, + ClassType::NonGeneric(c) => c == class, + }) { return true; }