UNPKG

jsii-diff

Version:

Assembly comparison for jsii

486 lines 16.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateBaseTypeAssignability = validateBaseTypeAssignability; exports.validateNotMadeAbstract = validateNotMadeAbstract; exports.validateSubclassableNotRemoved = validateSubclassableNotRemoved; exports.validateStaticSame = validateStaticSame; exports.validateAsyncSame = validateAsyncSame; exports.validateNotMadeNonVariadic = validateNotMadeNonVariadic; exports.validateNoNewAbstractMembers = validateNoNewAbstractMembers; exports.validateReturnTypeNotWeakened = validateReturnTypeNotWeakened; exports.validateReturnTypeSame = validateReturnTypeSame; exports.validatePropertyTypeNotWeakened = validatePropertyTypeNotWeakened; exports.validatePropertyTypeSame = validatePropertyTypeSame; exports.validateParameterTypeWeakened = validateParameterTypeWeakened; exports.validateParameterTypeSame = validateParameterTypeSame; exports.validateExistingParams = validateExistingParams; exports.validateNoNewRequiredParams = validateNoNewRequiredParams; exports.validateMethodCompatible = validateMethodCompatible; exports.subclassableType = subclassableType; exports.validateNotMadeImmutable = validateNotMadeImmutable; exports.memberPairs = memberPairs; exports.validateExistingMembers = validateExistingMembers; const reflect = require("jsii-reflect"); const log4js = require("log4js"); const stability_1 = require("./stability"); const type_analysis_1 = require("./type-analysis"); const LOG = log4js.getLogger('jsii-diff'); /** * The updated type is still nominally assignable to all original base types * * Make sure the following remains compilable: * * ``` * BASE instance = new CLASS(); * ``` * * Where CLASS ≤: BASE. */ function validateBaseTypeAssignability(original, updated, fqnRemapping, mismatches) { const ana = assignableToAllBaseTypes(original, updated, fqnRemapping); if (!ana.success) { mismatches.report({ ruleKey: 'base-types', message: `not assignable to all base types anymore: ${ana.reasons.join(', ')}`, violator: original, }); } } /** * The updated type has not been newly made abstract * * Make sure the following remains compilable: * * ``` * new CLASS(); * ``` */ function validateNotMadeAbstract(original, updated, mismatches) { if (updated.abstract && !original.abstract) { mismatches.report({ ruleKey: 'made-abstract', message: 'has gone from non-abstract to abstract', violator: original, }); } } /** * The updated type has not had its @subclassable attribute removed * * This would lift a restriction we can't afford. */ function validateSubclassableNotRemoved(original, updated, mismatches) { if (original.docs.subclassable && !updated.docs.subclassable) { mismatches.report({ ruleKey: 'remove-subclassable', message: 'has gone from @subclassable to non-@subclassable', violator: original, }); } } /** * Check that the `static`-ness of a member hasn't changed */ function validateStaticSame(original, updated, mismatches) { if (original.static !== updated.static) { mismatches.report({ ruleKey: 'changed-static', violator: original, message: `used to be ${original.static ? 'static' : 'not static'}, is now ${updated.static ? 'static' : 'not static'}`, }); } } /** * Check that the `async`-ness of a method hasn't changed */ function validateAsyncSame(original, updated, mismatches) { if (original.async !== updated.async) { const origQual = original.async ? 'asynchronous' : 'synchronous'; const updQual = updated.async ? 'asynchronous' : 'synchronous'; mismatches.report({ ruleKey: 'changed-async', violator: original, message: `was ${origQual}, is now ${updQual}`, }); } } /** * Once variadic, can never be made non-variadic anymore (because I could always have been passing N+1 arguments) */ function validateNotMadeNonVariadic(original, updated, mismatches) { if (original.variadic && !updated.variadic) { mismatches.report({ ruleKey: 'changed-variadic', violator: original, message: 'used to be variadic, not variadic anymore.', }); } } /** * Check that no new abstract members were added to a subclassable type * * You cannot have added abstract members to the class/interface, as they are * an added burden on potential implementors. */ function validateNoNewAbstractMembers(original, updated, mismatches) { const absMemberNames = new Set(updated.allMembers.filter((m) => m.abstract).map((m) => m.name)); const originalMemberNames = new Set(original.allMembers.map((m) => m.name)); for (const name of absMemberNames) { if (!originalMemberNames.has(name)) { mismatches.report({ ruleKey: 'new-abstract-member', message: `adds requirement for subclasses to implement '${name}'.`, violator: updated.getMembers(true)[name], }); } } } /** * Validate that a method return type is the same or strengthened * * Make sure the following remains compilable: * * ``` * T value = object.method(); * ``` * * Where RETURN_TYPE(method) ≤: T. */ function validateReturnTypeNotWeakened(original, updated, fqnRemapping, mismatches) { const retAna = isCompatibleReturnType(original.returns, updated.returns, fqnRemapping); if (!retAna.success) { mismatches.report({ ruleKey: 'change-return-type', violator: original, message: `returns ${describeOptionalValueMatchingFailure(original.returns, updated.returns, retAna)}`, }); } } /** * Validate that a method return type is the exact same * * Necessary for subclassable types in C#. */ function validateReturnTypeSame(original, updated, mismatches) { const origDescr = reflect.OptionalValue.describe(original.returns); const updaDescr = reflect.OptionalValue.describe(updated.returns); if (origDescr !== updaDescr) { mismatches.report({ ruleKey: 'change-return-type', violator: original, message: `returns ${updaDescr} (formerly ${origDescr})`, }); } } /** * Validate that a property type is the same or strengthened * * Make sure the following remains compilable: * * ``` * T value = object.prop; * ``` * * Where RETURN_TYPE(prop) ≤: T. */ function validatePropertyTypeNotWeakened(original, updated, fqnRemapping, mismatches) { const ana = isCompatibleReturnType(original, updated, fqnRemapping); if (!ana.success) { mismatches.report({ ruleKey: 'changed-type', violator: original, message: `type ${describeOptionalValueMatchingFailure(original, updated, ana)}`, }); } } /** * Validate that a property type is the exact same * * Necessary for subclassable types in C#. */ function validatePropertyTypeSame(original, updated, mismatches) { const oldDesc = reflect.OptionalValue.describe(original); const newDesc = reflect.OptionalValue.describe(updated); if (oldDesc !== newDesc) { mismatches.report({ ruleKey: 'changed-type', violator: original, message: `changed to ${newDesc} (formerly ${oldDesc})`, }); } } /** * Validate that a method return type is the same or weakened * * Make sure the following remains compilable if U is changed: * * ``` * function method(arg: U) { ... } * * object.method(<T>value); * ``` * * Where T ≤: U. */ function validateParameterTypeWeakened(method, original, updated, fqnRemapping, mismatches) { const argAna = isCompatibleArgumentType(original.type, updated.type, fqnRemapping); if (!argAna.success) { mismatches.report({ ruleKey: 'incompatible-argument', violator: method, message: `argument ${original.name}, takes ${describeOptionalValueMatchingFailure(original, updated, argAna)}`, }); return; } } /** * Validate that a method parameter type is the exact same * * Necessary for subclassable types in C#. */ function validateParameterTypeSame(method, original, updated, mismatches) { if (original.type.toString() !== updated.type.toString()) { mismatches.report({ ruleKey: 'incompatible-argument', violator: method, message: `argument ${original.name}, takes ${updated.type.toString()} (formerly ${original.type.toString()}): type is @subclassable`, }); } } function describeOptionalValueMatchingFailure(origType, updatedType, analysis) { const origDescr = reflect.OptionalValue.describe(origType); const updaDescr = reflect.OptionalValue.describe(updatedType); if (origDescr !== updaDescr) { return `${updaDescr} (formerly ${origDescr}): ${analysis.reasons.join(', ')}`; } return `${updaDescr}: ${analysis.reasons.join(', ')}`; } /** * Validate that each param in the old callable is still available in the new callable, and apply custom validation to the pairs * * Make sure the following remains compilable: * * ``` * object.method(a1, a2, ..., aN); * ``` * * (All types still assignable) */ function validateExistingParams(original, updated, mismatches, validateParam) { original.parameters.forEach((param, i) => { const updatedParam = findParam(updated.parameters, i); if (updatedParam === undefined) { mismatches.report({ ruleKey: 'removed-argument', violator: original, message: `argument ${param.name}, not accepted anymore.`, }); return; } validateParam(param, updatedParam); }); } /** * Validate that no new required params got added to the end of the method * * Make sure the following remains compilable: * * ``` * object.method(a1, a2, ..., aN); * ``` * * (Not too few arguments) */ function validateNoNewRequiredParams(original, updated, mismatches) { updated.parameters.forEach((param, i) => { if (param.optional) { return; } const origParam = findParam(original.parameters, i); if (!origParam || origParam.optional) { mismatches.report({ ruleKey: 'new-argument', violator: original, message: `argument ${param.name}, newly required argument.`, }); } }); } function validateMethodCompatible(original, updated, fqnRemapping, mismatches) { (0, stability_1.validateStabilities)(original, updated, mismatches); // Type guards on original are duplicated on updated to help tsc... They are required to be the same type by the declaration. if (reflect.isMethod(original) && reflect.isMethod(updated)) { validateStaticSame(original, updated, mismatches); validateAsyncSame(original, updated, mismatches); validateReturnTypeNotWeakened(original, updated, fqnRemapping, mismatches); } validateNotMadeNonVariadic(original, updated, mismatches); // Check that every original parameter can still be mapped to a parameter in the updated method validateExistingParams(original, updated, mismatches, (oldParam, newParam) => { validateParameterTypeWeakened(original, oldParam, newParam, fqnRemapping, mismatches); }); validateNoNewRequiredParams(original, updated, mismatches); } /** * Check if a class/interface has been marked as @subclassable */ function subclassableType(x) { return x.docs.subclassable; } /** * Find the indicated parameter with the given index * * May return the last parameter if it's variadic */ function findParam(parameters, i) { if (i < parameters.length) { return parameters[i]; } const lastParam = parameters.length > 0 ? parameters[parameters.length - 1] : undefined; if (lastParam && lastParam.variadic) { return lastParam; } return undefined; } /** * Validate that a previously mutable property is not made immutable * * Make sure the following remains compilable: * * ``` * object.prop = value; * ``` */ function validateNotMadeImmutable(original, updated, mismatches) { if (updated.immutable && !original.immutable) { mismatches.report({ ruleKey: 'removed-mutability', violator: original, message: 'used to be mutable, is now immutable', }); } } function* memberPairs(origClass, xs, updatedClass, mismatches) { for (const origMember of xs) { LOG.trace(`${origClass.fqn}#${origMember.name}`); const updatedMember = updatedClass.allMembers.find((m) => m.name === origMember.name); if (!updatedMember) { mismatches.report({ ruleKey: 'removed', violator: origMember, message: 'has been removed', }); continue; } if (origMember.kind !== updatedMember.kind) { mismatches.report({ ruleKey: 'changed-kind', violator: origMember, message: `changed from ${origMember.kind} to ${updatedMember.kind}`, }); } if (!origMember.protected && updatedMember.protected) { mismatches.report({ ruleKey: 'hidden', violator: origMember, message: "changed from 'public' to 'protected'", }); } yield [origMember, updatedMember]; } } /** * Whether we are strengthening the postcondition (output type of a method or property) * * Strengthening output values is allowed! */ function isCompatibleReturnType(original, updated, fqnRemapping) { if (original.type.void) { return { success: true }; } // If we didn't use to return anything, returning something now is fine if (updated.type.void) { return { success: false, reasons: ["now returning 'void'"] }; } // If we used to return something, we can't stop doing that if (!original.optional && updated.optional) { return { success: false, reasons: ['output type is now optional'] }; } return new type_analysis_1.TypeAnalysis(updated.system, fqnRemapping).isSuperType(original.type, updated.type); } /** * Whether we are weakening the pre (input type of a method) * * Weakening preconditions is allowed! */ function isCompatibleArgumentType(original, updated, fqnRemapping) { // Input can never be void, so no need to check return new type_analysis_1.TypeAnalysis(updated.system, fqnRemapping).isSuperType(updated, original); } /** * Verify assignability to supertypes * * For every base type B of type T, someone could have written: * * ``` * const variable: B = new T(); * ``` * * This code needs to be valid in the updated assembly, so for each * B an updated type B' needs to exist in the new assembly which is * still a supertype of T'. */ function assignableToAllBaseTypes(original, updated, fqnRemapping) { for (const B of baseTypes(original)) { const result = new type_analysis_1.TypeAnalysis(updated.system, fqnRemapping).isNominalSuperType(B.reference, updated.reference); if (!result.success) { return result; } } return { success: true }; } /** * Return all base types of the given reference type */ function baseTypes(type) { const ret = new Array(); const todo = [type]; const seen = new Set(); while (todo.length > 0) { const next = todo.pop(); if (seen.has(next.fqn)) { continue; } ret.push(next); seen.add(next.fqn); todo.push(...next.interfaces); if (next.isClassType() && next.base) { todo.push(next.base); } } return ret; } /** * Validate that each enum member in the old enum enum, and apply custom validation to the enums * * Make sure the following remains compilable: * * ``` * T x = ENUM.member; * ``` * * (For every member of enum) */ function validateExistingMembers(original, updated, mismatches, validateMember) { for (const origMember of original.members) { const updatedMember = updated.members.find((m) => m.name === origMember.name); if (!updatedMember) { mismatches.report({ ruleKey: 'removed', violator: origMember, message: `member ${origMember.name} has been removed`, }); continue; } validateMember(origMember, updatedMember); } } //# sourceMappingURL=validations.js.map