-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Fix type narrowing for type(x) expressions with type[T] parameters and reverse comparisons #11619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { assert } from '../common/debug'; | |
| import { | ||
| ArgCategory, | ||
| AssignmentExpressionNode, | ||
| CallNode, | ||
| ExpressionNode, | ||
| isExpressionNode, | ||
| NameNode, | ||
|
|
@@ -209,45 +210,83 @@ export function getTypeNarrowingCallback( | |
| } | ||
| } | ||
|
|
||
| // Look for "type(X) is Y", "type(X) is not Y", "type(X) == Y" or "type(X) != Y". | ||
| // Look for "type(X) is Y", "type(X) is not Y", "type(X) == Y" or "type(X) != Y", | ||
| // as well as reverse forms "Y is type(X)", "Y is not type(X)", "Y == type(X)", "Y != type(X)". | ||
| let typeCallNode: CallNode | undefined; | ||
| let otherExpr: ExpressionNode | undefined; | ||
|
|
||
| if (testExpression.d.leftExpr.nodeType === ParseNodeType.Call) { | ||
| if ( | ||
| testExpression.d.leftExpr.d.args.length === 1 && | ||
| testExpression.d.leftExpr.d.args[0].d.argCategory === ArgCategory.Simple | ||
| ) { | ||
| const arg0Expr = testExpression.d.leftExpr.d.args[0].d.valueExpr; | ||
| if (isMatchingExpressionOrWalrusRhs(evaluator, reference, arg0Expr)) { | ||
| const callType = evaluator.getTypeOfExpression( | ||
| testExpression.d.leftExpr.d.leftExpr, | ||
| EvalFlags.CallBaseDefaults | ||
| ).type; | ||
|
|
||
| if (isInstantiableClass(callType) && ClassType.isBuiltIn(callType, 'type')) { | ||
| const rhsResult = evaluator.getTypeOfExpression(testExpression.d.rightExpr); | ||
| const classTypes: ClassType[] = []; | ||
| let isClassType = true; | ||
|
|
||
| evaluator.mapSubtypesExpandTypeVars( | ||
| rhsResult.type, | ||
| /* options */ undefined, | ||
| (expandedSubtype) => { | ||
| if (isInstantiableClass(expandedSubtype)) { | ||
| classTypes.push(expandedSubtype); | ||
| } else { | ||
| isClassType = false; | ||
| typeCallNode = testExpression.d.leftExpr; | ||
| otherExpr = testExpression.d.rightExpr; | ||
| } else if (testExpression.d.rightExpr.nodeType === ParseNodeType.Call) { | ||
| typeCallNode = testExpression.d.rightExpr; | ||
| otherExpr = testExpression.d.leftExpr; | ||
| } | ||
|
|
||
| if ( | ||
| typeCallNode && | ||
| otherExpr && | ||
| typeCallNode.d.args.length === 1 && | ||
| typeCallNode.d.args[0].d.argCategory === ArgCategory.Simple | ||
| ) { | ||
| const arg0Expr = typeCallNode.d.args[0].d.valueExpr; | ||
| if (isMatchingExpressionOrWalrusRhs(evaluator, reference, arg0Expr)) { | ||
| const callType = evaluator.getTypeOfExpression( | ||
| typeCallNode.d.leftExpr, | ||
| EvalFlags.CallBaseDefaults | ||
| ).type; | ||
|
|
||
| if (isInstantiableClass(callType) && ClassType.isBuiltIn(callType, 'type')) { | ||
| const otherResult = evaluator.getTypeOfExpression(otherExpr); | ||
| const classTypes: ClassType[] = []; | ||
| let isClassType = true; | ||
|
|
||
| evaluator.mapSubtypesExpandTypeVars( | ||
| otherResult.type, | ||
| /* options */ undefined, | ||
| (expandedSubtype) => { | ||
| let instantiable: ClassType | undefined; | ||
| let isTypeParam = false; | ||
|
|
||
| if (isClass(expandedSubtype)) { | ||
| if ( | ||
| ClassType.isBuiltIn(expandedSubtype, 'type') && | ||
| expandedSubtype.priv.typeArgs && | ||
| expandedSubtype.priv.typeArgs.length > 0 | ||
| ) { | ||
| isTypeParam = true; | ||
| const extracted = convertToInstantiable( | ||
| expandedSubtype.priv.typeArgs[0], | ||
| /* includeSubclasses */ true | ||
| ); | ||
| if (isInstantiableClass(extracted)) { | ||
| instantiable = extracted; | ||
| } | ||
| } else if (isInstantiableClass(expandedSubtype)) { | ||
| instantiable = expandedSubtype; | ||
| } | ||
| return undefined; | ||
| } | ||
| ); | ||
|
|
||
| if (isClassType && classTypes.length > 0) { | ||
| return (type: Type) => { | ||
| return { | ||
| type: narrowTypeForTypeIs(evaluator, type, classTypes, adjIsPositiveTest), | ||
| isIncomplete: !!rhsResult.isIncomplete, | ||
| }; | ||
| }; | ||
| if (instantiable && !ClassType.isBuiltIn(instantiable, 'object')) { | ||
| classTypes.push( | ||
| isTypeParam | ||
| ? instantiable | ||
| : ClassType.cloneIncludeSubclasses(instantiable, false) | ||
| ); | ||
| } else { | ||
| isClassType = false; | ||
| } | ||
| return undefined; | ||
| } | ||
| ); | ||
|
|
||
| if (isClassType && classTypes.length > 0) { | ||
| return (type: Type) => { | ||
| return { | ||
| type: narrowTypeForTypeIs(evaluator, type, classTypes, adjIsPositiveTest), | ||
| isIncomplete: !!otherResult.isIncomplete, | ||
| }; | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The blanket
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The |
||
|
|
@@ -2538,17 +2577,20 @@ function narrowTypeForTypeIs(evaluator: TypeEvaluator, type: Type, classTypes: C | |
| /* options */ undefined, | ||
| (subtype: Type, unexpandedSubtype: Type) => { | ||
| if (isClassInstance(subtype)) { | ||
| const matches = ClassType.isDerivedFrom(classType, ClassType.cloneAsInstantiable(subtype)); | ||
| const instantiableSubtype = ClassType.cloneAsInstantiable(subtype); | ||
| const matches = ClassType.isDerivedFrom(classType, instantiableSubtype); | ||
| const isSubclass = ClassType.isDerivedFrom(instantiableSubtype, classType); | ||
|
|
||
| if (isPositiveTest) { | ||
| if (matches) { | ||
| if (ClassType.isSameGenericClass(ClassType.cloneAsInstantiable(subtype), classType)) { | ||
| if (ClassType.isSameGenericClass(instantiableSubtype, classType)) { | ||
| return addConditionToType(subtype, getTypeCondition(classType)); | ||
| } | ||
|
|
||
| return addConditionToType(ClassType.cloneAsInstance(classType), subtype.props?.condition); | ||
| } | ||
|
|
||
| if (!classType.priv.includeSubclasses) { | ||
| if (!classType.priv.includeSubclasses || !isSubclass) { | ||
| return undefined; | ||
| } | ||
|
|
||
|
|
@@ -2560,7 +2602,7 @@ function narrowTypeForTypeIs(evaluator: TypeEvaluator, type: Type, classTypes: C | |
| if (!classType.priv.includeSubclasses) { | ||
| // If the class if marked final and it matches, then | ||
| // we can eliminate it in the negative case. | ||
| if (matches && ClassType.isFinal(subtype)) { | ||
| if (matches && ClassType.isFinal(instantiableSubtype)) { | ||
| return undefined; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # This sample tests type narrowing for type(x) checks. | ||
|
|
||
| from typing import final | ||
|
|
||
| @final | ||
| class FinalA: pass | ||
|
|
||
| @final | ||
| class FinalB: pass | ||
|
|
||
| def func1(x: int | str, cls: type[int]): | ||
| if type(x) is cls: | ||
| reveal_type(x, expected_text="int") | ||
| else: | ||
| reveal_type(x, expected_text="int | str") | ||
|
|
||
| def func2(x: int | str, y: int): | ||
| if type(x) is type(y): | ||
| reveal_type(x, expected_text="int") | ||
| else: | ||
| reveal_type(x, expected_text="int | str") | ||
|
|
||
| def func3(x: int | str, cls: type[int]): | ||
| if cls is type(x): | ||
| reveal_type(x, expected_text="int") | ||
| else: | ||
| reveal_type(x, expected_text="int | str") | ||
|
|
||
| def func4(x: int | str): | ||
| if int is type(x): | ||
| reveal_type(x, expected_text="int") | ||
| else: | ||
| reveal_type(x, expected_text="int | str") | ||
|
|
||
| def func5(x: int | str, cls: type[int]): | ||
| if type(x) == cls: | ||
| reveal_type(x, expected_text="int") | ||
| else: | ||
| reveal_type(x, expected_text="int | str") | ||
|
|
||
| def func6(x: FinalA | FinalB, cls: type[FinalA]): | ||
| if type(x) is cls: | ||
| reveal_type(x, expected_text="FinalA") | ||
| else: | ||
| reveal_type(x, expected_text="FinalB") | ||
|
|
||
| def func7(x: int | str, cls: type[int] | type[str]): | ||
| if type(x) is cls: | ||
| reveal_type(x, expected_text="int | str") | ||
|
|
||
| class Base: pass | ||
|
|
||
| @final | ||
| class FinalSub(Base): pass | ||
|
|
||
| def func8(x: Base): | ||
| if type(x) is not FinalSub: | ||
| reveal_type(x, expected_text="Base") | ||
| else: | ||
| reveal_type(x, expected_text="FinalSub") | ||
|
rchiodo marked this conversation as resolved.
|
||
|
|
||
| class Index: pass | ||
|
|
||
| class MultiIndex(Index): pass | ||
|
|
||
| def test_spark_regression(self_val: MultiIndex, other: Index): | ||
| if type(self_val) is not type(other): | ||
| pass | ||
| reveal_type(self_val, expected_text="MultiIndex") | ||
|
|
||
| def test_direct_class_vs_type_param(x: Base, cls: type[Base]): | ||
| if type(x) is not Base: | ||
| reveal_type(x, expected_text="Base") | ||
|
|
||
| if type(x) is not cls: | ||
| reveal_type(x, expected_text="Base") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When both operands are calls, the left call is selected before checking whether its argument matches the reference. As a result, querying narrowing for
xintype(y) is type(x)stops aftertype(y)fails the match and never considers the RHStype(x). Please fall through to the RHS call when the selected call does not match the reference.