packages/jsii-diff/lib/validations.ts (440 lines of code) (raw):
import * as reflect from 'jsii-reflect';
import * as log4js from 'log4js';
import { validateStabilities } from './stability';
import {
Analysis,
FailedAnalysis,
isSuperType,
isNominalSuperType,
} from './type-analysis';
import { IReport } from './types';
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.
*/
export function validateBaseTypeAssignability<T extends reflect.ReferenceType>(
original: T,
updated: T,
mismatches: IReport,
) {
const ana = assignableToAllBaseTypes(original, updated);
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();
* ```
*/
export function validateNotMadeAbstract(
original: reflect.ClassType,
updated: reflect.ClassType,
mismatches: IReport,
) {
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.
*/
export function validateSubclassableNotRemoved<T extends reflect.ReferenceType>(
original: T,
updated: T,
mismatches: IReport,
) {
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
*/
export function validateStaticSame<T extends reflect.Method | reflect.Property>(
original: T,
updated: T,
mismatches: IReport,
) {
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
*/
export function validateAsyncSame(
original: reflect.Method,
updated: reflect.Method,
mismatches: IReport,
) {
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)
*/
export function validateNotMadeNonVariadic<
T extends reflect.Method | reflect.Initializer,
>(original: T, updated: T, mismatches: IReport) {
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.
*/
export function validateNoNewAbstractMembers<T extends reflect.ReferenceType>(
original: T,
updated: T,
mismatches: IReport,
) {
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.
*/
export function validateReturnTypeNotWeakened(
original: reflect.Method,
updated: reflect.Method,
mismatches: IReport,
) {
const retAna = isCompatibleReturnType(original.returns, updated.returns);
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#.
*/
export function validateReturnTypeSame(
original: reflect.Method,
updated: reflect.Method,
mismatches: IReport,
) {
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.
*/
export function validatePropertyTypeNotWeakened(
original: reflect.Property,
updated: reflect.Property,
mismatches: IReport,
) {
const ana = isCompatibleReturnType(original, updated);
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#.
*/
export function validatePropertyTypeSame(
original: reflect.Property,
updated: reflect.Property,
mismatches: IReport,
) {
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.
*/
export function validateParameterTypeWeakened(
method: reflect.Method | reflect.Initializer,
original: reflect.Parameter,
updated: reflect.Parameter,
mismatches: IReport,
) {
const argAna = isCompatibleArgumentType(original.type, updated.type);
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#.
*/
export function validateParameterTypeSame(
method: reflect.Method | reflect.Initializer,
original: reflect.Parameter,
updated: reflect.Parameter,
mismatches: IReport,
) {
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: reflect.OptionalValue,
updatedType: reflect.OptionalValue,
analysis: FailedAnalysis,
) {
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)
*/
export function validateExistingParams<
T extends reflect.Initializer | reflect.Method,
>(
original: T,
updated: T,
mismatches: IReport,
validateParam: (
oldParam: reflect.Parameter,
newParam: reflect.Parameter,
) => void,
) {
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)
*/
export function validateNoNewRequiredParams<
T extends reflect.Initializer | reflect.Method,
>(original: T, updated: T, mismatches: IReport) {
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.`,
});
}
});
}
export function validateMethodCompatible<
T extends reflect.Method | reflect.Initializer,
>(original: T, updated: T, mismatches: IReport) {
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, 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, mismatches);
},
);
validateNoNewRequiredParams(original, updated, mismatches);
}
/**
* Check if a class/interface has been marked as @subclassable
*/
export function subclassableType(x: reflect.Documentable) {
return x.docs.subclassable;
}
/**
* Find the indicated parameter with the given index
*
* May return the last parameter if it's variadic
*/
function findParam(
parameters: reflect.Parameter[],
i: number,
): reflect.Parameter | undefined {
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;
* ```
*/
export function validateNotMadeImmutable(
original: reflect.Property,
updated: reflect.Property,
mismatches: IReport,
) {
if (updated.immutable && !original.immutable) {
mismatches.report({
ruleKey: 'removed-mutability',
violator: original,
message: 'used to be mutable, is now immutable',
});
}
}
export function* memberPairs<
T extends reflect.TypeMember,
U extends reflect.ReferenceType,
>(
origClass: U,
xs: T[],
updatedClass: U,
mismatches: IReport,
): IterableIterator<[T, reflect.TypeMember]> {
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: reflect.OptionalValue,
updated: reflect.OptionalValue,
): Analysis {
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 isSuperType(original.type, updated.type, updated.system);
}
/**
* Whether we are weakening the pre (input type of a method)
*
* Weakening preconditions is allowed!
*/
function isCompatibleArgumentType(
original: reflect.TypeReference,
updated: reflect.TypeReference,
): Analysis {
// Input can never be void, so no need to check
return isSuperType(updated, original, updated.system);
}
/**
* 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: reflect.ReferenceType,
updated: reflect.ReferenceType,
): Analysis {
for (const B of baseTypes(original)) {
const result = isNominalSuperType(
B.reference,
updated.reference,
updated.system,
);
if (!result.success) {
return result;
}
}
return { success: true };
}
/**
* Return all base types of the given reference type
*/
function baseTypes(type: reflect.ReferenceType) {
const ret = new Array<reflect.ReferenceType>();
const todo: reflect.ReferenceType[] = [type];
const seen = new Set<string>();
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)
*/
export function validateExistingMembers(
original: reflect.EnumType,
updated: reflect.EnumType,
mismatches: IReport,
validateMember: (
oldParam: reflect.EnumMember,
newParam: reflect.EnumMember,
) => void,
) {
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);
}
}