From 746382f6fac36399f464848fd1828dcb1fcf6b21 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Jul 2025 10:51:38 +0200 Subject: [PATCH 1/6] =?UTF-8?q?[ty]=20Type=20inference=20for=20isinstance(?= =?UTF-8?q?=E2=80=A6)=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/mdtest/call/builtins.md | 47 +++++++ .../mdtest/exhaustiveness_checking.md | 116 ++++++++++++++++++ .../ty_python_semantic/src/types/function.rs | 53 ++++++-- 3 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index c9bb6621777fa3..0f8672c5d79058 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -105,3 +105,50 @@ str("Müsli", "utf-8") # error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[b"utf-8"]`" str(b"M\xc3\xbcsli", b"utf-8") ``` + +## Calls to `isinstance` + +We infer `Literal[True]` for a limited set of cases where we can be sure that the answer is correct, +but fall back to `bool` otherwise. + +```py +from enum import Enum + +class Answer(Enum): + NO = 0 + YES = 1 + +reveal_type(isinstance(True, bool)) # revealed: Literal[True] +reveal_type(isinstance(True, int)) # revealed: Literal[True] +reveal_type(isinstance(True, object)) # revealed: Literal[True] +reveal_type(isinstance("", str)) # revealed: Literal[True] +reveal_type(isinstance(1, int)) # revealed: Literal[True] +reveal_type(isinstance(b"", bytes)) # revealed: Literal[True] +reveal_type(isinstance(Answer.NO, Answer)) # revealed: Literal[True] + +reveal_type(isinstance("", int)) # revealed: bool + +class A: ... +class SubclassOfA(A): ... +class B: ... + +a = A() + +reveal_type(isinstance(a, A)) # revealed: Literal[True] +reveal_type(isinstance(a, object)) # revealed: Literal[True] +reveal_type(isinstance(a, SubclassOfA)) # revealed: bool +reveal_type(isinstance(a, B)) # revealed: bool + +s = SubclassOfA() +reveal_type(isinstance(s, SubclassOfA)) # revealed: Literal[True] +reveal_type(isinstance(s, A)) # revealed: Literal[True] + +def _(x: A | B): + reveal_type(isinstance(x, A)) # revealed: bool + + if isinstance(x, A): + pass + else: + reveal_type(x) # revealed: B & ~A + reveal_type(isinstance(x, B)) # revealed: Literal[True] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md new file mode 100644 index 00000000000000..497893ad94dcd8 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -0,0 +1,116 @@ +# Exhaustiveness checking + +```toml +[environment] +python-version = "3.11" +``` + +## Checks on literals + +```py +from typing import Literal, assert_never + +def if_else_exhaustive(x: Literal[0, 1, "a"]): + if x == 0: + pass + elif x == 1: + pass + elif x == "a": + pass + else: + no_diagnostic_here + + assert_never(x) + +def if_else_non_exhaustive(x: Literal[0, 1, "a"]): + if x == 0: + pass + elif x == "a": + pass + else: + this_should_be_an_error # error: [unresolved-reference] + + assert_never(x) # error: [type-assertion-failure] + +def match_exhaustive(x: Literal[0, 1, "a"]): + match x: + case 0: + pass + case 1: + pass + case "a": + pass + case _: + # TODO: this should not be an error + no_diagnostic_here # error: [unresolved-reference] + + assert_never(x) + +def match_non_exhaustive(x: Literal[0, 1, "a"]): + match x: + case 0: + pass + case "a": + pass + case _: + this_should_be_an_error # error: [unresolved-reference] + + assert_never(x) # error: [type-assertion-failure] +``` + +## `isinstance` checks + +```py +from typing import assert_never + +class A: ... +class B: ... +class C: ... + +def if_else_exhaustive(x: A | B | C): + if isinstance(x, A): + pass + elif isinstance(x, B): + pass + elif isinstance(x, C): + pass + else: + no_diagnostic_here + + assert_never(x) + +def if_else_non_exhaustive(x: A | B | C): + if isinstance(x, A): + pass + elif isinstance(x, C): + pass + else: + this_should_be_an_error # error: [unresolved-reference] + + assert_never(x) # error: [type-assertion-failure] + +def match_exhaustive(x: A | B | C): + match x: + case A(): + pass + case B(): + pass + case C(): + pass + case _: + # TODO: this should not be an error + no_diagnostic_here # error: [unresolved-reference] + + assert_never(x) + +def match_non_exhaustive(x: A | B | C): + match x: + case A(): + pass + case C(): + pass + case _: + this_should_be_an_error # error: [unresolved-reference] + + assert_never(x) # error: [type-assertion-failure] +``` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index be41d5820af895..3046e91fd161d1 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -76,8 +76,8 @@ use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; use crate::types::{ - BoundMethodType, CallableType, DeprecatedInstance, DynamicType, KnownClass, Type, TypeMapping, - TypeRelation, TypeTransformer, TypeVarInstance, UnionBuilder, walk_type_mapping, + BoundMethodType, CallableType, ClassType, DeprecatedInstance, DynamicType, KnownClass, Type, + TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, UnionBuilder, walk_type_mapping, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -1228,21 +1228,48 @@ impl KnownFunction { } KnownFunction::IsInstance | KnownFunction::IsSubclass => { - let [_, Some(Type::ClassLiteral(class))] = parameter_types else { + let [Some(first_arg), Some(Type::ClassLiteral(class))] = parameter_types else { return; }; - let Some(protocol_class) = class.into_protocol_class(db) else { - return; + + if let Some(protocol_class) = class.into_protocol_class(db) { + if !protocol_class.is_runtime_checkable(db) { + report_runtime_check_against_non_runtime_checkable_protocol( + context, + call_expression, + protocol_class, + self, + ); + } + } + + let is_instance = |ty: &Type<'_>| { + if let Type::NominalInstance(instance) = ty { + if instance + .class + .is_subclass_of(db, ClassType::NonGeneric(*class)) + { + return true; + } + } + false }; - if protocol_class.is_runtime_checkable(db) { - return; + + if match (self, first_arg) { + (KnownFunction::IsInstance, Type::NominalInstance(_)) => is_instance(first_arg), + // We do not handle unions specifically here, because something like `A | SubclassOfA` would + // have been simplified to `A` anyway + (KnownFunction::IsInstance, Type::Intersection(intersection)) => { + intersection.positive(db).iter().any(is_instance) + } + (KnownFunction::IsInstance, ty) => ty + .literal_fallback_instance(db) + .as_ref() + .is_some_and(is_instance), + _ => false, + } { + overload.set_return_type(Type::BooleanLiteral(true)); } - report_runtime_check_against_non_runtime_checkable_protocol( - context, - call_expression, - protocol_class, - self, - ); } known @ (KnownFunction::DunderImport | KnownFunction::ImportModule) => { From f3ce7546ab0a5601c3daf83fa68cc9e4d8f2a1b8 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Jul 2025 12:01:14 +0200 Subject: [PATCH 2/6] Add some more tests --- .../mdtest/exhaustiveness_checking.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 497893ad94dcd8..868acad0306079 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -22,6 +22,14 @@ def if_else_exhaustive(x: Literal[0, 1, "a"]): assert_never(x) +def if_else_exhaustive_no_assertion(x: Literal[0, 1, "a"]) -> int: + if x == 0: + return 0 + elif x == 1: + return 1 + elif x == "a": + return 2 + def if_else_non_exhaustive(x: Literal[0, 1, "a"]): if x == 0: pass @@ -46,6 +54,17 @@ def match_exhaustive(x: Literal[0, 1, "a"]): assert_never(x) +# TODO: there should be no error here +# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `int`" +def match_exhaustive_no_assertion(x: Literal[0, 1, "a"]) -> int: + match x: + case 0: + return 0 + case 1: + return 1 + case "a": + return 2 + def match_non_exhaustive(x: Literal[0, 1, "a"]): match x: case 0: @@ -58,6 +77,84 @@ def match_non_exhaustive(x: Literal[0, 1, "a"]): assert_never(x) # error: [type-assertion-failure] ``` +## Checks on enum literals + +```py +from enum import Enum +from typing import assert_never + +class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + +def if_else_exhaustive(x: Color): + if x == Color.RED: + pass + elif x == Color.GREEN: + pass + elif x == Color.BLUE: + pass + else: + no_diagnostic_here + + assert_never(x) + +def if_else_exhaustive_no_assertion(x: Color) -> int: + if x == Color.RED: + return 1 + elif x == Color.GREEN: + return 2 + elif x == Color.BLUE: + return 3 + +def if_else_non_exhaustive(x: Color): + if x == Color.RED: + pass + elif x == Color.BLUE: + pass + else: + this_should_be_an_error # error: [unresolved-reference] + + assert_never(x) # error: [type-assertion-failure] + +def match_exhaustive(x: Color): + match x: + case Color.RED: + pass + case Color.GREEN: + pass + case Color.BLUE: + pass + case _: + # TODO: this should not be an error + no_diagnostic_here # error: [unresolved-reference] + + assert_never(x) + +# TODO: there should be no error here +# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `int`" +def match_exhaustive_no_assertion(x: Color) -> int: + match x: + case Color.RED: + return 1 + case Color.GREEN: + return 2 + case Color.BLUE: + return 3 + +def match_non_exhaustive(x: Color): + match x: + case Color.RED: + pass + case Color.BLUE: + pass + case _: + this_should_be_an_error # error: [unresolved-reference] + + assert_never(x) # error: [type-assertion-failure] +``` + ## `isinstance` checks ```py @@ -79,6 +176,14 @@ def if_else_exhaustive(x: A | B | C): assert_never(x) +def if_else_exhaustive_no_assertion(x: A | B | C) -> int: + 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 | B | C): if isinstance(x, A): pass @@ -103,6 +208,17 @@ def match_exhaustive(x: A | B | C): assert_never(x) +# TODO: there should be no error here +# error: [invalid-return-type] "Function can implicitly return `None`, which is not assignable to return type `int`" +def match_exhaustive_no_assertion(x: A | B | C) -> int: + match x: + case A(): + return 0 + case B(): + return 1 + case C(): + return 2 + def match_non_exhaustive(x: A | B | C): match x: case A(): From 315e4424a480e6311033c82d5b91f5937a54bba2 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Jul 2025 12:14:03 +0200 Subject: [PATCH 3/6] Add comments explaining the type assertion failures --- .../resources/mdtest/exhaustiveness_checking.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md index 868acad0306079..9ecc26be0e96f1 100644 --- a/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md +++ b/crates/ty_python_semantic/resources/mdtest/exhaustiveness_checking.md @@ -38,6 +38,7 @@ def if_else_non_exhaustive(x: Literal[0, 1, "a"]): else: this_should_be_an_error # error: [unresolved-reference] + # this diagnostic is correct: the inferred type of `x` is `Literal[1]` assert_never(x) # error: [type-assertion-failure] def match_exhaustive(x: Literal[0, 1, "a"]): @@ -74,6 +75,7 @@ def match_non_exhaustive(x: Literal[0, 1, "a"]): case _: this_should_be_an_error # error: [unresolved-reference] + # this diagnostic is correct: the inferred type of `x` is `Literal[1]` assert_never(x) # error: [type-assertion-failure] ``` @@ -116,6 +118,7 @@ def if_else_non_exhaustive(x: Color): else: this_should_be_an_error # error: [unresolved-reference] + # this diagnostic is correct: inferred type of `x` is `Literal[Color.GREEN]` assert_never(x) # error: [type-assertion-failure] def match_exhaustive(x: Color): @@ -152,6 +155,7 @@ def match_non_exhaustive(x: Color): case _: this_should_be_an_error # error: [unresolved-reference] + # this diagnostic is correct: inferred type of `x` is `Literal[Color.GREEN]` assert_never(x) # error: [type-assertion-failure] ``` @@ -192,6 +196,7 @@ def if_else_non_exhaustive(x: A | B | C): else: this_should_be_an_error # error: [unresolved-reference] + # this diagnostic is correct: the inferred type of `x` is `B & ~A & ~C` assert_never(x) # error: [type-assertion-failure] def match_exhaustive(x: A | B | C): @@ -228,5 +233,6 @@ def match_non_exhaustive(x: A | B | C): case _: this_should_be_an_error # error: [unresolved-reference] + # this diagnostic is correct: the inferred type of `x` is `B & ~A & ~C` assert_never(x) # error: [type-assertion-failure] ``` From c04a9fcd89551768243e6275e45efd391c25def4 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Jul 2025 12:38:40 +0200 Subject: [PATCH 4/6] Handle a few more cases --- .../resources/mdtest/call/builtins.md | 7 + .../ty_python_semantic/src/types/function.rs | 121 ++++++++++++++---- 2 files changed, 100 insertions(+), 28 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index 0f8672c5d79058..ca5ce046898839 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -113,6 +113,7 @@ but fall back to `bool` otherwise. ```py from enum import Enum +from types import FunctionType class Answer(Enum): NO = 0 @@ -126,6 +127,12 @@ reveal_type(isinstance(1, int)) # revealed: Literal[True] reveal_type(isinstance(b"", bytes)) # revealed: Literal[True] reveal_type(isinstance(Answer.NO, Answer)) # revealed: Literal[True] +reveal_type(isinstance((1, 2), tuple)) # revealed: Literal[True] + +def f(): ... + +reveal_type(isinstance(f, FunctionType)) # revealed: Literal[True] + reveal_type(isinstance("", int)) # revealed: bool class A: ... diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 3046e91fd161d1..0a63caeeb6b73b 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -76,8 +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, ClassType, DeprecatedInstance, DynamicType, KnownClass, Type, - TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, UnionBuilder, walk_type_mapping, + BoundMethodType, CallableType, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, + KnownClass, Truthiness, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, + UnionBuilder, walk_type_mapping, }; use crate::{Db, FxOrderSet, ModuleName, resolve_module}; @@ -882,6 +883,92 @@ impl<'db> FunctionType<'db> { } } +/// Evaluate an `isinstance` call. Return `Truthiness::AlwaysTrue` if we can definitely infer that +/// this will return `True` at runtime, `Truthiness::AlwaysFalse` if we can definitely infer +/// that this will return `False` at runtime, or `Truthiness::Ambiguous` if we should infer `bool` +/// instead. +fn is_instance_truthiness<'db>( + db: &'db dyn Db, + ty: Type<'db>, + class: ClassLiteral<'db>, +) -> Truthiness { + let is_instance = |ty: &Type<'_>| { + if let Type::NominalInstance(instance) = ty { + if instance + .class + .is_subclass_of(db, ClassType::NonGeneric(class)) + { + return true; + } + } + false + }; + + let always_true_if = |test: bool| { + if test { + Truthiness::AlwaysTrue + } else { + Truthiness::Ambiguous + } + }; + + match ty { + Type::Union(_) => { + // We do not handle unions specifically here, because something like `A | SubclassOfA` would + // have been simplified to `A` anyway + Truthiness::Ambiguous + } + Type::Intersection(intersection) => { + always_true_if(intersection.positive(db).iter().any(is_instance)) + } + + Type::NominalInstance(_) => always_true_if(is_instance(&ty)), + + Type::BooleanLiteral(_) + | Type::BytesLiteral(_) + | Type::IntLiteral(_) + | Type::StringLiteral(_) + | Type::LiteralString + | Type::ModuleLiteral(_) + | Type::EnumLiteral(_) => always_true_if( + ty.literal_fallback_instance(db) + .as_ref() + .is_some_and(is_instance), + ), + + Type::Tuple(..) => always_true_if(class.is_known(db, KnownClass::Tuple)), + + Type::FunctionLiteral(..) => { + always_true_if(is_instance(&KnownClass::FunctionType.to_instance(db))) + } + + Type::BoundMethod(..) + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) + | Type::DataclassDecorator(..) + | Type::DataclassTransformer(..) + | Type::ClassLiteral(..) + | Type::GenericAlias(..) + | Type::SubclassOf(..) + | Type::ProtocolInstance(..) + | Type::SpecialForm(..) + | Type::KnownInstance(..) + | Type::PropertyInstance(..) + | Type::AlwaysTruthy + | Type::AlwaysFalsy + | Type::TypeVar(..) + | Type::BoundSuper(..) + | Type::TypeIs(..) + | Type::Callable(..) + | Type::Dynamic(..) + | Type::Never => { + // We could probably try to infer more precise types in some of these cases, but it's unclear + // if it's worth the effort. + Truthiness::Ambiguous + } + } +} + fn signature_cycle_recover<'db>( _db: &'db dyn Db, _value: &CallableSignature<'db>, @@ -1243,32 +1330,10 @@ impl KnownFunction { } } - let is_instance = |ty: &Type<'_>| { - if let Type::NominalInstance(instance) = ty { - if instance - .class - .is_subclass_of(db, ClassType::NonGeneric(*class)) - { - return true; - } - } - false - }; - - if match (self, first_arg) { - (KnownFunction::IsInstance, Type::NominalInstance(_)) => is_instance(first_arg), - // We do not handle unions specifically here, because something like `A | SubclassOfA` would - // have been simplified to `A` anyway - (KnownFunction::IsInstance, Type::Intersection(intersection)) => { - intersection.positive(db).iter().any(is_instance) - } - (KnownFunction::IsInstance, ty) => ty - .literal_fallback_instance(db) - .as_ref() - .is_some_and(is_instance), - _ => false, - } { - overload.set_return_type(Type::BooleanLiteral(true)); + if self == KnownFunction::IsInstance { + overload.set_return_type( + is_instance_truthiness(db, *first_arg, *class).into_type(db), + ); } } From 5615b10bb5db02ff444279e67bd4289b7a11677a Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Jul 2025 12:41:10 +0200 Subject: [PATCH 5/6] Handle intersections recursively --- crates/ty_python_semantic/src/types/function.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 0a63caeeb6b73b..2da82fad1afff7 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -918,9 +918,12 @@ fn is_instance_truthiness<'db>( // have been simplified to `A` anyway Truthiness::Ambiguous } - Type::Intersection(intersection) => { - always_true_if(intersection.positive(db).iter().any(is_instance)) - } + Type::Intersection(intersection) => always_true_if( + intersection + .positive(db) + .iter() + .any(|element| is_instance_truthiness(db, *element, class).is_always_true()), + ), Type::NominalInstance(_) => always_true_if(is_instance(&ty)), From 653ec2a14fb4a87c8dee02a94cea772b1a717f39 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Jul 2025 12:55:05 +0200 Subject: [PATCH 6/6] Add inference for isinstance(SomeClass, type) --- .../resources/mdtest/call/builtins.md | 2 ++ .../ty_python_semantic/src/types/function.rs | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/builtins.md b/crates/ty_python_semantic/resources/mdtest/call/builtins.md index ca5ce046898839..b7415de8b409fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/ty_python_semantic/resources/mdtest/call/builtins.md @@ -139,6 +139,8 @@ class A: ... class SubclassOfA(A): ... class B: ... +reveal_type(isinstance(A, type)) # revealed: Literal[True] + a = A() reveal_type(isinstance(a, A)) # revealed: Literal[True] diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 2da82fad1afff7..5934b2963419e3 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -913,7 +913,7 @@ fn is_instance_truthiness<'db>( }; match ty { - Type::Union(_) => { + Type::Union(..) => { // We do not handle unions specifically here, because something like `A | SubclassOfA` would // have been simplified to `A` anyway Truthiness::Ambiguous @@ -925,15 +925,15 @@ fn is_instance_truthiness<'db>( .any(|element| is_instance_truthiness(db, *element, class).is_always_true()), ), - Type::NominalInstance(_) => always_true_if(is_instance(&ty)), + Type::NominalInstance(..) => always_true_if(is_instance(&ty)), - Type::BooleanLiteral(_) - | Type::BytesLiteral(_) - | Type::IntLiteral(_) - | Type::StringLiteral(_) + Type::BooleanLiteral(..) + | Type::BytesLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) | Type::LiteralString - | Type::ModuleLiteral(_) - | Type::EnumLiteral(_) => always_true_if( + | Type::ModuleLiteral(..) + | Type::EnumLiteral(..) => always_true_if( ty.literal_fallback_instance(db) .as_ref() .is_some_and(is_instance), @@ -945,12 +945,13 @@ fn is_instance_truthiness<'db>( always_true_if(is_instance(&KnownClass::FunctionType.to_instance(db))) } + Type::ClassLiteral(..) => always_true_if(is_instance(&KnownClass::Type.to_instance(db))), + Type::BoundMethod(..) | Type::MethodWrapper(..) | Type::WrapperDescriptor(..) | Type::DataclassDecorator(..) | Type::DataclassTransformer(..) - | Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) | Type::ProtocolInstance(..)