Fixed bug that leads to inconsistent narrowing-on-assignment behavior when a tuple instance with unknown type arguments is assigned to a target symbol with a declared tuple type. This addresses #8001. (#8005)

This commit is contained in:
Eric Traut 2024-05-26 23:42:37 -07:00 committed by GitHub
parent a4d165ec02
commit 59eabcb60f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 35 additions and 5 deletions

View File

@ -25952,7 +25952,9 @@ export function createTypeEvaluator(
if (isAnyOrUnknown(expectedTypeArgType) || isAnyOrUnknown(typeArg)) {
replacedTypeArg = true;
return expectedTypeArgType;
} else if (isClassInstance(expectedTypeArgType) && isClassInstance(typeArg)) {
}
if (isClassInstance(expectedTypeArgType) && isClassInstance(typeArg)) {
// Recursively replace Any in the type argument.
const recursiveReplacement = replaceTypeArgsWithAny(
node,
@ -25960,6 +25962,7 @@ export function createTypeEvaluator(
typeArg,
recursionCount
);
if (recursiveReplacement) {
replacedTypeArg = true;
return recursiveReplacement;
@ -26004,6 +26007,12 @@ export function createTypeEvaluator(
// When a value is assigned to a variable with a declared type,
// we may be able to narrow the type based on the assignment.
function narrowTypeBasedOnAssignment(node: ExpressionNode, declaredType: Type, assignedType: Type): Type {
// TODO: The rules for narrowing types on assignment are defined in
// the typing spec. Pyright's current logic is currently not even internally
// consistent and probably not sound from a type theory perspective. It
// should be completely reworked once there has been a public discussion
// about the correct behavior.
const narrowedType = mapSubtypes(assignedType, (assignedSubtype) => {
// Handle the special case where the assigned type is a literal type.
// Some types include very large unions of literal types, and we don't
@ -26045,8 +26054,18 @@ export function createTypeEvaluator(
}
// If the declared type doesn't contain any `Any` but the assigned
// type does, stick with the declared type.
if (containsAnyRecursive(assignedSubtype) && !containsAnyRecursive(declaredSubtype)) {
// type does, stick with the declared type. We don't include unknowns
// in the assigned subtype check here so unknowns are preserved so
// reportUnknownVariableType assignment diagnostics are reported.
// TODO - this is an inconsistency because Any and Unknown should
// always be treated the same for purposes of type narrowing. This
// should be revisited once the narrowing-on-assignment behavior
// is properly specified in the typing spec.
if (
containsAnyRecursive(assignedSubtype, /* includeUnknown */ false) &&
!containsAnyRecursive(declaredSubtype)
) {
return declaredSubtype;
}

View File

@ -2551,17 +2551,28 @@ export function getMembersForModule(moduleType: ModuleType, symbolTable: SymbolT
}
// Determines if the type contains an Any recursively.
export function containsAnyRecursive(type: Type): boolean {
export function containsAnyRecursive(type: Type, includeUnknown = true): boolean {
class AnyWalker extends TypeWalker {
foundAny = false;
constructor(private _includeUnknown: boolean) {
super();
}
override visitAny(type: AnyType) {
this.foundAny = true;
this.cancelWalk();
}
override visitUnknown(type: UnknownType): void {
if (this._includeUnknown) {
this.foundAny = true;
this.cancelWalk();
}
}
}
const walker = new AnyWalker();
const walker = new AnyWalker(includeUnknown);
walker.walk(type);
return walker.foundAny;
}