Consistently propagate intersectionState in relations (#52392)

This commit is contained in:
Anders Hejlsberg
2023-02-02 07:19:00 -08:00
committed by GitHub
parent 9ee33156f5
commit 30993855fd
7 changed files with 226 additions and 51 deletions
+49 -39
View File
@@ -1261,8 +1261,8 @@ export const enum SignatureCheckMode {
const enum IntersectionState {
None = 0,
Source = 1 << 0,
Target = 1 << 1,
Source = 1 << 0, // Source type is a constituent of an outer intersection
Target = 1 << 1, // Target type is a constituent of an outer intersection
}
const enum RecursionFlags {
@@ -19990,7 +19990,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let overrideNextErrorInfo = 0; // How many `reportRelationError` calls should be skipped in the elaboration pyramid
let lastSkippedInfo: [Type, Type] | undefined;
let incompatibleStack: [DiagnosticMessage, (string | number)?, (string | number)?, (string | number)?, (string | number)?][] | undefined;
let inPropertyCheck = false;
Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking");
@@ -20955,30 +20954,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
result = isRelatedTo(constraint, target, RecursionFlags.Source, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState);
}
}
// For certain combinations involving intersections and optional, excess, or mismatched properties we need
// an extra property check where the intersection is viewed as a single object. The following are motivating
// examples that all should be errors, but aren't without this extra property check:
// When the target is an intersection we need an extra property check in order to detect nested excess
// properties and nested weak types. The following are motivating examples that all should be errors, but
// aren't without this extra property check:
//
// let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
//
// declare let wrong: { a: { y: string } };
// let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
//
if (result && !(intersectionState & IntersectionState.Target) && target.flags & TypeFlags.Intersection &&
!isGenericObjectType(target) && source.flags & (TypeFlags.Object | TypeFlags.Intersection)) {
result &= propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, /*optionalsOnly*/ false, IntersectionState.None);
if (result && isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral) {
result &= indexSignaturesRelatedTo(source, target, /*sourceIsPrimitive*/ false, reportErrors, IntersectionState.None);
}
}
// When the source is an intersection we need an extra check of any optional properties in the target to
// detect possible mismatched property types. For example:
//
// function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
// x = y; // Mismatched property in source intersection
// }
//
// We suppress recursive intersection property checks because they can generate lots of work when relating
// recursive intersections that are structurally similar but not exactly identical. See #37854.
if (result && !inPropertyCheck && (
target.flags & TypeFlags.Intersection && !isGenericObjectType(target) && source.flags & (TypeFlags.Object | TypeFlags.Intersection) ||
isNonGenericObjectType(target) && !isArrayOrTupleType(target) && source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType && !some((source as IntersectionType).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) {
inPropertyCheck = true;
result &= propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None);
if (result && isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral) {
result &= indexSignaturesRelatedTo(source, target, /*sourceIsPrimitive*/ false, reportErrors, IntersectionState.None);
}
inPropertyCheck = false;
else if (result && isNonGenericObjectType(target) && !isArrayOrTupleType(target) &&
source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType &&
!some((source as IntersectionType).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType))) {
result &= propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, /*optionalsOnly*/ true, intersectionState);
}
}
if (result) {
@@ -21486,11 +21488,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (sourceFlags & (TypeFlags.Object | TypeFlags.Intersection) && targetFlags & TypeFlags.Object) {
// Report structural errors only if we haven't reported any errors yet
const reportStructuralErrors = reportErrors && errorInfo === saveErrorInfo.errorInfo && !sourceIsPrimitive;
result = propertiesRelatedTo(source, target, reportStructuralErrors, /*excludedProperties*/ undefined, intersectionState);
result = propertiesRelatedTo(source, target, reportStructuralErrors, /*excludedProperties*/ undefined, /*optionalsOnly*/ false, intersectionState);
if (result) {
result &= signaturesRelatedTo(source, target, SignatureKind.Call, reportStructuralErrors);
result &= signaturesRelatedTo(source, target, SignatureKind.Call, reportStructuralErrors, intersectionState);
if (result) {
result &= signaturesRelatedTo(source, target, SignatureKind.Construct, reportStructuralErrors);
result &= signaturesRelatedTo(source, target, SignatureKind.Construct, reportStructuralErrors, intersectionState);
if (result) {
result &= indexSignaturesRelatedTo(source, target, sourceIsPrimitive, reportStructuralErrors, intersectionState);
}
@@ -21661,11 +21663,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Compare the remaining non-discriminant properties of each match.
let result = Ternary.True;
for (const type of matchingTypes) {
result &= propertiesRelatedTo(source, type, /*reportErrors*/ false, excludedProperties, IntersectionState.None);
result &= propertiesRelatedTo(source, type, /*reportErrors*/ false, excludedProperties, /*optionalsOnly*/ false, IntersectionState.None);
if (result) {
result &= signaturesRelatedTo(source, type, SignatureKind.Call, /*reportStructuralErrors*/ false);
result &= signaturesRelatedTo(source, type, SignatureKind.Call, /*reportStructuralErrors*/ false, IntersectionState.None);
if (result) {
result &= signaturesRelatedTo(source, type, SignatureKind.Construct, /*reportStructuralErrors*/ false);
result &= signaturesRelatedTo(source, type, SignatureKind.Construct, /*reportStructuralErrors*/ false, IntersectionState.None);
if (result && !(isTupleType(source) && isTupleType(type))) {
// Comparing numeric index types when both `source` and `type` are tuples is unnecessary as the
// element types should be sufficiently covered by `propertiesRelatedTo`. It also causes problems
@@ -21828,7 +21830,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// No array like or unmatched property error - just issue top level error (errorInfo = undefined)
}
function propertiesRelatedTo(source: Type, target: Type, reportErrors: boolean, excludedProperties: Set<__String> | undefined, intersectionState: IntersectionState): Ternary {
function propertiesRelatedTo(source: Type, target: Type, reportErrors: boolean, excludedProperties: Set<__String> | undefined, optionalsOnly: boolean, intersectionState: IntersectionState): Ternary {
if (relation === identityRelation) {
return propertiesIdenticalTo(source, target, excludedProperties);
}
@@ -21963,7 +21965,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const numericNamesOnly = isTupleType(source) && isTupleType(target);
for (const targetProp of excludeProperties(properties, excludedProperties)) {
const name = targetProp.escapedName;
if (!(targetProp.flags & SymbolFlags.Prototype) && (!numericNamesOnly || isNumericLiteralName(name) || name === "length")) {
if (!(targetProp.flags & SymbolFlags.Prototype) && (!numericNamesOnly || isNumericLiteralName(name) || name === "length") && (!optionalsOnly || targetProp.flags & SymbolFlags.Optional)) {
const sourceProp = getPropertyOfType(source, name);
if (sourceProp && sourceProp !== targetProp) {
const related = propertyRelatedTo(source, target, sourceProp, targetProp, getNonMissingTypeOfSymbol, reportErrors, intersectionState, relation === comparableRelation);
@@ -22001,7 +22003,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}
function signaturesRelatedTo(source: Type, target: Type, kind: SignatureKind, reportErrors: boolean): Ternary {
function signaturesRelatedTo(source: Type, target: Type, kind: SignatureKind, reportErrors: boolean, intersectionState: IntersectionState): Ternary {
if (relation === identityRelation) {
return signaturesIdenticalTo(source, target, kind);
}
@@ -22046,7 +22048,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// of the much more expensive N * M comparison matrix we explore below. We erase type parameters
// as they are known to always be the same.
for (let i = 0; i < targetSignatures.length; i++) {
const related = signatureRelatedTo(sourceSignatures[i], targetSignatures[i], /*erase*/ true, reportErrors, incompatibleReporter(sourceSignatures[i], targetSignatures[i]));
const related = signatureRelatedTo(sourceSignatures[i], targetSignatures[i], /*erase*/ true, reportErrors, intersectionState, incompatibleReporter(sourceSignatures[i], targetSignatures[i]));
if (!related) {
return Ternary.False;
}
@@ -22062,7 +22064,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const eraseGenerics = relation === comparableRelation || !!compilerOptions.noStrictGenericChecks;
const sourceSignature = first(sourceSignatures);
const targetSignature = first(targetSignatures);
result = signatureRelatedTo(sourceSignature, targetSignature, eraseGenerics, reportErrors, incompatibleReporter(sourceSignature, targetSignature));
result = signatureRelatedTo(sourceSignature, targetSignature, eraseGenerics, reportErrors, intersectionState, incompatibleReporter(sourceSignature, targetSignature));
if (!result && reportErrors && kind === SignatureKind.Construct && (sourceObjectFlags & targetObjectFlags) &&
(targetSignature.declaration?.kind === SyntaxKind.Constructor || sourceSignature.declaration?.kind === SyntaxKind.Constructor)) {
const constructSignatureToString = (signature: Signature) =>
@@ -22078,7 +22080,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Only elaborate errors from the first failure
let shouldElaborateErrors = reportErrors;
for (const s of sourceSignatures) {
const related = signatureRelatedTo(s, t, /*erase*/ true, shouldElaborateErrors, incompatibleReporter(s, t));
const related = signatureRelatedTo(s, t, /*erase*/ true, shouldElaborateErrors, intersectionState, incompatibleReporter(s, t));
if (related) {
result &= related;
resetErrorInfo(saveErrorInfo);
@@ -22128,9 +22130,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
/**
* See signatureAssignableTo, compareSignaturesIdentical
*/
function signatureRelatedTo(source: Signature, target: Signature, erase: boolean, reportErrors: boolean, incompatibleReporter: (source: Type, target: Type) => void): Ternary {
function signatureRelatedTo(source: Signature, target: Signature, erase: boolean, reportErrors: boolean, intersectionState: IntersectionState, incompatibleReporter: (source: Type, target: Type) => void): Ternary {
return compareSignaturesRelated(erase ? getErasedSignature(source) : source, erase ? getErasedSignature(target) : target,
relation === strictSubtypeRelation ? SignatureCheckMode.StrictArity : 0, reportErrors, reportError, incompatibleReporter, isRelatedToWorker, reportUnreliableMapper);
function isRelatedToWorker(source: Type, target: Type, reportErrors?: boolean) {
return isRelatedTo(source, target, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState);
}
}
function signaturesIdenticalTo(source: Type, target: Type, kind: SignatureKind): Ternary {
@@ -22176,7 +22181,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
for (const info of getIndexInfosOfType(source)) {
if (isApplicableIndexType(info.keyType, keyType)) {
const related = indexInfoRelatedTo(info, targetInfo, reportErrors);
const related = indexInfoRelatedTo(info, targetInfo, reportErrors, intersectionState);
if (!related) {
return Ternary.False;
}
@@ -22186,8 +22191,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}
function indexInfoRelatedTo(sourceInfo: IndexInfo, targetInfo: IndexInfo, reportErrors: boolean) {
const related = isRelatedTo(sourceInfo.type, targetInfo.type, RecursionFlags.Both, reportErrors);
function indexInfoRelatedTo(sourceInfo: IndexInfo, targetInfo: IndexInfo, reportErrors: boolean, intersectionState: IntersectionState) {
const related = isRelatedTo(sourceInfo.type, targetInfo.type, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState);
if (!related && reportErrors) {
if (sourceInfo.keyType === targetInfo.keyType) {
reportError(Diagnostics._0_index_signatures_are_incompatible, typeToString(sourceInfo.keyType));
@@ -22221,7 +22226,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function typeRelatedToIndexInfo(source: Type, targetInfo: IndexInfo, reportErrors: boolean, intersectionState: IntersectionState): Ternary {
const sourceInfo = getApplicableIndexInfo(source, targetInfo.keyType);
if (sourceInfo) {
return indexInfoRelatedTo(sourceInfo, targetInfo, reportErrors);
return indexInfoRelatedTo(sourceInfo, targetInfo, reportErrors, intersectionState);
}
if (!(intersectionState & IntersectionState.Source) && isObjectTypeWithInferableIndex(source)) {
// Intersection constituents are never considered to have an inferred index signature
@@ -22597,10 +22602,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// Return true if the given type is deeply nested. We consider this to be the case when structural type comparisons
// for maxDepth or more occurrences or instantiations of the type have been recorded on the given stack. It is possible,
// though highly unlikely, for this test to be true in a situation where a chain of instantiations is not infinitely
// expanding. Effectively, we will generate a false positive when two types are structurally equal to at least maxDepth
// levels, but unequal at some level beyond that.
// for maxDepth or more occurrences or instantiations of the same type have been recorded on the given stack. The
// "sameness" of instantiations is determined by the getRecursionIdentity function. An intersection is considered
// deeply nested if any constituent of the intersection is deeply nested. It is possible, though highly unlikely, for
// the deeply nested check to be true in a situation where a chain of instantiations is not infinitely expanding.
// Effectively, we will generate a false positive when two types are structurally equal to at least maxDepth levels,
// but unequal at some level beyond that.
// In addition, this will also detect when an indexed access has been chained off of maxDepth more times (which is
// essentially the dual of the structural comparison), and likewise mark the type as deeply nested, potentially adding
// false positives for finite but deeply expanding indexed accesses (eg, for `Q[P1][P2][P3][P4][P5]`).
@@ -22610,12 +22617,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// to terminate the expansion, and we do so here.
function isDeeplyNestedType(type: Type, stack: Type[], depth: number, maxDepth = 3): boolean {
if (depth >= maxDepth) {
if (type.flags & TypeFlags.Intersection) {
return some((type as IntersectionType).types, t => isDeeplyNestedType(t, stack, depth, maxDepth));
}
const identity = getRecursionIdentity(type);
let count = 0;
let lastTypeId = 0;
for (let i = 0; i < depth; i++) {
const t = stack[i];
if (getRecursionIdentity(t) === identity) {
if (t.flags & TypeFlags.Intersection ? some((t as IntersectionType).types, u => getRecursionIdentity(u) === identity) : getRecursionIdentity(t) === identity) {
// We only count occurrences with a higher type id than the previous occurrence, since higher
// type ids are an indicator of newer instantiations caused by recursion.
if (t.id >= lastTypeId) {
@@ -67,4 +67,25 @@ tests/cases/compiler/nestedExcessPropertyChecking.ts(40,9): error TS2559: Type '
!!! related TS6500 tests/cases/compiler/nestedExcessPropertyChecking.ts:35:24: The expected type comes from property 'overrides' which is declared here on type 'VariablesA & VariablesB'
}
};
// Simplified repro from #52252
type T1 = {
primary: { __typename?: 'Feature' } & { colors: { light: number, dark: number } },
};
type T2 = {
primary: { __typename?: 'Feature' } & { colors: { light: number } },
};
type Query = T1 & T2;
const response: Query = {
primary: {
colors: {
light: 1,
dark: 3,
},
},
};
@@ -41,6 +41,27 @@ const foo2: Unrelated & { variables: VariablesA & VariablesB } = {
overrides: false // Error
}
};
// Simplified repro from #52252
type T1 = {
primary: { __typename?: 'Feature' } & { colors: { light: number, dark: number } },
};
type T2 = {
primary: { __typename?: 'Feature' } & { colors: { light: number } },
};
type Query = T1 & T2;
const response: Query = {
primary: {
colors: {
light: 1,
dark: 3,
},
},
};
//// [nestedExcessPropertyChecking.js]
@@ -59,3 +80,11 @@ var foo2 = {
overrides: false // Error
}
};
var response = {
primary: {
colors: {
light: 1,
dark: 3,
},
},
};
@@ -113,3 +113,53 @@ const foo2: Unrelated & { variables: VariablesA & VariablesB } = {
}
};
// Simplified repro from #52252
type T1 = {
>T1 : Symbol(T1, Decl(nestedExcessPropertyChecking.ts, 41, 2))
primary: { __typename?: 'Feature' } & { colors: { light: number, dark: number } },
>primary : Symbol(primary, Decl(nestedExcessPropertyChecking.ts, 45, 11))
>__typename : Symbol(__typename, Decl(nestedExcessPropertyChecking.ts, 46, 14))
>colors : Symbol(colors, Decl(nestedExcessPropertyChecking.ts, 46, 43))
>light : Symbol(light, Decl(nestedExcessPropertyChecking.ts, 46, 53))
>dark : Symbol(dark, Decl(nestedExcessPropertyChecking.ts, 46, 68))
};
type T2 = {
>T2 : Symbol(T2, Decl(nestedExcessPropertyChecking.ts, 47, 2))
primary: { __typename?: 'Feature' } & { colors: { light: number } },
>primary : Symbol(primary, Decl(nestedExcessPropertyChecking.ts, 49, 11))
>__typename : Symbol(__typename, Decl(nestedExcessPropertyChecking.ts, 50, 14))
>colors : Symbol(colors, Decl(nestedExcessPropertyChecking.ts, 50, 43))
>light : Symbol(light, Decl(nestedExcessPropertyChecking.ts, 50, 53))
};
type Query = T1 & T2;
>Query : Symbol(Query, Decl(nestedExcessPropertyChecking.ts, 51, 2))
>T1 : Symbol(T1, Decl(nestedExcessPropertyChecking.ts, 41, 2))
>T2 : Symbol(T2, Decl(nestedExcessPropertyChecking.ts, 47, 2))
const response: Query = {
>response : Symbol(response, Decl(nestedExcessPropertyChecking.ts, 55, 5))
>Query : Symbol(Query, Decl(nestedExcessPropertyChecking.ts, 51, 2))
primary: {
>primary : Symbol(primary, Decl(nestedExcessPropertyChecking.ts, 55, 25))
colors: {
>colors : Symbol(colors, Decl(nestedExcessPropertyChecking.ts, 56, 14))
light: 1,
>light : Symbol(light, Decl(nestedExcessPropertyChecking.ts, 57, 17))
dark: 3,
>dark : Symbol(dark, Decl(nestedExcessPropertyChecking.ts, 58, 21))
},
},
};
@@ -106,3 +106,55 @@ const foo2: Unrelated & { variables: VariablesA & VariablesB } = {
}
};
// Simplified repro from #52252
type T1 = {
>T1 : { primary: { __typename?: 'Feature';} & { colors: { light: number; dark: number; };}; }
primary: { __typename?: 'Feature' } & { colors: { light: number, dark: number } },
>primary : { __typename?: "Feature" | undefined; } & { colors: { light: number; dark: number;}; }
>__typename : "Feature" | undefined
>colors : { light: number; dark: number; }
>light : number
>dark : number
};
type T2 = {
>T2 : { primary: { __typename?: 'Feature';} & { colors: { light: number; };}; }
primary: { __typename?: 'Feature' } & { colors: { light: number } },
>primary : { __typename?: "Feature" | undefined; } & { colors: { light: number;}; }
>__typename : "Feature" | undefined
>colors : { light: number; }
>light : number
};
type Query = T1 & T2;
>Query : T1 & T2
const response: Query = {
>response : Query
>{ primary: { colors: { light: 1, dark: 3, }, },} : { primary: { colors: { light: number; dark: number; }; }; }
primary: {
>primary : { colors: { light: number; dark: number; }; }
>{ colors: { light: 1, dark: 3, }, } : { colors: { light: number; dark: number; }; }
colors: {
>colors : { light: number; dark: number; }
>{ light: 1, dark: 3, } : { light: number; dark: number; }
light: 1,
>light : number
>1 : 1
dark: 3,
>dark : number
>3 : 3
},
},
};
@@ -5,12 +5,8 @@ tests/cases/conformance/types/thisType/unionThisTypeInFunctions.ts(10,5): error
Type '(this: Real, n: number) => void' is not assignable to type '(this: Fake, n: number) => void'.
The 'this' types of each signature are incompatible.
Type 'Fake' is not assignable to type 'Real'.
Types of property 'method' are incompatible.
Type '(this: Fake, n: number) => void' is not assignable to type '(this: Real, n: number) => void'.
The 'this' types of each signature are incompatible.
Type 'Real' is not assignable to type 'Fake'.
Types of property 'data' are incompatible.
Type 'string' is not assignable to type 'number'.
Types of property 'data' are incompatible.
Type 'number' is not assignable to type 'string'.
==== tests/cases/conformance/types/thisType/unionThisTypeInFunctions.ts (1 errors) ====
@@ -32,11 +28,7 @@ tests/cases/conformance/types/thisType/unionThisTypeInFunctions.ts(10,5): error
!!! error TS2684: Type '(this: Real, n: number) => void' is not assignable to type '(this: Fake, n: number) => void'.
!!! error TS2684: The 'this' types of each signature are incompatible.
!!! error TS2684: Type 'Fake' is not assignable to type 'Real'.
!!! error TS2684: Types of property 'method' are incompatible.
!!! error TS2684: Type '(this: Fake, n: number) => void' is not assignable to type '(this: Real, n: number) => void'.
!!! error TS2684: The 'this' types of each signature are incompatible.
!!! error TS2684: Type 'Real' is not assignable to type 'Fake'.
!!! error TS2684: Types of property 'data' are incompatible.
!!! error TS2684: Type 'string' is not assignable to type 'number'.
!!! error TS2684: Types of property 'data' are incompatible.
!!! error TS2684: Type 'number' is not assignable to type 'string'.
}
@@ -42,3 +42,24 @@ const foo2: Unrelated & { variables: VariablesA & VariablesB } = {
overrides: false // Error
}
};
// Simplified repro from #52252
type T1 = {
primary: { __typename?: 'Feature' } & { colors: { light: number, dark: number } },
};
type T2 = {
primary: { __typename?: 'Feature' } & { colors: { light: number } },
};
type Query = T1 & T2;
const response: Query = {
primary: {
colors: {
light: 1,
dark: 3,
},
},
};