Fixed a bug that led to a false negative when determining type compatibility between two unions in an invariant context. This addresses #5534. (#5536)

Co-authored-by: Eric Traut <erictr@microsoft.com>
This commit is contained in:
Eric Traut 2023-07-18 15:51:27 -07:00 committed by GitHub
parent 9855dc9c7f
commit 5569939ee6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 96 additions and 17 deletions

View File

@ -21717,10 +21717,14 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
return true;
}
diag?.addMessage(
Localizer.DiagnosticAddendum.typeAssignmentMismatch().format(printSrcDestTypes(srcType, destType))
);
return false;
if (!isUnion(destType)) {
diag?.addMessage(
Localizer.DiagnosticAddendum.typeAssignmentMismatch().format(
printSrcDestTypes(srcType, destType)
)
);
return false;
}
}
}
@ -22379,6 +22383,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
// whose primary type matches.
remainingSrcSubtypes.forEach((srcSubtype) => {
const destTypeIndex = remainingDestSubtypes.findIndex((destSubtype) => {
if (isTypeSame(destSubtype, srcSubtype)) {
return true;
}
if (
isClass(srcSubtype) &&
isClass(destSubtype) &&
@ -22420,6 +22428,16 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
// If there is are remaining dest subtypes and they're all type variables,
// attempt to assign the remaining source subtypes to them.
if (canUseFastPath && (remainingDestSubtypes.length !== 0 || remainingSrcSubtypes.length !== 0)) {
if ((flags & AssignTypeFlags.EnforceInvariance) !== 0) {
// If we have no src subtypes remaining but not all dest types have been subsumed
// by other dest types, then the types are not compatible if we're enforcing invariance.
if (remainingSrcSubtypes.length === 0) {
return remainingDestSubtypes.every((destSubtype) =>
isTypeSubsumedByOtherType(destSubtype, destType.subtypes, recursionCount)
);
}
}
const isReversed = (flags & AssignTypeFlags.ReverseTypeVarMatching) !== 0;
const effectiveDestSubtypes = isReversed ? remainingSrcSubtypes : remainingDestSubtypes;
@ -22512,21 +22530,9 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
recursionCount
)
) {
const concreteSubtype = makeTopLevelTypeVarsConcrete(subtype);
// Determine if the current subtype is subsumed by another subtype
// in the same union. If so, we can ignore this.
let isSubtypeSubsumed = false;
srcType.subtypes.forEach((innerSubtype) => {
if (
!isSubtypeSubsumed &&
!isTypeSame(innerSubtype, subtype) &&
!isAnyOrUnknown(innerSubtype) &&
isProperSubtype(innerSubtype, concreteSubtype, recursionCount)
) {
isSubtypeSubsumed = true;
}
});
const isSubtypeSubsumed = isTypeSubsumedByOtherType(subtype, srcType.subtypes, recursionCount);
// Try again with a concrete version of the subtype.
if (
@ -22556,6 +22562,24 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
return true;
}
// Determines whether a type is "subsumed by" (i.e. is a proper subtype of) one
// of the other types in a list.
function isTypeSubsumedByOtherType(type: Type, otherTypes: Type[], recursionCount = 0) {
const concreteType = makeTopLevelTypeVarsConcrete(type);
for (const otherType of otherTypes) {
if (isTypeSame(otherType, type)) {
continue;
}
if (!isAnyOrUnknown(otherType) && isProperSubtype(otherType, concreteType, recursionCount)) {
return true;
}
}
return false;
}
// Determines whether the srcType is a subtype of destType but the converse
// is not true. It's important that we check both directions to avoid
// matches for types like `tuple[Any]` and `tuple[int]` from being considered

View File

@ -0,0 +1,49 @@
# This sample tests that invariance is properly enforced when appropriate.
from typing import Any, TypeVar
T = TypeVar("T")
def func1(v: list[float | str]):
# This should generate an error.
x1: list[float] = v
x2: list[str | float] = v
# This should generate an error.
x3: list[float | str | None] = v
x4: list[Any | str] = v
# This should generate an error.
x5: list[int | str] = v
x6: list[float | int | str] = v
def func2(v: list[T]) -> T:
x1: list[T] = v
x2: list[Any | T] = v
# This should generate an error.
x3: list[int | T] = v
return v[0]
def func3(v: list[float | T]) -> float | T:
# This should generate an error.
x1: list[T] = v
x2: list[Any | T] = v
x3: list[T | float] = v
# This should generate an error.
x4: list[T | int] = v
x5: list[float | int | T] = v
return v[0]

View File

@ -1013,6 +1013,12 @@ test('GenericType44', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('GenericType45', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['genericType45.py']);
TestUtils.validateResults(analysisResults, 6);
});
test('Protocol1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol1.py']);