diff --git a/packages/pyright-internal/src/analyzer/typeGuards.ts b/packages/pyright-internal/src/analyzer/typeGuards.ts index 82f2353e0f72..d4e3a2e6ea68 100644 --- a/packages/pyright-internal/src/analyzer/typeGuards.ts +++ b/packages/pyright-internal/src/analyzer/typeGuards.ts @@ -2137,8 +2137,11 @@ function narrowTypeForContainerType( return referenceType; } - // Determine which tuple types can be eliminated. Only "None" and - // literal types can be handled here. + // Determine which tuple types can be eliminated. Only "None", + // literal types, and final instantiable classes can be handled here. + // Non-final instantiable classes (type[A]) cannot be eliminated in negative + // tests because a subclass SubA(A) reaches the negative branch at runtime + // (since SubA != A). const typesToEliminate: Type[] = []; containerType.priv.tupleTypeArgs.forEach((tupleEntry) => { if (!tupleEntry.isUnbounded) { @@ -2146,6 +2149,8 @@ function narrowTypeForContainerType( typesToEliminate.push(tupleEntry.type); } else if (isClassInstance(tupleEntry.type) && isLiteralType(tupleEntry.type)) { typesToEliminate.push(tupleEntry.type); + } else if (isInstantiableClass(tupleEntry.type) && ClassType.isFinal(tupleEntry.type)) { + typesToEliminate.push(tupleEntry.type); } } }); diff --git a/packages/pyright-internal/src/tests/checker.test.ts b/packages/pyright-internal/src/tests/checker.test.ts index 75c25ecc617b..b77f31987886 100644 --- a/packages/pyright-internal/src/tests/checker.test.ts +++ b/packages/pyright-internal/src/tests/checker.test.ts @@ -721,3 +721,8 @@ test('Deprecated8', () => { const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated8.py'], configOptions); TestUtils.validateResults(analysisResults2, 4); }); + +test('TypeNarrowingContainer1', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeNarrowingContainer1.py']); + TestUtils.validateResults(analysisResults, 0); +}); diff --git a/packages/pyright-internal/src/tests/samples/typeNarrowingContainer1.py b/packages/pyright-internal/src/tests/samples/typeNarrowingContainer1.py new file mode 100644 index 000000000000..a1961ffcadfb --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/typeNarrowingContainer1.py @@ -0,0 +1,38 @@ +# This sample tests negative type narrowing for tuple membership checks (in / not in) +# containing instantiable class objects (type[T]). + +from typing import final +from typing_extensions import assert_type + +@final +class FinalClassA: pass + +@final +class FinalClassB: pass + +class ClassC: pass + +class NonFinalClassA: pass +class SubA(NonFinalClassA): pass + + +def test_in_final_class_tuple(x: type[FinalClassA] | type[FinalClassB] | type[ClassC]): + if x in (FinalClassA, FinalClassB): + assert_type(x, type[FinalClassA] | type[FinalClassB]) + else: + assert_type(x, type[ClassC]) + +def test_not_in_final_class_tuple(x: type[FinalClassA] | type[FinalClassB] | type[ClassC]): + if x not in (FinalClassA, FinalClassB): + assert_type(x, type[ClassC]) + else: + assert_type(x, type[FinalClassA] | type[FinalClassB]) + +def test_not_in_non_final_class_tuple(x: type[NonFinalClassA] | type[ClassC]): + if x not in (NonFinalClassA,): + # SubA is a subclass of NonFinalClassA. At runtime, SubA in (NonFinalClassA,) + # evaluates to False, so SubA reaches this negative branch. Therefore, + # type[NonFinalClassA] must not be eliminated when NonFinalClassA is not final. + assert_type(x, type[NonFinalClassA] | type[ClassC]) + else: + assert_type(x, type[NonFinalClassA])