packages/jsii-diff/lib/type-analysis.ts (147 lines of code) (raw):

/* eslint-disable complexity */ import * as reflect from 'jsii-reflect'; import { flatMap } from './util'; /** * Check whether A is a supertype of B * * Put differently: whether any value of type B would be assignable to a * variable of type A. * * We always check the relationship in the NEW (latest, updated) typesystem. */ export function isSuperType( a: reflect.TypeReference, b: reflect.TypeReference, updatedSystem: reflect.TypeSystem, ): Analysis { if (a.void || b.void) { throw new Error('isSuperType() does not handle voids'); } if (a.isAny) { return { success: true }; } if (a.primitive !== undefined) { if (a.primitive === b.primitive) { return { success: true }; } return failure(`${b.toString()} is not assignable to ${a.toString()}`); } if (a.arrayOfType !== undefined) { // Arrays are covariant if (b.arrayOfType === undefined) { return failure(`${b.toString()} is not an array type`); } return prependReason( isSuperType(a.arrayOfType, b.arrayOfType, updatedSystem), `${b.toString()} is not assignable to ${a.toString()}`, ); } if (a.mapOfType !== undefined) { // Maps are covariant (are they?) if (b.mapOfType === undefined) { return failure(`${b.toString()} is not a map type`); } return prependReason( isSuperType(a.mapOfType, b.mapOfType, updatedSystem), `${b.toString()} is not assignable to ${a.toString()}`, ); } // Every element of B can be assigned to A if (b.unionOfTypes !== undefined) { const analyses = b.unionOfTypes.map((bbb) => isSuperType(a, bbb, updatedSystem), ); if (analyses.every((x) => x.success)) { return { success: true }; } return failure( `some of ${b.toString()} are not assignable to ${a.toString()}`, ...flatMap(analyses, (x) => (x.success ? [] : x.reasons)), ); } // There should be an element of A which can accept all of B if (a.unionOfTypes !== undefined) { const analyses = a.unionOfTypes.map((aaa) => isSuperType(aaa, b, updatedSystem), ); if (analyses.some((x) => x.success)) { return { success: true }; } return failure( `none of ${b.toString()} are assignable to ${a.toString()}`, ...flatMap(analyses, (x) => (x.success ? [] : x.reasons)), ); } // We have two named types, recursion might happen so protect against it. try { // For named types, we'll always do a nominal typing relationship. // That is, if in the updated typesystem someone were to use the type name // from the old assembly, do they have a typing relationship that's accepted // by a nominal type system. (That check also rules out enums) const nominalCheck = isNominalSuperType(a, b, updatedSystem); if (nominalCheck.success === false) { return nominalCheck; } // At this point, the only thing left to do is recurse into the structs. // We used to do that here, but we don't anymore; structs check themselves // for structural weakening/strengthening. return { success: true }; } catch (e: any) { return failure(e.message); } } /** * Find types A and B in the updated type system, and check whether they have a supertype relationship in the type system */ export function isNominalSuperType( a: reflect.TypeReference, b: reflect.TypeReference, updatedSystem: reflect.TypeSystem, ): Analysis { if (a.fqn === undefined) { throw new Error(`I was expecting a named type, got '${a.toString()}'`); } // Named type vs a non-named type if (b.fqn === undefined) { return failure(`${b.toString()} is not assignable to ${a.toString()}`); } // Short-circuit of the types are the same name, saves us some lookup if (a.fqn === b.fqn) { return { success: true }; } // We now need to do subtype analysis on the // Find A in B's typesystem, and see if B is a subtype of A' const B = updatedSystem.tryFindFqn(b.fqn); const A = updatedSystem.tryFindFqn(a.fqn); if (!B) { return failure(`could not find type ${b.toString()}`); } if (!A) { return failure(`could not find type ${a.toString()}`); } // If they're enums, they should have been exactly the same (tested above) // enums are never subtypes of any other type. if (A.isEnumType()) { return failure(`${a.toString()} is an enum different from ${b.toString()}`); } if (B.isEnumType()) { return failure(`${b.toString()} is an enum different from ${a.toString()}`); } if (B.extends(A)) { return { success: true }; } return failure(`${b.toString()} does not extend ${a.toString()}`); } /** * Is struct A a structural supertype of struct B? * * Trying to answer the question, is this assignment legal for all values * of `expr in B`. * * ```ts * const var: A = expr as B; * ``` * * A is a structural supertype of B if all required members of A are also * required in B, and of a compatible type. * * Nullable members of A must either not exist in B, or be of a compatible * type. */ export function isStructuralSuperType( a: reflect.InterfaceType, b: reflect.InterfaceType, updatedSystem: reflect.TypeSystem, ): Analysis { // We know all members can only be properties, so that makes it easier. const bProps = b.getProperties(true); // Use timing words in error message to make it more understandable const formerly = b.system === updatedSystem ? 'formerly' : 'newly'; const is = b.system === updatedSystem ? 'is' : 'used to be'; const removed = b.system === updatedSystem ? 'removed' : 'added'; for (const [name, aProp] of Object.entries(a.getProperties(true))) { const bProp = bProps[name]; if (aProp.optional) { // Optional field, only requirement is that IF it exists, the type must match. if (!bProp) { continue; } } else { if (!bProp) { return failure(`${formerly} required property '${name}' ${removed}`); } if (bProp.optional) { return failure( `${formerly} required property '${name}' ${is} optional`, ); } } const ana = isSuperType(aProp.type, bProp.type, updatedSystem); if (!ana.success) { return failure(`property ${name}`, ...ana.reasons); } } return { success: true }; } // Oh tagged union types I love you so much! export type Analysis = { success: true } | FailedAnalysis; export type FailedAnalysis = { success: false; reasons: string[] }; function failure(...reasons: string[]): FailedAnalysis { return { success: false, reasons }; } export function prependReason(analysis: Analysis, message: string): Analysis { if (analysis.success) { return analysis; } return failure(message, ...analysis.reasons); }