Fix negative type narrowing for isinstance/issubclass with type[T] variable or tuple - #11616
Conversation
…filter is a type[T] variable or tuple of type[T]
|
🔒 Automated review in progress — Stella Huang (@StellaHuang95) is auto-reviewing this PR. |
|
Clearing |
|
Clearing |
|
Negative narrowing becomes unsound because |
| if (isInstantiableClass(typeArg)) { | ||
| subtype = typeArg; | ||
| } else if (isClass(typeArg) && TypeBase.isInstance(typeArg)) { | ||
| subtype = convertToInstantiable(typeArg); |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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]. | |||
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
Stella Huang (StellaHuang95)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
Rich Chiodo (rchiodo)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
Rich Chiodo (rchiodo)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
Summary
Fixes an issue where
isinstance()andissubclass()type narrowing failed to extract class filters when passed a variable of typetype[T]ortuple[type[T], ...]. Also enables sound negative type narrowing whenTis a@finalclass.Problem & Root Cause
getIsInstanceClassTypesextracts element class types forisinstance/issubclass. When given a variable of typetype[T]ortuple[type[A], type[B]], the elements intupleTypeArgsareClassInstances oftype(e.g.type[A]), rather than instantiable class typesA. Previously,getIsInstanceClassTypessetfoundNonClassType = trueontype[T]instances, failing to extract the target filter types and resulting in no type narrowing.isPositiveTest = false), non-final classes typed astype[T]must preserveincludeSubclasses = truebecausetype[T]may hold a runtime subclass ofT. However, whenTis@final(e.g.@finalclass or built-in final type),Tcannot have runtime subclasses, making negative narrowing sound.Fix
getIsInstanceClassTypes:ClassInstances oftype(ClassType.isBuiltIn(subtype, 'type')) and extract their type arguments (type[T] -> T).type[Any]ortype[Unknown]as concrete class filters.includeSubclasses = trueon extracted instantiable class types.narrowTypeForInstanceOrSubclassInternal:ClassType.isFinal(concreteFilterType)is true, permitting sound negative narrowing for final classtype[T]variables.Verification
typeEvaluator1.test.tspass cleanly (160/160).git diff --check,npm run check, andnpm run typecheckpass with 0 errors.