packages/jsii-diff/lib/types.ts (165 lines of code) (raw):

import { Stability } from '@jsii/spec'; import * as reflect from 'jsii-reflect'; export interface ComparisonOptions { /** * Whether to treat API elements as experimental if unmarked. * * @default Treat as stable */ defaultExperimental?: boolean; } export interface ComparisonContext extends ComparisonOptions { /** * Where to report errors */ mismatches: Mismatches; } export interface ApiMismatch { message: string; violationKey: string; stability: Stability; } export type ApiElement = reflect.Type | reflect.TypeMember | reflect.EnumMember; export interface ReportOptions { ruleKey: string; violator: ApiElement; message: string; } export interface IReport { report(options: ReportOptions): void; withMotivation(reason: string): IReport; } export class Mismatches implements IReport { public readonly mismatches = new Array<ApiMismatch>(); private readonly defaultStability: Stability; public constructor(opts: { defaultStability: Stability }) { this.defaultStability = opts.defaultStability; } public report(options: ReportOptions) { const fqn = apiElementIdentifier(options.violator); const key = `${options.ruleKey}:${fqn}`; this.mismatches.push({ violationKey: key, message: `${describeApiElement(options.violator)} ${fqn}: ${ options.message }`, stability: options.violator.docs.stability ?? this.defaultStability, }); } public *messages() { for (const mis of this.mismatches) { yield mis.message; } } public get count() { return this.mismatches.length; } public filter(pred: (x: ApiMismatch) => boolean): Mismatches { const ret = new Mismatches({ defaultStability: this.defaultStability }); ret.mismatches.push(...this.mismatches.filter(pred)); return ret; } public withMotivation(motivation: string): IReport { return { report: (options) => this.report({ ...options, message: `${options.message}: ${motivation}`, }), withMotivation: (innerMotivation) => this.withMotivation(`${innerMotivation}: ${motivation}`), }; } } export function apiElementIdentifier(apiElement: ApiElement) { return dispatch(apiElement, { method(x) { return `${x.parentType.fqn}.${x.name}`; }, init(x) { return `${x.parentType.fqn}.${x.name}`; }, property(x) { return `${x.parentType.fqn}.${x.name}`; }, enumMember(x) { return `${x.enumType.fqn}.${x.name}`; }, enumType(x) { return `${x.fqn}`; }, klass(x) { return `${x.fqn}`; }, iface(x) { return `${x.fqn}`; }, }); } function describeApiElement(apiElement: ApiElement) { return dispatch(apiElement, { method() { return 'METHOD'; }, init() { return 'INITIALIZER'; }, property() { return 'PROP'; }, enumMember() { return 'ENUM VALUE'; }, enumType() { return 'ENUM'; }, klass() { return 'CLASS'; }, iface() { return 'IFACE'; }, }); } function dispatch<T>( apiElement: ApiElement, fns: { method(m: reflect.Method): T; init(m: reflect.Initializer): T; property(m: reflect.Property): T; enumMember(m: reflect.EnumMember): T; enumType(m: reflect.EnumType): T; klass(m: reflect.ClassType): T; iface(m: reflect.InterfaceType): T; }, ) { if (apiElement instanceof reflect.Method) { return fns.method(apiElement); } if (apiElement instanceof reflect.Property) { return fns.property(apiElement); } if (apiElement instanceof reflect.EnumMember) { return fns.enumMember(apiElement); } if (apiElement instanceof reflect.ClassType) { return fns.klass(apiElement); } if (apiElement instanceof reflect.InterfaceType) { return fns.iface(apiElement); } if (apiElement instanceof reflect.Initializer) { return fns.init(apiElement); } if (apiElement instanceof reflect.EnumType) { return fns.enumType(apiElement); } // eslint-disable-next-line @typescript-eslint/no-base-to-string throw new Error(`Unrecognized violator: ${apiElement.toString()}`); } export function describeType(type: reflect.Type) { if (type.isClassType()) { return 'CLASS'; } if (type.isInterfaceType()) { return 'IFACE'; } if (type.isEnumType()) { return 'ENUM'; } return 'TYPE'; } export function describeInterfaceType(dataType: boolean) { return dataType ? 'struct' : 'regular interface'; }