jsii-diff
Version:
Assembly comparison for jsii
486 lines • 16.8 kB
JavaScript
"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