From 1cc72a317b8ca69d5d8168a09529fc02153fb530 Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Sat, 8 Jun 2019 21:55:19 -0400 Subject: [PATCH 1/4] Make alias assignments invariant Summary: This prevents unsafe operations since mutable covariant arrays can no longer be assigned to their contravariant. TODO: hide this new behavior behind a flag Test Plan: - yarn gulp build-min - node built/local/tsc.js --skipLibCheck --strict --noEmit foo.ts --- foo.ts | 13 ++++++++++ src/compiler/checker.ts | 57 +++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 foo.ts diff --git a/foo.ts b/foo.ts new file mode 100644 index 0000000000000..641268c9d3ca2 --- /dev/null +++ b/foo.ts @@ -0,0 +1,13 @@ +class Animal {} +class Dog extends Animal { bark() {} } +class Cat extends Animal { purr() {} } + +const cats: Array = [new Cat]; +const badCats: Array = []; +const goodAnimals: Array = [new Cat, new Dog]; +const readonlyAnimals: ReadonlyArray = cats; +const badAnimals: Array = cats; // error + +badAnimals.push(new Dog); +// Unsound and bad +cats.forEach(cat => cat.purr()); diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3ed6768cb3d72..0628dfa7ca01d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11676,7 +11676,7 @@ namespace ts { } function checkTypeRelatedToAndOptionallyElaborate(source: Type, target: Type, relation: Map, errorNode: Node | undefined, expr: Expression | undefined, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined): boolean { - if (isTypeRelatedTo(source, target, relation)) return true; + if (isTypeRelatedTo(source, target, relation, errorNode)) return true; if (!errorNode || !elaborateError(expr, source, target, relation, headMessage)) { return checkTypeRelatedTo(source, target, relation, errorNode, headMessage, containingMessageChain); } @@ -12317,7 +12317,7 @@ namespace ts { return false; } - function isTypeRelatedTo(source: Type, target: Type, relation: Map) { + function isTypeRelatedTo(source: Type, target: Type, relation: Map, errorNode?: Node) { if (isFreshLiteralType(source)) { source = (source).regularType; } @@ -12336,7 +12336,7 @@ namespace ts { } } if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) { - return checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined); + return checkTypeRelatedTo(source, target, relation, errorNode); } return false; } @@ -12378,7 +12378,7 @@ namespace ts { Debug.assert(relation !== identityRelation || !errorNode, "no error reporting in identity checking"); - const result = isRelatedTo(source, target, /*reportErrors*/ !!errorNode, headMessage); + const result = isRelatedTo(source, target, /*reportErrors*/ !!errorNode, {headMessage, errorNode}); if (overflow) { error(errorNode, Diagnostics.Excessive_stack_depth_comparing_types_0_and_1, typeToString(source), typeToString(target)); } @@ -12503,13 +12503,25 @@ namespace ts { return true; } + type RelatedToOptions = { + headMessage?: DiagnosticMessage, + isIntersectionConstituent?: boolean, + errorNode?: Node, + }; + /** * Compare two types and return * * Ternary.True if they are related with no assumptions, * * Ternary.Maybe if they are related with assumptions of other relationships, or * * Ternary.False if they are not related. */ - function isRelatedTo(source: Type, target: Type, reportErrors = false, headMessage?: DiagnosticMessage, isApparentIntersectionConstituent?: boolean): Ternary { + function isRelatedTo(source: Type, target: Type, reportErrors: boolean = false, options: RelatedToOptions = {}): Ternary { + const { + headMessage, + isIntersectionConstituent: isApparentIntersectionConstituent, + errorNode, + } = options; + if (isFreshLiteralType(source)) { source = (source).regularType; } @@ -12637,7 +12649,7 @@ namespace ts { result = someTypeRelatedToType(source, target, /*reportErrors*/ false); } if (!result && (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable)) { - if (result = recursiveTypeRelatedTo(source, target, reportErrors, isIntersectionConstituent)) { + if (result = recursiveTypeRelatedTo(source, target, reportErrors, {isIntersectionConstituent, errorNode})) { errorInfo = saveErrorInfo; } } @@ -12654,7 +12666,7 @@ namespace ts { // 'string & number | number & number' which reduces to just 'number'. const constraint = getUnionConstraintOfIntersection(source, !!(target.flags & TypeFlags.Union)); if (constraint) { - if (result = isRelatedTo(constraint, target, reportErrors, /*headMessage*/ undefined, isIntersectionConstituent)) { + if (result = isRelatedTo(constraint, target, reportErrors, {isIntersectionConstituent, errorNode})) { errorInfo = saveErrorInfo; } } @@ -12699,7 +12711,7 @@ namespace ts { let result: Ternary; const flags = source.flags & target.flags; if (flags & TypeFlags.Object || flags & TypeFlags.IndexedAccess || flags & TypeFlags.Conditional || flags & TypeFlags.Index || flags & TypeFlags.Substitution) { - return recursiveTypeRelatedTo(source, target, /*reportErrors*/ false, /*isIntersectionConstituent*/ false); + return recursiveTypeRelatedTo(source, target, /*reportErrors*/ false, {isIntersectionConstituent: false}); } if (flags & (TypeFlags.Union | TypeFlags.Intersection)) { if (result = eachTypeRelatedToSomeType(source, target)) { @@ -12894,7 +12906,7 @@ namespace ts { let result = Ternary.True; const targetTypes = target.types; for (const targetType of targetTypes) { - const related = isRelatedTo(source, targetType, reportErrors, /*headMessage*/ undefined, /*isIntersectionConstituent*/ true); + const related = isRelatedTo(source, targetType, reportErrors, {isIntersectionConstituent: true}); if (!related) { return Ternary.False; } @@ -12941,7 +12953,9 @@ namespace ts { // When variance information isn't available we default to covariance. This happens // in the process of computing variance information for recursive types and when // comparing 'this' type arguments. - const varianceFlags = i < variances.length ? variances[i] : VarianceFlags.Covariant; + // const varianceFlags = i < variances.length ? variances[i] : VarianceFlags.Covariant; + const varianceFlags = i < variances.length ? variances[i] : VarianceFlags.Invariant; + const variance = varianceFlags & VarianceFlags.VarianceMask; // We ignore arguments for independent type parameters (because they're never witnessed). if (variance !== VarianceFlags.Independent) { @@ -13005,7 +13019,7 @@ namespace ts { // Third, check if both types are part of deeply nested chains of generic type instantiations and if so assume the types are // equal and infinitely expanding. Fourth, if we have reached a depth of 100 nested comparisons, assume we have runaway recursion // and issue an error. Otherwise, actually compare the structure of the two types. - function recursiveTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, isIntersectionConstituent: boolean): Ternary { + function recursiveTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, options: RelatedToOptions = {}): Ternary { if (overflow) { return Ternary.False; } @@ -13056,7 +13070,7 @@ namespace ts { const saveExpandingFlags = expandingFlags; if (!(expandingFlags & ExpandingFlags.Source) && isDeeplyNestedType(source, sourceStack, depth)) expandingFlags |= ExpandingFlags.Source; if (!(expandingFlags & ExpandingFlags.Target) && isDeeplyNestedType(target, targetStack, depth)) expandingFlags |= ExpandingFlags.Target; - const result = expandingFlags !== ExpandingFlags.Both ? structuredTypeRelatedTo(source, target, reportErrors, isIntersectionConstituent) : Ternary.Maybe; + const result = expandingFlags !== ExpandingFlags.Both ? structuredTypeRelatedTo(source, target, reportErrors, options) : Ternary.Maybe; expandingFlags = saveExpandingFlags; depth--; if (result) { @@ -13077,7 +13091,7 @@ namespace ts { return result; } - function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, isIntersectionConstituent: boolean): Ternary { + function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, options: RelatedToOptions = {}): Ternary { const flags = source.flags & target.flags; if (relation === identityRelation && !(flags & TypeFlags.Object)) { if (flags & TypeFlags.Index) { @@ -13230,12 +13244,12 @@ namespace ts { } } // hi-speed no-this-instantiation check (less accurate, but avoids costly `this`-instantiation when the constraint will suffice), see #28231 for report on why this is needed - else if (result = isRelatedTo(constraint, target, /*reportErrors*/ false, /*headMessage*/ undefined, isIntersectionConstituent)) { + else if (result = isRelatedTo(constraint, target, /*reportErrors*/ false, options)) { errorInfo = saveErrorInfo; return result; } // slower, fuller, this-instantiated check (necessary when comparing raw `this` types from base classes), see `subclassWithPolymorphicThisIsAssignable.ts` test for example - else if (result = isRelatedTo(getTypeWithThisArgument(constraint, source), target, reportErrors, /*headMessage*/ undefined, isIntersectionConstituent)) { + else if (result = isRelatedTo(getTypeWithThisArgument(constraint, source), target, reportErrors, options)) { errorInfo = saveErrorInfo; return result; } @@ -13303,7 +13317,16 @@ namespace ts { // We have type references to the same generic type, and the type references are not marker // type references (which are intended by be compared structurally). Obtain the variance // information for the type parameters and relate the type arguments accordingly. - const variances = getVariances((source).target); + const {errorNode} = options; + let variances = getVariances((source).target); + if (errorNode && isVariableDeclaration(errorNode)) { + if (errorNode.initializer && isArrayLiteralExpression(errorNode.initializer)) { + // Array<> only takes a single type parameter + variances = [VarianceFlags.Covariant]; + } + } + + // const variances = [VarianceFlags.Covariant]; const varianceResult = relateVariances((source).typeArguments, (target).typeArguments, variances); if (varianceResult !== undefined) { return varianceResult; @@ -13557,7 +13580,7 @@ namespace ts { let result = unionParent ? Ternary.False : Ternary.True; const targetTypes = links.deferralConstituents!; for (const targetType of targetTypes) { - const related = isRelatedTo(source, targetType, /*reportErrors*/ false, /*headMessage*/ undefined, /*isIntersectionConstituent*/ !unionParent); + const related = isRelatedTo(source, targetType, /*reportErrors*/ false, {isIntersectionConstituent: !unionParent}); if (!unionParent) { if (!related) { // Can't assign to a target individually - have to fallback to assigning to the _whole_ intersection (which forces normalization) From cc2e80f285b029be367fed0ed19746147df20b2f Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Sun, 9 Jun 2019 14:14:19 -0400 Subject: [PATCH 2/4] handle arguments to function calls --- foo.ts | 18 ++++++++++++++---- src/compiler/checker.ts | 20 ++++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/foo.ts b/foo.ts index 641268c9d3ca2..24191b4709ce7 100644 --- a/foo.ts +++ b/foo.ts @@ -2,11 +2,21 @@ class Animal {} class Dog extends Animal { bark() {} } class Cat extends Animal { purr() {} } -const cats: Array = [new Cat]; -const badCats: Array = []; -const goodAnimals: Array = [new Cat, new Dog]; +const cats: Cat[] = []; +const badCats: Cat[] = []; +const goodAnimals: Animal[] = [new Cat, new Dog]; const readonlyAnimals: ReadonlyArray = cats; -const badAnimals: Array = cats; // error +const badAnimals: Animal[] = cats; // error + +const foo = (animals: Animal[]) => {}; +foo(goodAnimals); +foo([new Cat]); +foo(cats); // error + +const bar = (animals: ReadonlyArray) => {}; +bar(goodAnimals); +bar(cats); +bar([new Cat]); badAnimals.push(new Dog); // Unsound and bad diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0628dfa7ca01d..51723143eded4 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11676,7 +11676,7 @@ namespace ts { } function checkTypeRelatedToAndOptionallyElaborate(source: Type, target: Type, relation: Map, errorNode: Node | undefined, expr: Expression | undefined, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined): boolean { - if (isTypeRelatedTo(source, target, relation, errorNode)) return true; + if (isTypeRelatedTo(source, target, relation, expr)) return true; if (!errorNode || !elaborateError(expr, source, target, relation, headMessage)) { return checkTypeRelatedTo(source, target, relation, errorNode, headMessage, containingMessageChain); } @@ -12330,7 +12330,7 @@ namespace ts { return true; } if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) { - const related = relation.get(getRelationKey(source, target, relation)); + const related = relation.get(getRelationKey(source, target, relation, errorNode)); if (related !== undefined) { return related === RelationComparisonResult.Succeeded; } @@ -13023,7 +13023,7 @@ namespace ts { if (overflow) { return Ternary.False; } - const id = getRelationKey(source, target, relation); + const id = getRelationKey(source, target, relation, options.errorNode); const related = relation.get(id); if (related !== undefined) { if (reportErrors && related === RelationComparisonResult.Failed) { @@ -13324,6 +13324,10 @@ namespace ts { // Array<> only takes a single type parameter variances = [VarianceFlags.Covariant]; } + } else if (errorNode && isArrayLiteralExpression(errorNode)) { + // TODO: check that errorNode is child of a function call + // Array<> only takes a single type parameter + variances = [VarianceFlags.Covariant]; } // const variances = [VarianceFlags.Covariant]; @@ -14186,7 +14190,7 @@ namespace ts { * To improve caching, the relation key for two generic types uses the target's id plus ids of the type parameters. * For other cases, the types ids are used. */ - function getRelationKey(source: Type, target: Type, relation: Map) { + function getRelationKey(source: Type, target: Type, relation: Map, errorNode?: Node) { if (relation === identityRelation && source.id > target.id) { const temp = source; source = target; @@ -14196,6 +14200,14 @@ namespace ts { const typeParameters: Type[] = []; return getTypeReferenceId(source, typeParameters) + "," + getTypeReferenceId(target, typeParameters); } + // We differentiate array literal types from normal array types so that + // their type relations can be differentiated. In particular we want to + // assignment of array literals to be covariant and assignment of other + // arrays to be invariant. + // TODO: do the same for object literals + if (errorNode && errorNode.kind === SyntaxKind.ArrayLiteralExpression) { + return source.id + "," + target.id + ":Lit"; + } return source.id + "," + target.id; } From 0edb16c5bb7d170c9be40377aafaad027a687794 Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Mon, 10 Jun 2019 00:42:23 -0400 Subject: [PATCH 3/4] handle object properties that are union types --- foo.ts | 21 +++++++++++++++++++-- src/compiler/checker.ts | 26 +++++++++++++++++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/foo.ts b/foo.ts index 24191b4709ce7..1fb020fab3498 100644 --- a/foo.ts +++ b/foo.ts @@ -2,8 +2,7 @@ class Animal {} class Dog extends Animal { bark() {} } class Cat extends Animal { purr() {} } -const cats: Cat[] = []; -const badCats: Cat[] = []; +const cats: Cat[] = [new Cat]; const goodAnimals: Animal[] = [new Cat, new Dog]; const readonlyAnimals: ReadonlyArray = cats; const badAnimals: Animal[] = cats; // error @@ -21,3 +20,21 @@ bar([new Cat]); badAnimals.push(new Dog); // Unsound and bad cats.forEach(cat => cat.purr()); + +type NumberNode = { id: number }; +type MixedNode = { id: number | string }; +const numNode: NumberNode = { id: 5 }; +const goodMixedNode: MixedNode = { id: 5 }; +const badMixedNode: MixedNode = numNode; // error +badMixedNode.id = "five"; // whoops, now node0 has a string for its "id" +const readonlyMixedNode: Readonly = numNode; // this is okay + +const mixedNodeFunc = (node: MixedNode) => {}; +mixedNodeFunc(goodMixedNode); +mixedNodeFunc(numNode); +mixedNodeFunc({ id: 5 }); + +const readonlyMixedNodeFunc = (node: Readonly) => {}; +readonlyMixedNodeFunc(goodMixedNode); +readonlyMixedNodeFunc(numNode); +readonlyMixedNodeFunc({ id: 5 }); diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 51723143eded4..b7f177e0cc99f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11676,6 +11676,10 @@ namespace ts { } function checkTypeRelatedToAndOptionallyElaborate(source: Type, target: Type, relation: Map, errorNode: Node | undefined, expr: Expression | undefined, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined): boolean { + // TODO: pass both errorNode and expr since each of them are useful in different contexts + // for function calls, expr is the arg + // for variable declarations, expr is the right-hand-side expression which is usually enough + // but when checking properties we need to know if the left-hand-side is readonly if (isTypeRelatedTo(source, target, relation, expr)) return true; if (!errorNode || !elaborateError(expr, source, target, relation, headMessage)) { return checkTypeRelatedTo(source, target, relation, errorNode, headMessage, containingMessageChain); @@ -12507,6 +12511,9 @@ namespace ts { headMessage?: DiagnosticMessage, isIntersectionConstituent?: boolean, errorNode?: Node, + isProperty?: boolean, + targetIsReadonly?: boolean, + sourceIsObjectLiteral?: boolean, }; /** @@ -12613,6 +12620,11 @@ namespace ts { } else { if (target.flags & TypeFlags.Union) { + // If the target is a union and the source isn't but this check is for + // a property return false + if (options.isProperty && !options.targetIsReadonly && !options.sourceIsObjectLiteral) { + return Ternary.False; + } result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive)); if (result && isPerformingExcessPropertyChecks) { // Validate against excess props using the original `source` @@ -13572,7 +13584,8 @@ namespace ts { return result || properties; } - function isPropertySymbolTypeRelated(sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean): Ternary { + function isPropertySymbolTypeRelated(sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean, targetIsReadonly: boolean, sourceIsObjectLiteral: boolean): Ternary { + const relatedToOptions: RelatedToOptions = {isProperty: true, targetIsReadonly, sourceIsObjectLiteral}; const targetIsOptional = strictNullChecks && !!(getCheckFlags(targetProp) & CheckFlags.Partial); const source = getTypeOfSourceProperty(sourceProp); if (getCheckFlags(targetProp) & CheckFlags.DeferredType && !getSymbolLinks(targetProp).type) { @@ -13588,7 +13601,7 @@ namespace ts { if (!unionParent) { if (!related) { // Can't assign to a target individually - have to fallback to assigning to the _whole_ intersection (which forces normalization) - return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors); + return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors, relatedToOptions); } result &= related; } @@ -13606,12 +13619,12 @@ namespace ts { // If it turns out this is too costly too often, we can replicate the error handling logic within // typeRelatedToSomeType without the discriminatable type branch (as that requires a manifest union // type on which to hand discriminable properties, which we are expressly trying to avoid here) - return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors); + return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors, relatedToOptions); } return result; } else { - return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors); + return isRelatedTo(source, addOptionality(getTypeOfSymbol(targetProp), targetIsOptional), reportErrors, relatedToOptions); } } @@ -13657,7 +13670,10 @@ namespace ts { return Ternary.False; } // If the target comes from a partial union prop, allow `undefined` in the target type - const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors); + const targetIsReadonly = !!(getObjectFlags(target) & ObjectFlags.Mapped && + (target).declaration.readonlyToken); + const sourceIsObjectLiteral = !!(getObjectFlags(source) & ObjectFlags.ObjectLiteral); + const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors, targetIsReadonly, sourceIsObjectLiteral); if (!related) { if (reportErrors) { reportError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp)); From 0fc3c3b63f09ae0fe427a54dfcad570d66b7a7e7 Mon Sep 17 00:00:00 2001 From: Kevin Barabash Date: Sat, 15 Jun 2019 17:03:07 -0400 Subject: [PATCH 4/4] check that objects' properties are invariant in aliasing assignments --- foo.ts | 11 ++++++++++- src/compiler/checker.ts | 40 ++++++++++++++++++++++++++++++--------- src/compiler/utilities.ts | 11 ++++++++++- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/foo.ts b/foo.ts index 1fb020fab3498..9e95926e88d64 100644 --- a/foo.ts +++ b/foo.ts @@ -31,10 +31,19 @@ const readonlyMixedNode: Readonly = numNode; // this is okay const mixedNodeFunc = (node: MixedNode) => {}; mixedNodeFunc(goodMixedNode); -mixedNodeFunc(numNode); +mixedNodeFunc(numNode); // error, mixedNodeFunc could mutate numNode to set "id" to be a string mixedNodeFunc({ id: 5 }); const readonlyMixedNodeFunc = (node: Readonly) => {}; readonlyMixedNodeFunc(goodMixedNode); readonlyMixedNodeFunc(numNode); readonlyMixedNodeFunc({ id: 5 }); + +type CatNode = { animal: Cat }; +type AnimalNode = { animal: Animal }; +const catNode: CatNode = { animal: new Cat }; +const goodAnimalNode: AnimalNode = { animal: new Cat }; +const badAnimalNode: AnimalNode = catNode; // error + +// const a: number = 5; +// const b: number | string = a; diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b7f177e0cc99f..f38b33fca6349 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -80,6 +80,8 @@ namespace ts { const strictFunctionTypes = getStrictOptionValue(compilerOptions, "strictFunctionTypes"); const strictBindCallApply = getStrictOptionValue(compilerOptions, "strictBindCallApply"); const strictPropertyInitialization = getStrictOptionValue(compilerOptions, "strictPropertyInitialization"); + const strictAliasAssigment = getStrictOptionValue(compilerOptions, "strictAliasAssignment"); + const strictAliasArgs = getStrictOptionValue(compilerOptions, "strictAliasArgs"); const noImplicitAny = getStrictOptionValue(compilerOptions, "noImplicitAny"); const noImplicitThis = getStrictOptionValue(compilerOptions, "noImplicitThis"); const keyofStringsOnly = !!compilerOptions.keyofStringsOnly; @@ -11680,7 +11682,7 @@ namespace ts { // for function calls, expr is the arg // for variable declarations, expr is the right-hand-side expression which is usually enough // but when checking properties we need to know if the left-hand-side is readonly - if (isTypeRelatedTo(source, target, relation, expr)) return true; + if (isTypeRelatedTo(source, target, relation, errorNode)) return true; if (!errorNode || !elaborateError(expr, source, target, relation, headMessage)) { return checkTypeRelatedTo(source, target, relation, errorNode, headMessage, containingMessageChain); } @@ -13364,7 +13366,7 @@ namespace ts { if (source.flags & (TypeFlags.Object | TypeFlags.Intersection) && target.flags & TypeFlags.Object) { // Report structural errors only if we haven't reported any errors yet const reportStructuralErrors = reportErrors && errorInfo === saveErrorInfo && !sourceIsPrimitive; - result = propertiesRelatedTo(source, target, reportStructuralErrors, /*excludedProperties*/ undefined); + result = propertiesRelatedTo(source, target, reportStructuralErrors, /*excludedProperties*/ undefined, options); if (result) { result &= signaturesRelatedTo(source, target, SignatureKind.Call, reportStructuralErrors); if (result) { @@ -13584,8 +13586,13 @@ namespace ts { return result || properties; } - function isPropertySymbolTypeRelated(sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean, targetIsReadonly: boolean, sourceIsObjectLiteral: boolean): Ternary { - const relatedToOptions: RelatedToOptions = {isProperty: true, targetIsReadonly, sourceIsObjectLiteral}; + function isPropertySymbolTypeRelated(sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean, targetIsReadonly: boolean, sourceIsObjectLiteral: boolean, options: RelatedToOptions = {}): Ternary { + const relatedToOptions: RelatedToOptions = { + ...options, + targetIsReadonly, + sourceIsObjectLiteral, + isProperty: true, + }; const targetIsOptional = strictNullChecks && !!(getCheckFlags(targetProp) & CheckFlags.Partial); const source = getTypeOfSourceProperty(sourceProp); if (getCheckFlags(targetProp) & CheckFlags.DeferredType && !getSymbolLinks(targetProp).type) { @@ -13628,7 +13635,7 @@ namespace ts { } } - function propertyRelatedTo(source: Type, target: Type, sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean): Ternary { + function propertyRelatedTo(source: Type, target: Type, sourceProp: Symbol, targetProp: Symbol, getTypeOfSourceProperty: (sym: Symbol) => Type, reportErrors: boolean, options: RelatedToOptions = {}): Ternary { const sourcePropFlags = getDeclarationModifierFlagsFromSymbol(sourceProp); const targetPropFlags = getDeclarationModifierFlagsFromSymbol(targetProp); if (sourcePropFlags & ModifierFlags.Private || targetPropFlags & ModifierFlags.Private) { @@ -13673,7 +13680,7 @@ namespace ts { const targetIsReadonly = !!(getObjectFlags(target) & ObjectFlags.Mapped && (target).declaration.readonlyToken); const sourceIsObjectLiteral = !!(getObjectFlags(source) & ObjectFlags.ObjectLiteral); - const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors, targetIsReadonly, sourceIsObjectLiteral); + const related = isPropertySymbolTypeRelated(sourceProp, targetProp, getTypeOfSourceProperty, reportErrors, targetIsReadonly, sourceIsObjectLiteral, options); if (!related) { if (reportErrors) { reportError(Diagnostics.Types_of_property_0_are_incompatible, symbolToString(targetProp)); @@ -13698,9 +13705,24 @@ namespace ts { return related; } - function propertiesRelatedTo(source: Type, target: Type, reportErrors: boolean, excludedProperties: UnderscoreEscapedMap | undefined): Ternary { + function propertiesRelatedTo(source: Type, target: Type, reportErrors: boolean, excludedProperties: UnderscoreEscapedMap | undefined, options: RelatedToOptions = {}): Ternary { if (relation === identityRelation) { return propertiesIdenticalTo(source, target, excludedProperties); + } else if (options.errorNode && options.errorNode.kind !== SyntaxKind.ClassDeclaration && options.errorNode.kind !== SyntaxKind.ObjectLiteralExpression) { + const {errorNode} = options; + if (target.aliasSymbol && target.aliasSymbol.escapedName === "Readonly") { + // If the target is Readonly treat the source as covariant + } else if (isVariableDeclaration(errorNode) && errorNode.initializer && errorNode.initializer.kind !== SyntaxKind.ObjectLiteralExpression) { + debugger; + // Invariant when assigning an alias as a source + const isCovariant = propertiesIdenticalTo(source, target, excludedProperties); + const isContravariant = propertiesIdenticalTo(target, source, excludedProperties); + if (isCovariant === Ternary.True && isContravariant === Ternary.True) { + return Ternary.True; + } else { + return Ternary.False; + } + } } const requireOptionalProperties = relation === subtypeRelation && !isObjectLiteralType(source) && !isEmptyArrayLiteralType(source) && !isTupleType(source); const unmatchedProperty = getUnmatchedProperty(source, target, requireOptionalProperties, /*matchDiscriminantProperties*/ false); @@ -13785,7 +13807,7 @@ namespace ts { if (!(targetProp.flags & SymbolFlags.Prototype)) { const sourceProp = getPropertyOfType(source, targetProp.escapedName); if (sourceProp && sourceProp !== targetProp) { - const related = propertyRelatedTo(source, target, sourceProp, targetProp, getTypeOfSymbol, reportErrors); + const related = propertyRelatedTo(source, target, sourceProp, targetProp, getTypeOfSymbol, reportErrors, options); if (!related) { return Ternary.False; } @@ -28141,7 +28163,7 @@ namespace ts { } else { // Report static side error only when instance type is assignable - checkTypeAssignableTo(staticType, getTypeWithoutSignatures(staticBaseType), node.name || node, + checkTypeAssignableTo(staticType, getTypeWithoutSignatures(staticBaseType), node, // node.name || node, Diagnostics.Class_static_side_0_incorrectly_extends_base_class_static_side_1); } if (baseConstructorType.flags & TypeFlags.TypeVariable && !isMixinConstructorType(staticType)) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 0ec81502cf799..23f69da15bd76 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7259,7 +7259,16 @@ namespace ts { return !!(options.incremental || options.composite); } - export type StrictOptionName = "noImplicitAny" | "noImplicitThis" | "strictNullChecks" | "strictFunctionTypes" | "strictBindCallApply" | "strictPropertyInitialization" | "alwaysStrict"; + export type StrictOptionName = + | "noImplicitAny" + | "noImplicitThis" + | "strictNullChecks" + | "strictFunctionTypes" + | "strictBindCallApply" + | "strictPropertyInitialization" + | "strictAliasAssignment" + | "strictAliasArgs" + | "alwaysStrict"; export function getStrictOptionValue(compilerOptions: CompilerOptions, flag: StrictOptionName): boolean { return compilerOptions[flag] === undefined ? !!compilerOptions.strict : !!compilerOptions[flag];