Skip to content

Fix negative type narrowing for isinstance/issubclass with type[T] variable or tuple - #11616

Open
Henry Su (hsusul) wants to merge 2 commits into
microsoft:mainfrom
hsusul:fix/isinstance-typevar-tuple-negative-narrowing
Open

Fix negative type narrowing for isinstance/issubclass with type[T] variable or tuple#11616
Henry Su (hsusul) wants to merge 2 commits into
microsoft:mainfrom
hsusul:fix/isinstance-typevar-tuple-negative-narrowing

Conversation

@hsusul

@hsusul Henry Su (hsusul) commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes an issue where isinstance() and issubclass() type narrowing failed to extract class filters when passed a variable of type type[T] or tuple[type[T], ...]. Also enables sound negative type narrowing when T is a @final class.

Problem & Root Cause

  1. getIsInstanceClassTypes extracts element class types for isinstance/issubclass. When given a variable of type type[T] or tuple[type[A], type[B]], the elements in tupleTypeArgs are ClassInstances of type (e.g. type[A]), rather than instantiable class types A. Previously, getIsInstanceClassTypes set foundNonClassType = true on type[T] instances, failing to extract the target filter types and resulting in no type narrowing.
  2. In negative narrowing (isPositiveTest = false), non-final classes typed as type[T] must preserve includeSubclasses = true because type[T] may hold a runtime subclass of T. However, when T is @final (e.g. @final class or built-in final type), T cannot have runtime subclasses, making negative narrowing sound.

Fix

  1. In getIsInstanceClassTypes:
    • Specifically check for ClassInstances of type (ClassType.isBuiltIn(subtype, 'type')) and extract their type arguments (type[T] -> T).
    • Reject type[Any] or type[Unknown] as concrete class filters.
    • Preserve includeSubclasses = true on extracted instantiable class types.
  2. In narrowTypeForInstanceOrSubclassInternal:
    • Allow negative elimination when ClassType.isFinal(concreteFilterType) is true, permitting sound negative narrowing for final class type[T] variables.

Verification

  • All 160 unit tests in typeEvaluator1.test.ts pass cleanly (160/160).
  • git diff --check, npm run check, and npm run typecheck pass with 0 errors.
…filter is a type[T] variable or tuple of type[T]
@StellaHuang95

Stella Huang (StellaHuang95) commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

🔒 Automated review in progress — Stella Huang (@StellaHuang95) is auto-reviewing this PR.

@StellaHuang95

Copy link
Copy Markdown
Collaborator

Clearing includeSubclasses makes negative narrowing for type[T] unsound: a runtime strict subclass can cause an A value to enter an else branch where this change removes A.

@StellaHuang95

Copy link
Copy Markdown
Collaborator

Clearing includeSubclasses makes negative narrowing for type[T] filters unsound and regresses existing subclass behavior.

@StellaHuang95

Copy link
Copy Markdown
Collaborator

Negative narrowing becomes unsound because type[A] may hold a strict subclass of A, and the new conversion also accepts non-type[...] class instances as valid filters.

@StellaHuang95 Stella Huang (StellaHuang95) added the review-auto:changes-requested Automated review: posted blocking findings to address. label Aug 10, 2026
if (isInstantiableClass(typeArg)) {
subtype = typeArg;
} else if (isClass(typeArg) && TypeBase.isInstance(typeArg)) {
subtype = convertToInstantiable(typeArg);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info · Optional note

The instantiable typeArg branch assigns the filter directly, while the instance-form branch converts it and thereby preserves includeSubclasses. Normalize the instantiable branch as well so any future representation reaching it cannot bypass the conservative non-final negative-narrowing behavior. [verified]

if isinstance(x, cls):
reveal_type(x, expected_text="FinalClass")
else:
reveal_type(x, expected_text="B")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Please add a non-final negative case. cls: type[A] may hold a subclass of A, so if not isinstance(x, cls) must retain A and reveal A | B; this is the soundness boundary protected by the new final-class guard. [verified]

@@ -0,0 +1,37 @@
# This sample tests type narrowing for isinstance and issubclass when
# the class argument is passed as a type[T] variable or tuple of type[T].

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

The sample and PR claim coverage for both isinstance and issubclass, but this file exercises only isinstance. Add an issubclass(..., cls: type[A]) regression case so its narrowing consumer remains covered. [verified]

if isinstance(x, cls):
reveal_type(x, expected_text="FinalClass")
else:
reveal_type(x, expected_text="B")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Add regression coverage for type[Any] and type[Unknown]. The new foundNonClassType path intentionally rejects these as concrete filters, but the behavior is currently untested.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@StellaHuang95 Stella Huang (StellaHuang95) added review-auto:approved Automated review: no blocking findings (approval posted). and removed review-auto:changes-requested Automated review: posted blocking findings to address. labels Aug 10, 2026

@rchiodo Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@rchiodo Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

@rchiodo

Rich Chiodo (rchiodo) commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

🔒 Automated review in progress — Rich Chiodo (@rchiodo) is auto-reviewing this PR.

reveal_type(x, expected_text="A | B")


def test_final_class_param(x: FinalClass | B, cls: type[FinalClass]):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Please add regression coverage for issubclass, including final-class negative narrowing, and for a non-final type[A] negative case that remains conservative. The tuple test should also include an excluded alternative so it demonstrates that the tuple filter narrows rather than merely preserving A | B.

@rchiodo Rich Chiodo (rchiodo) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

if isinstance(x, cls):
reveal_type(x, expected_text="FinalClass")
else:
reveal_type(x, expected_text="B")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info · Optional note

The regression sample exercises only isinstance with concrete arguments, despite the change also claiming issubclass and explicit type[Any]/type[Unknown] behavior. Add focused assertions for those paths so the helper's full changed contract is protected.

[verified]

@rchiodo

Copy link
Copy Markdown
Collaborator

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-auto:approved Automated review: no blocking findings (approval posted).

4 participants