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.

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).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants