jsii-diff
Version:
Assembly comparison for jsii
530 lines • 20.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComparableEnumType = exports.ComparableStructType = exports.ComparableInterfaceType = exports.ComparableClassType = exports.ComparableReferenceType = exports.ComparableType = exports.AssemblyComparison = void 0;
const spec_1 = require("@jsii/spec");
const reflect = require("jsii-reflect");
const log4js = require("log4js");
const stability_1 = require("./stability");
const type_analysis_1 = require("./type-analysis");
const types_1 = require("./types");
const util_1 = require("./util");
const validations_1 = require("./validations");
const LOG = log4js.getLogger('jsii-diff');
/**
* Root object for comparing two assemblies
*
* Tracks mismatches and used as a lookup table to convert FQNs -> ComparableType objects
*/
class AssemblyComparison {
constructor(options) {
this.options = options;
this.types = new Map();
this.mismatches = new types_1.Mismatches({
defaultStability: options.defaultExperimental
? spec_1.Stability.Experimental
: spec_1.Stability.Stable,
});
}
/**
* Load the types from two assemblies to compare
*
* Adds appropriate ComparableType<> instances.
*/
load(original, updated) {
/* eslint-disable prettier/prettier */
for (const [origClass, updatedClass] of this.typePairs(original.allClasses, updated)) {
const { fqn, displayFqn } = this.resolveFqn(origClass);
this.types.set(fqn, new ComparableClassType(this, origClass, updatedClass, displayFqn));
}
for (const [origIface, updatedIface] of this.typePairs(original.allInterfaces, updated)) {
if (origIface.datatype !== updatedIface.datatype) {
this.mismatches.report({
ruleKey: 'iface-type',
violator: origIface,
message: `used to be a ${(0, types_1.describeInterfaceType)(origIface.datatype)}, is now a ${(0, types_1.describeInterfaceType)(updatedIface.datatype)}.`,
});
continue;
}
const { fqn, displayFqn } = this.resolveFqn(origIface);
this.types.set(fqn, origIface.datatype
? new ComparableStructType(this, origIface, updatedIface, displayFqn)
: new ComparableInterfaceType(this, origIface, updatedIface, displayFqn));
}
for (const [origEnum, updatedEnum] of this.typePairs(original.allEnums, updated)) {
const { fqn, displayFqn } = this.resolveFqn(origEnum);
this.types.set(fqn, new ComparableEnumType(this, origEnum, updatedEnum, displayFqn));
}
/* eslint-enable prettier/prettier */
}
/**
* Perform the comparison for all loaded types
*/
compare() {
LOG.debug(`Comparing ${this.comparableTypes.length} types`);
this.comparableTypes.forEach((t) => t.markTypeRoles());
this.comparableTypes.forEach((t) => t.compare());
}
/**
* Based on a JSII TypeReference, return all reachable ComparableType<> objects.
*/
typesIn(ref) {
const ret = new Array();
for (const fqn of fqnsFrom(ref)) {
const t = this.types.get(this.resolveFqn(fqn).fqn);
if (t) {
ret.push(t);
}
}
return ret;
}
/**
* Return the type's FQN, running it through the translation table if present.
*/
resolveFqn(x) {
const fqn = typeof x === 'string' ? x : x.fqn;
const finalFqn = this.options.fqnRemapping?.[fqn] ?? fqn;
if (fqn !== finalFqn) {
return { fqn: finalFqn, displayFqn: `${fqn} -> ${finalFqn}` };
}
return { fqn, displayFqn: fqn };
}
/**
* All ComparableType<>s
*/
get comparableTypes() {
return Array.from(this.types.values());
}
/**
* Find the matching type in the updated assembly based on all types in the original assembly
*/
*typePairs(xs, updatedAssembly) {
for (const origType of xs) {
const { fqn, displayFqn } = this.resolveFqn(origType);
LOG.trace(displayFqn);
const updatedType = updatedAssembly.tryFindType(fqn);
if (!updatedType) {
this.mismatches.report({
ruleKey: 'removed',
violator: origType,
message: 'has been removed',
});
continue;
}
if ((0, types_1.describeType)(origType) !== (0, types_1.describeType)(updatedType)) {
this.mismatches.report({
ruleKey: 'struct-change',
violator: origType,
message: `has been turned into a ${(0, types_1.describeType)(updatedType)}`,
});
continue;
}
yield [origType, updatedType]; // Trust me I know what I'm doing
}
}
}
exports.AssemblyComparison = AssemblyComparison;
/**
* Base class for comparable types
*
* Manages notions of crawling types for other reference types, and whether
* they occur in an input/output role, and marking as such on the comparison
* object.
*/
class ComparableType {
constructor(assemblyComparison, oldType, newType, displayFqn) {
this.assemblyComparison = assemblyComparison;
this.oldType = oldType;
this.newType = newType;
this.displayFqn = displayFqn;
this._inputTypeReasons = new Array();
this._outputTypeReasons = new Array();
}
get fqnRemapping() {
return this.assemblyComparison.options.fqnRemapping ?? {};
}
/**
* Does this type occur in an input role?
*/
get inputType() {
return this._inputTypeReasons.length > 0;
}
/**
* Does this type occur in an output role?
*/
get outputType() {
return this._outputTypeReasons.length > 0;
}
/**
* Mark this type as occurring in an input rule.
*
* All types reachable from this type will be marked as input types as well.
*/
markAsInputType(...reasonFragments) {
ComparableType.recursionBreaker.do(this, () => {
this._inputTypeReasons.push(reasonFragments.join(', '));
this.forEachRoleSharingType((type, reason) => {
type.markAsInputType(reason, ...reasonFragments);
});
});
}
/**
* Mark this type as occurring in an input rule.
*
* All types reachable from this type will be marked as input types as well.
*/
markAsOutputType(...reasonFragments) {
ComparableType.recursionBreaker.do(this, () => {
this._outputTypeReasons.push(reasonFragments.join(', '));
this.forEachRoleSharingType((type, reason) => {
type.markAsOutputType(reason, ...reasonFragments);
});
});
}
/**
* Describe why this type is an input type (if it is)
*/
get inputTypeReason() {
return describeReasons(this._inputTypeReasons);
}
/**
* Describe why this type is an output type (if it is)
*/
get outputTypeReason() {
return describeReasons(this._outputTypeReasons);
}
/**
* Should be overriden in subclasses to mark reachable types as input/output types
*
* Should only be implemented by subclasses that contain callables.
*/
markTypeRoles() {
// Empty on purpose
}
/**
* Alias for the root object Mismaches object
*/
get mismatches() {
return this.assemblyComparison.mismatches;
}
/**
* Should be overriden in subclasses to execute the callback on reachable types
*
* Should be overriden only for product types (structs).
*/
forEachRoleSharingType(cb) {
Array.isArray(cb);
}
}
exports.ComparableType = ComparableType;
ComparableType.recursionBreaker = new util_1.RecursionBreaker();
/**
* Base class for reference types
*
* Contains shared code that applies to both class and interface types.
*/
class ComparableReferenceType extends ComparableType {
/**
* Compare members of the reference types
*/
compare() {
LOG.debug(`Reference type ${this.displayFqn}`);
(0, stability_1.validateStabilities)(this.oldType, this.newType, this.mismatches);
(0, validations_1.validateBaseTypeAssignability)(this.oldType, this.newType, this.fqnRemapping, this.mismatches);
(0, validations_1.validateSubclassableNotRemoved)(this.oldType, this.newType, this.mismatches);
if (this.subclassableType) {
(0, validations_1.validateNoNewAbstractMembers)(this.oldType, this.newType, this.mismatches);
}
this.validateMethods();
this.validateProperties();
}
/**
* Mark type accesses (input/output) of methods and properties
*/
markTypeRoles() {
for (const method of this.oldType.ownMethods) {
determineTypeRolesFromMethod(this.assemblyComparison, method);
}
for (const property of this.oldType.ownProperties) {
determineTypeRolesFromProperty(this.assemblyComparison, property);
}
}
/**
* Validate type signatures on all methods
*/
validateMethods() {
for (const [orig, updated] of (0, validations_1.memberPairs)(this.oldType, this.oldType.allMethods, this.newType, this.mismatches)) {
if (reflect.isMethod(updated)) {
this.validateMethod(orig, updated);
}
}
}
/**
* Validate type signature changes on the given method
*/
validateMethod(original, updated) {
(0, validations_1.validateStaticSame)(original, updated, this.mismatches);
(0, validations_1.validateAsyncSame)(original, updated, this.mismatches);
if (this.subclassableType) {
(0, validations_1.validateReturnTypeSame)(original, updated, this.mismatches.withMotivation('type is @subclassable'));
}
else {
(0, validations_1.validateReturnTypeNotWeakened)(original, updated, this.fqnRemapping, this.mismatches);
}
this.validateCallable(original, updated);
}
/**
* Validate type signature changes on the given callable (method or initializer)
*/
validateCallable(original, updated) {
(0, stability_1.validateStabilities)(original, updated, this.mismatches);
(0, validations_1.validateNotMadeNonVariadic)(original, updated, this.mismatches);
// Check that every original parameter can still be mapped to a parameter in the updated method
(0, validations_1.validateExistingParams)(original, updated, this.mismatches, (oldParam, newParam) => {
if (this.subclassableType) {
(0, validations_1.validateParameterTypeSame)(original, oldParam, newParam, this.mismatches.withMotivation('type is @subclassable'));
}
else {
(0, validations_1.validateParameterTypeWeakened)(original, oldParam, newParam, this.fqnRemapping, this.mismatches);
}
});
(0, validations_1.validateNoNewRequiredParams)(original, updated, this.mismatches);
}
/**
* Validate type signature changes on all properties
*/
validateProperties() {
for (const [orig, updated] of (0, validations_1.memberPairs)(this.oldType, this.oldType.allProperties, this.newType, this.mismatches)) {
if (reflect.isProperty(updated)) {
this.validateProperty(orig, updated);
}
}
}
/**
* Validate type signature changes on the given property
*/
validateProperty(original, updated) {
(0, stability_1.validateStabilities)(original, updated, this.mismatches);
(0, validations_1.validateStaticSame)(original, updated, this.mismatches);
(0, validations_1.validateNotMadeImmutable)(original, updated, this.mismatches);
if (this.subclassableType) {
// Hello C# my old friend
(0, validations_1.validatePropertyTypeSame)(original, updated, this.mismatches.withMotivation('type is @subclassable'));
}
else if (!original.immutable) {
// If the type can be read, it can't be weakened (can't change Dog to Animal, consumers might be counting on a Dog).
// If the type can be written, it can't be strengthened (can't change Animal to Dog, consumers might be sending a Cat).
// => it must remain the same
(0, validations_1.validatePropertyTypeSame)(original, updated, this.mismatches.withMotivation('mutable property cannot change type'));
}
else {
(0, validations_1.validatePropertyTypeNotWeakened)(original, updated, this.fqnRemapping, this.mismatches);
}
}
/**
* Whether the current reference type has been marked as subclassable
*/
get subclassableType() {
return (0, validations_1.subclassableType)(this.oldType);
}
}
exports.ComparableReferenceType = ComparableReferenceType;
class ComparableClassType extends ComparableReferenceType {
/**
* Perform the reference type comparison and include class-specific checks
*/
compare() {
super.compare();
(0, validations_1.validateNotMadeAbstract)(this.oldType, this.newType, this.mismatches);
// JSII assembler has already taken care of inheritance here
if (this.oldType.initializer && this.newType.initializer) {
(0, validations_1.validateMethodCompatible)(this.oldType.initializer, this.newType.initializer, this.fqnRemapping, this.mismatches);
}
}
/**
* Type role marking -- include the initializer
*/
markTypeRoles() {
if (this.oldType.initializer) {
determineTypeRolesFromMethod(this.assemblyComparison, this.oldType.initializer);
}
super.markTypeRoles();
}
}
exports.ComparableClassType = ComparableClassType;
/**
* Interface type comparison
*
* (Actually just plain reference type comparison)
*/
class ComparableInterfaceType extends ComparableReferenceType {
}
exports.ComparableInterfaceType = ComparableInterfaceType;
/**
* Struct type comparison
*
* Most notably: does no-strengthening/no-weakening checks based on whether
* structs appear in input/output positions.
*/
class ComparableStructType extends ComparableType {
compare() {
LOG.debug(`Struct type ${this.displayFqn}`);
(0, stability_1.validateStabilities)(this.oldType, this.newType, this.mismatches);
(0, validations_1.validateBaseTypeAssignability)(this.oldType, this.newType, this.fqnRemapping, this.mismatches);
this.validateNoPropertiesRemoved();
if (this.inputType) {
// If the struct is written, it can't be strengthened (ex: can't change an optional property to required)
this.validateNotStrengthened(this.mismatches.withMotivation(this.inputTypeReason));
}
if (this.outputType) {
// If the struct is read, it can't be weakened (ex: can't change a required property to optional)
this.validateNotWeakened(this.mismatches.withMotivation(this.outputTypeReason));
}
}
/**
* Every type of every property should have the same in/out classification as the outer type
*/
forEachRoleSharingType(cb) {
for (const prop of this.oldType.allProperties) {
for (const t of this.assemblyComparison.typesIn(prop.type)) {
cb(t, `type of property ${prop.name}`);
}
}
}
/**
* Check that all properties are still present
*
* This is because for all non-structurally typed languages it is not allowed
* to specify members which aren't actually present in the type.
*/
validateNoPropertiesRemoved() {
// A single run of memberPairs() with nothing else will do this check.
Array.from((0, validations_1.memberPairs)(this.oldType, this.oldType.allProperties, this.newType, this.mismatches));
}
/**
* Check that the current type is not weakened
*/
validateNotWeakened(mismatches) {
const ana = this.isStructuralSuperType(this.oldType, this.newType);
if (!ana.success) {
mismatches.report({
ruleKey: 'weakened',
violator: this.oldType,
message: ana.reasons.join(', '),
});
}
}
/**
* Check that the current type is not strengthened
*/
validateNotStrengthened(mismatches) {
const ana = this.isStructuralSuperType(this.newType, this.oldType);
if (!ana.success) {
mismatches.report({
ruleKey: 'strengthened',
violator: this.oldType,
message: ana.reasons.join(', '),
});
}
}
isStructuralSuperType(a, b) {
try {
return new type_analysis_1.TypeAnalysis(this.newType.system, this.fqnRemapping).isStructuralSuperType(a, b);
}
catch (e) {
// We might get an exception if the type is supposed to come from a different
// assembly and the lookup fails.
return { success: false, reasons: [e.message] };
}
}
}
exports.ComparableStructType = ComparableStructType;
/**
* Comparison for enums
*/
class ComparableEnumType extends ComparableType {
/**
* Perform comparisons on enum members
*/
compare() {
LOG.debug(`Enum type ${this.displayFqn}`);
(0, stability_1.validateStabilities)(this.oldType, this.newType, this.mismatches);
(0, validations_1.validateExistingMembers)(this.oldType, this.newType, this.mismatches, (oldMember, newMember) => {
(0, stability_1.validateStabilities)(oldMember, newMember, this.mismatches);
});
}
}
exports.ComparableEnumType = ComparableEnumType;
/**
* Determines input/output roles of types used in this method
*
* - Argument types are treated as IN types
* - Return type is treated as OUT type
*/
function determineTypeRolesFromMethod(comparison, method) {
if (reflect.isMethod(method)) {
for (const t of comparison.typesIn(method.returns.type)) {
t.markAsOutputType(`returned from ${(0, types_1.apiElementIdentifier)(method)}`);
}
}
for (const param of method.parameters ?? []) {
for (const t of comparison.typesIn(param.type)) {
t.markAsInputType(`input to ${(0, types_1.apiElementIdentifier)(method)}`);
}
}
}
/**
* Determines input/output roles of types used in this property
*
* - Property type is treated as OUT type
* - If mutable, property type is also treated as IN type
*
* In effect, a property is treated as the following methods:
*
* - property(): T;
* - setProperty(: T); <- only if mutable
*/
function determineTypeRolesFromProperty(comparison, property) {
for (const t of comparison.typesIn(property.type)) {
t.markAsOutputType(`type of ${(0, types_1.apiElementIdentifier)(property)}`);
}
if (!property.immutable) {
for (const t of comparison.typesIn(property.type)) {
t.markAsInputType(`type of mutable ${(0, types_1.apiElementIdentifier)(property)}`);
}
}
}
/**
* Return all the FQNs from a type reference
*
* In the simple case, a simple FQN, but the type might
* be a union or complex type as well.
*/
function fqnsFrom(ref) {
const ret = new Array();
recurse(ref);
return ret;
function recurse(type) {
if (type.mapOfType) {
recurse(type.mapOfType);
}
else if (type.arrayOfType) {
recurse(type.arrayOfType);
}
else if (type.unionOfTypes) {
type.unionOfTypes.forEach(recurse);
}
else if (type.fqn) {
ret.push(type.fqn);
}
}
}
function describeReasons(reasons) {
if (reasons.length === 0) {
return '';
}
if (reasons.length === 1) {
return reasons[0];
}
return `${reasons[0]} (...and ${reasons.length - 1} more...)`;
}
//# sourceMappingURL=type-comparison.js.map