From b7e9ceaa2855f5ab4b101f974f034b52395f373c Mon Sep 17 00:00:00 2001 From: Daniel Rosenwasser Date: Sat, 19 Oct 2019 12:00:22 -0700 Subject: [PATCH] Perform weak type checks when performing comparisons. --- src/compiler/checker.ts | 49 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b83fa543a89..2f09796a029 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -873,6 +873,7 @@ namespace ts { const subtypeRelation = createMap(); const assignableRelation = createMap(); const comparableRelation = createMap(); + const castableRelation = createMap(); const identityRelation = createMap(); const enumRelation = createMap(); @@ -13394,6 +13395,18 @@ namespace ts { hasBaseType(source, getTargetType(target)); } + /** + * This is *not* a bi-directional relationship. + * If one needs to check both directions for castability, use a second call to this function or 'checkTypeCastableTo'. + * + * A type S is comparable to a type T if some (but not necessarily all) of the possible values of S are also possible values of T. + * It is used to check following cases: + * - the type of an expression in a type assertion with the type being asserted. + */ + function isTypeCastableTo(source: Type, target: Type): boolean { + return isTypeRelatedTo(source, target, castableRelation); + } + /** * This is *not* a bi-directional relationship. * If one needs to check both directions for comparability, use a second call to this function or 'checkTypeComparableTo'. @@ -13402,7 +13415,6 @@ namespace ts { * It is used to check following cases: * - the types of the left and right sides of equality/inequality operators (`===`, `!==`, `==`, `!=`). * - the types of `case` clause expressions and their respective `switch` expressions. - * - the type of an expression in a type assertion with the type being asserted. */ function isTypeComparableTo(source: Type, target: Type): boolean { return isTypeRelatedTo(source, target, comparableRelation); @@ -13835,6 +13847,14 @@ namespace ts { return elaborateElementwise(generateObjectLiteralElements(node), source, target, relation, containingMessageChain, errorOutputContainer); } + /** + * This is *not* a bi-directional relationship. + * If one needs to check both directions for comparability, use a second call to this function or 'isTypeComparableTo'. + */ + function checkTypeCastableTo(source: Type, target: Type, errorNode: Node, headMessage?: DiagnosticMessage, containingMessageChain?: () => DiagnosticMessageChain | undefined): boolean { + return checkTypeRelatedTo(source, target, castableRelation, errorNode, headMessage, containingMessageChain); + } + /** * This is *not* a bi-directional relationship. * If one needs to check both directions for comparability, use a second call to this function or 'isTypeComparableTo'. @@ -14133,7 +14153,7 @@ namespace ts { if (s & TypeFlags.Undefined && (!strictNullChecks || t & (TypeFlags.Undefined | TypeFlags.Void))) return true; if (s & TypeFlags.Null && (!strictNullChecks || t & TypeFlags.Null)) return true; if (s & TypeFlags.Object && t & TypeFlags.NonPrimitive) return true; - if (relation === assignableRelation || relation === comparableRelation) { + if (relation === assignableRelation || relation === comparableRelation || relation === castableRelation) { if (s & TypeFlags.Any) return true; // Type number or any numeric literal type is assignable to any numeric enum type or any // numeric enum literal type. This rule exists for backwards compatibility reasons because @@ -14152,7 +14172,7 @@ namespace ts { target = (target).regularType; } if (source === target || - relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || + (relation === comparableRelation || relation === castableRelation) && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || relation !== identityRelation && isSimpleTypeRelatedTo(source, target, relation)) { return true; } @@ -14176,7 +14196,7 @@ namespace ts { * Checks if 'source' is related to 'target' (e.g.: is a assignable to). * @param source The left-hand-side of the relation. * @param target The right-hand-side of the relation. - * @param relation The relation considered. One of 'identityRelation', 'subtypeRelation', 'assignableRelation', or 'comparableRelation'. + * @param relation The relation considered. One of 'identityRelation', 'subtypeRelation', 'assignableRelation', 'comparableRelation', or 'castableRelation'. * Used as both to determine which checks are performed and as a cache of previously computed results. * @param errorNode The suggested node upon which all errors will be reported, if defined. This may or may not be the actual node used. * @param headMessage If the error chain should be prepended by a head message, then headMessage will be used. @@ -14410,7 +14430,7 @@ namespace ts { } if (!message) { - if (relation === comparableRelation) { + if (relation === comparableRelation || relation === castableRelation) { message = Diagnostics.Type_0_is_not_comparable_to_type_1; } else if (sourceType === targetType) { @@ -14520,7 +14540,7 @@ namespace ts { return isIdenticalTo(source, target); } - if (relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || + if ((relation === comparableRelation || relation === castableRelation) && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True; const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes); @@ -14534,8 +14554,7 @@ namespace ts { return Ternary.False; } } - - const isPerformingCommonPropertyChecks = relation !== comparableRelation && !isApparentIntersectionConstituent && + const isPerformingCommonPropertyChecks = relation !== castableRelation && !isApparentIntersectionConstituent && source.flags & (TypeFlags.Primitive | TypeFlags.Object | TypeFlags.Intersection) && source !== globalObjectType && target.flags & (TypeFlags.Object | TypeFlags.Intersection) && isWeakType(target) && (getPropertiesOfType(source).length > 0 || typeHasCallOrConstructSignatures(source)); @@ -14562,7 +14581,7 @@ namespace ts { // we need to deconstruct unions before intersections (because unions are always at the top), // and we need to handle "each" relations before "some" relations for the same kind of type. if (source.flags & TypeFlags.Union) { - result = relation === comparableRelation ? + result = relation === comparableRelation || relation === castableRelation ? someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), isIntersectionConstituent) : eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive)); } @@ -14694,7 +14713,7 @@ namespace ts { } if (isExcessPropertyCheckTarget(target)) { const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes); - if ((relation === assignableRelation || relation === comparableRelation) && + if ((relation === assignableRelation || relation === comparableRelation || relation === castableRelation) && (isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) { return false; } @@ -15435,7 +15454,7 @@ namespace ts { // related to Y, where X' is an instantiation of X in which P is replaced with Q. Notice // that S and T are contra-variant whereas X and Y are co-variant. function mappedTypeRelatedTo(source: MappedType, target: MappedType, reportErrors: boolean): Ternary { - const modifiersRelated = relation === comparableRelation || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : + const modifiersRelated = relation === comparableRelation || relation === castableRelation || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target)); if (modifiersRelated) { let result: Ternary; @@ -15651,7 +15670,7 @@ namespace ts { return Ternary.False; } // When checking for comparability, be more lenient with optional properties. - if (relation !== comparableRelation && sourceProp.flags & SymbolFlags.Optional && !(targetProp.flags & SymbolFlags.Optional)) { + if ((relation !== comparableRelation && relation !== castableRelation) && sourceProp.flags & SymbolFlags.Optional && !(targetProp.flags & SymbolFlags.Optional)) { // TypeScript 1.0 spec (April 2014): 3.8.3 // S is a subtype of a type T, and T is a supertype of S if ... // S' and T are object types and, for each member M in T.. @@ -15848,7 +15867,7 @@ namespace ts { // in the context of the target signature before checking the relationship. Ideally we'd do // this regardless of the number of signatures, but the potential costs are prohibitive due // to the quadratic nature of the logic below. - const eraseGenerics = relation === comparableRelation || !!compilerOptions.noStrictGenericChecks; + const eraseGenerics = relation === comparableRelation || relation === castableRelation || !!compilerOptions.noStrictGenericChecks; result = signatureRelatedTo(sourceSignatures[0], targetSignatures[0], eraseGenerics, reportErrors, incompatibleReporter(sourceSignatures[0], targetSignatures[0])); } else { @@ -25245,8 +25264,8 @@ namespace ts { const targetType = getTypeFromTypeNode(type); if (produceDiagnostics && targetType !== errorType) { const widenedType = getWidenedType(exprType); - if (!isTypeComparableTo(targetType, widenedType)) { - checkTypeComparableTo(exprType, targetType, errNode, + if (!isTypeCastableTo(targetType, widenedType)) { + checkTypeCastableTo(exprType, targetType, errNode, Diagnostics.Conversion_of_type_0_to_type_1_may_be_a_mistake_because_neither_type_sufficiently_overlaps_with_the_other_If_this_was_intentional_convert_the_expression_to_unknown_first); } }