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

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) {