From 0a70aa7f61f78b95a6799ee2e28d03bb88b0de73 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 23 May 2024 15:43:36 -0700 Subject: [PATCH] Organize versioning library (#3309) Versioning library is a big mess of things all into one file, this makes it hard to contribute. Did this originally because I was going to deal with multiple madeRequired and madeOptional over time but that will be done later but finished the cleanup anyway. Split the versioning.ts into multiple separate logical files: - `decorators.ts`: Contains all the versioning decorators and accessor - `internal-projection-functions.ts`: Contains implementation of the helper function used inside the versioning projection(not meant for external use) - `projection.ts`: Contains the projection building functions - `versioning.ts`: Contains the various versioning computation function(timeline, etc.) This makes it clearer of what is supposed to be public apis vs internal, waht is just mean for decorator, projection, etc. --- ...oning-library-cleanup-2024-4-9-22-35-15.md | 8 + packages/versioning/lib/decorators.tsp | 392 +++++------ packages/versioning/lib/main.tsp | 3 + .../lib/{versioning.tsp => projection.tsp} | 36 +- packages/versioning/package.json | 2 +- packages/versioning/src/decorators.ts | 463 ++++++++++++ packages/versioning/src/index.ts | 23 + .../src/internal-projection-functions.ts | 125 ++++ packages/versioning/src/lib.ts | 23 +- packages/versioning/src/projection.ts | 58 ++ packages/versioning/src/testing/index.ts | 2 +- packages/versioning/src/types.ts | 2 +- packages/versioning/src/validate.ts | 20 +- .../versioning/src/versioning-timeline.ts | 4 +- packages/versioning/src/versioning.ts | 664 +----------------- .../test/incompatible-versioning.test.ts | 4 +- .../versioning/test/library-loading.test.ts | 8 +- packages/versioning/test/test-host.ts | 4 +- packages/versioning/test/utils.ts | 2 +- .../test/versioned-dependencies.test.ts | 7 +- .../test/versioning-timeline.test.ts | 2 +- packages/versioning/test/versioning.test.ts | 29 +- packages/versioning/tsconfig.json | 1 + 23 files changed, 972 insertions(+), 910 deletions(-) create mode 100644 .chronus/changes/versioning-library-cleanup-2024-4-9-22-35-15.md create mode 100644 packages/versioning/lib/main.tsp rename packages/versioning/lib/{versioning.tsp => projection.tsp} (85%) create mode 100644 packages/versioning/src/decorators.ts create mode 100644 packages/versioning/src/internal-projection-functions.ts create mode 100644 packages/versioning/src/projection.ts diff --git a/.chronus/changes/versioning-library-cleanup-2024-4-9-22-35-15.md b/.chronus/changes/versioning-library-cleanup-2024-4-9-22-35-15.md new file mode 100644 index 000000000..bd60b1908 --- /dev/null +++ b/.chronus/changes/versioning-library-cleanup-2024-4-9-22-35-15.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/versioning" +--- + +Organize versioning library diff --git a/packages/versioning/lib/decorators.tsp b/packages/versioning/lib/decorators.tsp index ab9e8306a..6b327eed5 100644 --- a/packages/versioning/lib/decorators.tsp +++ b/packages/versioning/lib/decorators.tsp @@ -1,224 +1,192 @@ -import "../dist/src/versioning.js"; +import "../dist/src/decorators.js"; using TypeSpec.Reflection; -namespace TypeSpec { - namespace Versioning { - /** - * Identifies that the decorated namespace is versioned by the provided enum. - * @param versions The enum that describes the supported versions. - * - * @example - * - * ```tsp - * @versioned(Versions) - * namespace MyService; - * enum Versions { - * v1, - * v2, - * v3, - * } - * ``` - */ - extern dec versioned(target: Namespace, versions: Enum); +namespace TypeSpec.Versioning; - /** - * Identifies that a namespace or a given versioning enum member relies upon a versioned package. - * @param versionRecords The dependent library version(s) for the target namespace or version. - * - * @example Select a single version of `MyLib` to use - * - * ```tsp - * @useDependency(MyLib.Versions.v1_1) - * namespace NonVersionedService; - * ``` - * - * @example Select which version of the library match to which version of the service. - * - * ```tsp - * @versioned(Versions) - * namespace MyService1; - * enum Version { - * @useDependency(MyLib.Versions.v1_1) // V1 use lib v1_1 - * v1, - * @useDependency(MyLib.Versions.v1_1) // V2 use lib v1_1 - * v2, - * @useDependency(MyLib.Versions.v2) // V3 use lib v2 - * v3, - * } - * ``` - */ - extern dec useDependency(target: EnumMember | Namespace, ...versionRecords: EnumMember[]); +/** + * Identifies that the decorated namespace is versioned by the provided enum. + * @param versions The enum that describes the supported versions. + * + * @example + * + * ```tsp + * @versioned(Versions) + * namespace MyService; + * enum Versions { + * v1, + * v2, + * v3, + * } + * ``` + */ +extern dec versioned(target: Namespace, versions: Enum); - /** - * Identifies when the target was added. - * @param version The version that the target was added in. - * - * @example - * - * ```tsp - * @added(Versions.v2) - * op addedInV2(): void; - * - * @added(Versions.v2) - * model AlsoAddedInV2 {} - * - * model Foo { - * name: string; - * - * @added(Versions.v3) - * addedInV3: string; - * } - * ``` - */ - extern dec added( - target: - | Model - | ModelProperty - | Operation - | Enum - | EnumMember - | Union - | UnionVariant - | Scalar - | Interface, - version: EnumMember - ); +/** + * Identifies that a namespace or a given versioning enum member relies upon a versioned package. + * @param versionRecords The dependent library version(s) for the target namespace or version. + * + * @example Select a single version of `MyLib` to use + * + * ```tsp + * @useDependency(MyLib.Versions.v1_1) + * namespace NonVersionedService; + * ``` + * + * @example Select which version of the library match to which version of the service. + * + * ```tsp + * @versioned(Versions) + * namespace MyService1; + * enum Version { + * @useDependency(MyLib.Versions.v1_1) // V1 use lib v1_1 + * v1, + * @useDependency(MyLib.Versions.v1_1) // V2 use lib v1_1 + * v2, + * @useDependency(MyLib.Versions.v2) // V3 use lib v2 + * v3, + * } + * ``` + */ +extern dec useDependency(target: EnumMember | Namespace, ...versionRecords: EnumMember[]); - /** - * Identifies when the target was removed. - * @param version The version that the target was removed in. - * - * @example - * ```tsp - * @removed(Versions.v2) - * op removedInV2(): void; - * - * @removed(Versions.v2) - * model AlsoRemovedInV2 {} - * - * model Foo { - * name: string; - * - * @removed(Versions.v3) - * removedInV3: string; - * } - * ``` - */ - extern dec removed( - target: - | Model - | ModelProperty - | Operation - | Enum - | EnumMember - | Union - | UnionVariant - | Scalar - | Interface, - version: EnumMember - ); +/** + * Identifies when the target was added. + * @param version The version that the target was added in. + * + * @example + * + * ```tsp + * @added(Versions.v2) + * op addedInV2(): void; + * + * @added(Versions.v2) + * model AlsoAddedInV2 {} + * + * model Foo { + * name: string; + * + * @added(Versions.v3) + * addedInV3: string; + * } + * ``` + */ +extern dec added( + target: + | Model + | ModelProperty + | Operation + | Enum + | EnumMember + | Union + | UnionVariant + | Scalar + | Interface, + version: EnumMember +); - /** - * Identifies when the target has been renamed. - * @param version The version that the target was renamed in. - * @param oldName The previous name of the target. - * - * @example - * ```tsp - * @renamedFrom(Versions.v2, "oldName") - * op newName(): void; - * ``` - */ - extern dec renamedFrom( - target: - | Model - | ModelProperty - | Operation - | Enum - | EnumMember - | Union - | UnionVariant - | Scalar - | Interface, - version: EnumMember, - oldName: valueof string - ); +/** + * Identifies when the target was removed. + * @param version The version that the target was removed in. + * + * @example + * ```tsp + * @removed(Versions.v2) + * op removedInV2(): void; + * + * @removed(Versions.v2) + * model AlsoRemovedInV2 {} + * + * model Foo { + * name: string; + * + * @removed(Versions.v3) + * removedInV3: string; + * } + * ``` + */ +extern dec removed( + target: + | Model + | ModelProperty + | Operation + | Enum + | EnumMember + | Union + | UnionVariant + | Scalar + | Interface, + version: EnumMember +); - /** - * Identifies when a target was made optional. - * @param version The version that the target was made optional in. - * - * @example - * - * ```tsp - * model Foo { - * name: string; - * @madeOptional(Versions.v2) - * nickname: string; - * } - * ``` - */ - extern dec madeOptional(target: ModelProperty, version: EnumMember); +/** + * Identifies when the target has been renamed. + * @param version The version that the target was renamed in. + * @param oldName The previous name of the target. + * + * @example + * ```tsp + * @renamedFrom(Versions.v2, "oldName") + * op newName(): void; + * ``` + */ +extern dec renamedFrom( + target: + | Model + | ModelProperty + | Operation + | Enum + | EnumMember + | Union + | UnionVariant + | Scalar + | Interface, + version: EnumMember, + oldName: valueof string +); - /** - * Identifies when a target was made required. - * @param version The version that the target was made required in. - * - * @example - * - * ```tsp - * model Foo { - * name: string; - * @madeRequired(Versions.v2) - * nickname: string; - * } - * ``` - */ - extern dec madeRequired(target: ModelProperty, version: EnumMember); +/** + * Identifies when a target was made optional. + * @param version The version that the target was made optional in. + * + * @example + * + * ```tsp + * model Foo { + * name: string; + * @madeOptional(Versions.v2) + * nickname: string; + * } + * ``` + */ +extern dec madeOptional(target: ModelProperty, version: EnumMember); - /** - * Identifies when the target type changed. - * @param version The version that the target type changed in. - * @param oldType The previous type of the target. - */ - extern dec typeChangedFrom(target: ModelProperty, version: EnumMember, oldType: unknown); +/** + * Identifies when a target was made required. + * @param version The version that the target was made required in. + * + * @example + * + * ```tsp + * model Foo { + * name: string; + * @madeRequired(Versions.v2) + * nickname: string; + * } + * ``` + */ +extern dec madeRequired(target: ModelProperty, version: EnumMember); - /** - * Identifies when the target type changed. - * @param version The version that the target type changed in. - * @param oldType The previous type of the target. - */ - extern dec returnTypeChangedFrom(target: Operation, version: EnumMember, oldType: unknown); +/** + * Identifies when the target type changed. + * @param version The version that the target type changed in. + * @param oldType The previous type of the target. + */ +extern dec typeChangedFrom(target: ModelProperty, version: EnumMember, oldType: unknown); - /** - * Returns whether the target exists for the given version. - * @param version The version to check. - */ - extern fn existsAtVersion(target: unknown, version: EnumMember): boolean; - - /** - * Returns whether the target has a different name for the given version. - * @param version The version to check. - */ - extern fn hasDifferentNameAtVersion(target: unknown, version: EnumMember): boolean; - - /** - * Returns whether the target was made optional after the given version. - * @param version The version to check. - */ - extern fn madeOptionalAfter(target: unknown, version: EnumMember): boolean; - - /** - * Returns whether the target was made required after the given version. - * @param version The version to check. - */ - extern fn madeRequiredAfter(target: unknown, version: EnumMember): boolean; - - /** - * Returns whether the version exists for the provided enum member. - * @param version The version to check. - */ - extern fn getVersionForEnumMember(target: unknown, version: EnumMember): boolean; - } -} +/** + * Identifies when the target type changed. + * @param version The version that the target type changed in. + * @param oldType The previous type of the target. + */ +extern dec returnTypeChangedFrom(target: Operation, version: EnumMember, oldType: unknown); diff --git a/packages/versioning/lib/main.tsp b/packages/versioning/lib/main.tsp new file mode 100644 index 000000000..96b299f8a --- /dev/null +++ b/packages/versioning/lib/main.tsp @@ -0,0 +1,3 @@ +import "./decorators.tsp"; +import "./projection.tsp"; +import "../dist/src/validate.js"; diff --git a/packages/versioning/lib/versioning.tsp b/packages/versioning/lib/projection.tsp similarity index 85% rename from packages/versioning/lib/versioning.tsp rename to packages/versioning/lib/projection.tsp index be8234aee..8a7342d43 100644 --- a/packages/versioning/lib/versioning.tsp +++ b/packages/versioning/lib/projection.tsp @@ -1,7 +1,39 @@ -import "./decorators.tsp"; -import "../dist/src/validate.js"; +import "../dist/src/internal-projection-functions.js"; using TypeSpec.Versioning; +using TypeSpec.Reflection; + +namespace TypeSpec.Versioning; + +/** + * Returns whether the target exists for the given version. + * @param version The version to check. + */ +extern fn existsAtVersion(target: unknown, version: EnumMember): boolean; + +/** + * Returns whether the target has a different name for the given version. + * @param version The version to check. + */ +extern fn hasDifferentNameAtVersion(target: unknown, version: EnumMember): boolean; + +/** + * Returns whether the target has a different return type for the given version. + * @param version The version to check. + */ +extern fn hasDifferentReturnTypeAtVersion(target: unknown, version: EnumMember): boolean; + +/** + * Returns whether the target was made optional after the given version. + * @param version The version to check. + */ +extern fn madeOptionalAfter(target: unknown, version: EnumMember): boolean; + +/** + * Returns whether the target was made required after the given version. + * @param version The version to check. + */ +extern fn madeRequiredAfter(target: unknown, version: EnumMember): boolean; #suppress "projections-are-experimental" projection op#v { diff --git a/packages/versioning/package.json b/packages/versioning/package.json index 7cb68879e..9592c5e1c 100644 --- a/packages/versioning/package.json +++ b/packages/versioning/package.json @@ -18,7 +18,7 @@ ], "type": "module", "main": "dist/src/index.js", - "tspMain": "lib/versioning.tsp", + "tspMain": "lib/main.tsp", "exports": { ".": { "types": "./dist/src/index.d.ts", diff --git a/packages/versioning/src/decorators.ts b/packages/versioning/src/decorators.ts new file mode 100644 index 000000000..47591b5c0 --- /dev/null +++ b/packages/versioning/src/decorators.ts @@ -0,0 +1,463 @@ +import type { + DecoratorContext, + DiagnosticTarget, + Enum, + EnumMember, + Interface, + Model, + ModelProperty, + Namespace, + Operation, + Program, + Scalar, + Type, + Union, + UnionVariant, +} from "@typespec/compiler"; +import type { + AddedDecorator, + MadeOptionalDecorator, + MadeRequiredDecorator, + RenamedFromDecorator, + ReturnTypeChangedFromDecorator, + TypeChangedFromDecorator, + VersionedDecorator, +} from "../generated-defs/TypeSpec.Versioning.js"; +import { VersioningStateKeys, reportDiagnostic } from "./lib.js"; +import type { Version } from "./types.js"; +import { getVersionForEnumMember } from "./versioning.js"; + +export const namespace = "TypeSpec.Versioning"; + +function checkIsVersion( + program: Program, + enumMember: EnumMember, + diagnosticTarget: DiagnosticTarget +): Version | undefined { + const version = getVersionForEnumMember(program, enumMember); + + if (!version) { + reportDiagnostic(program, { + code: "version-not-found", + target: diagnosticTarget, + format: { version: enumMember.name, enumName: enumMember.enum.name }, + }); + } + return version; +} + +export const $added: AddedDecorator = ( + context: DecoratorContext, + t: + | Model + | ModelProperty + | Operation + | Enum + | EnumMember + | Union + | UnionVariant + | Scalar + | Interface, + v: EnumMember +) => { + const { program } = context; + + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + + // retrieve statemap to update or create a new one + const record = + program.stateMap(VersioningStateKeys.addedOn).get(t as Type) ?? new Array(); + record.push(version); + // ensure that records are stored in ascending order + (record as Version[]).sort((a, b) => a.index - b.index); + + program.stateMap(VersioningStateKeys.addedOn).set(t as Type, record); +}; + +export function $removed( + context: DecoratorContext, + t: + | Model + | ModelProperty + | Operation + | Enum + | EnumMember + | Union + | UnionVariant + | Scalar + | Interface, + v: EnumMember +) { + const { program } = context; + + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + + // retrieve statemap to update or create a new one + const record = + program.stateMap(VersioningStateKeys.removedOn).get(t as Type) ?? new Array(); + record.push(version); + // ensure that records are stored in ascending order + (record as Version[]).sort((a, b) => a.index - b.index); + + program.stateMap(VersioningStateKeys.removedOn).set(t as Type, record); +} + +/** + * Returns the mapping of versions to old type values, if applicable + * @param p TypeSpec program + * @param t type to query + * @returns Map of versions to old types, if any + */ +export function getTypeChangedFrom(p: Program, t: Type): Map | undefined { + return p.stateMap(VersioningStateKeys.typeChangedFrom).get(t) as Map; +} + +export const $typeChangedFrom: TypeChangedFromDecorator = ( + context: DecoratorContext, + prop: ModelProperty, + v: EnumMember, + oldType: any +) => { + const { program } = context; + + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + + // retrieve statemap to update or create a new one + let record = getTypeChangedFrom(program, prop) ?? new Map(); + record.set(version, oldType); + // ensure the map is sorted by version + record = new Map([...record.entries()].sort((a, b) => a[0].index - b[0].index)); + program.stateMap(VersioningStateKeys.typeChangedFrom).set(prop, record); +}; + +/** + * Returns the mapping of versions to old return type values, if applicable + * @param p TypeSpec program + * @param t type to query + * @returns Map of versions to old types, if any + */ +export function getReturnTypeChangedFrom(p: Program, t: Type): Map | undefined { + return p.stateMap(VersioningStateKeys.returnTypeChangedFrom).get(t) as Map; +} + +export const $returnTypeChangedFrom: ReturnTypeChangedFromDecorator = ( + context: DecoratorContext, + op: Operation, + v: EnumMember, + oldReturnType: Type +) => { + const { program } = context; + + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + + // retrieve statemap to update or create a new one + let record = getReturnTypeChangedFrom(program, op) ?? new Map(); + record.set(version, oldReturnType); + // ensure the map is sorted by version + record = new Map([...record.entries()].sort((a, b) => a[0].index - b[0].index)); + program.stateMap(VersioningStateKeys.returnTypeChangedFrom).set(op, record); +}; + +interface RenamedFrom { + version: Version; + oldName: string; +} + +export const $renamedFrom: RenamedFromDecorator = ( + context: DecoratorContext, + t: + | Model + | ModelProperty + | Operation + | Enum + | EnumMember + | Union + | UnionVariant + | Scalar + | Interface, + v: EnumMember, + oldName: string +) => { + const { program } = context; + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + + if (oldName === "") { + reportDiagnostic(program, { + code: "invalid-renamed-from-value", + target: t as Type, + }); + } + + // retrieve statemap to update or create a new one + const record = getRenamedFrom(program, t as Type) ?? []; + record.push({ version: version, oldName: oldName }); + // ensure that records are stored in ascending order + record.sort((a, b) => a.version.index - b.version.index); + + program.stateMap(VersioningStateKeys.renamedFrom).set(t as Type, record); +}; + +export const $madeOptional: MadeOptionalDecorator = ( + context: DecoratorContext, + t: ModelProperty, + v: EnumMember +) => { + const { program } = context; + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + program.stateMap(VersioningStateKeys.madeOptional).set(t, version); +}; + +export const $madeRequired: MadeRequiredDecorator = ( + context: DecoratorContext, + t: ModelProperty, + v: EnumMember +) => { + const { program } = context; + const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); + if (!version) { + return; + } + program.stateMap(VersioningStateKeys.madeRequired).set(t, version); +}; + +/** + * @returns version when the given type was made required if applicable. + */ +export function getMadeRequiredOn(p: Program, t: Type): Version | undefined { + return p.stateMap(VersioningStateKeys.madeRequired).get(t); +} + +/** + * @returns the array of RenamedFrom metadata if applicable. + */ +export function getRenamedFrom(p: Program, t: Type): Array | undefined { + return p.stateMap(VersioningStateKeys.renamedFrom).get(t) as Array; +} + +/** + * @returns the list of versions for which this decorator has been applied + */ +export function getRenamedFromVersions(p: Program, t: Type): Version[] | undefined { + return getRenamedFrom(p, t)?.map((x) => x.version); +} + +export function getAddedOnVersions(p: Program, t: Type): Version[] | undefined { + return p.stateMap(VersioningStateKeys.addedOn).get(t) as Version[]; +} + +export function getRemovedOnVersions(p: Program, t: Type): Version[] | undefined { + return p.stateMap(VersioningStateKeys.removedOn).get(t) as Version[]; +} + +/** + * @returns version when the given type was made optional if applicable. + */ +export function getMadeOptionalOn(p: Program, t: Type): Version | undefined { + return p.stateMap(VersioningStateKeys.madeOptional).get(t); +} + +export class VersionMap { + private map = new Map(); + + constructor(namespace: Namespace, enumType: Enum) { + let index = 0; + for (const member of enumType.members.values()) { + this.map.set(member, { + name: member.name, + value: member.value?.toString() ?? member.name, + enumMember: member, + index, + namespace, + }); + index++; + } + } + + public getVersionForEnumMember(member: EnumMember): Version | undefined { + return this.map.get(member); + } + + public getVersions(): Version[] { + return [...this.map.values()]; + } + + public get size(): number { + return this.map.size; + } +} + +export const $versioned: VersionedDecorator = ( + context: DecoratorContext, + t: Namespace, + versions: Enum +) => { + context.program.stateMap(VersioningStateKeys.versions).set(t, new VersionMap(t, versions)); +}; + +/** + * Get the version map of the namespace. + */ +export function getVersion(program: Program, namespace: Namespace): VersionMap | undefined { + return program.stateMap(VersioningStateKeys.versions).get(namespace); +} + +export function findVersionedNamespace( + program: Program, + namespace: Namespace +): Namespace | undefined { + let current: Namespace | undefined = namespace; + + while (current) { + if (program.stateMap(VersioningStateKeys.versions).has(current)) { + return current; + } + current = current.namespace; + } + + return undefined; +} + +export function $useDependency( + context: DecoratorContext, + target: EnumMember | Namespace, + ...versionRecords: EnumMember[] +) { + const versions: Version[] = []; + // ensure only valid versions are passed in + for (const record of versionRecords) { + const ver = checkIsVersion(context.program, record, context.getArgumentTarget(0)!); + if (ver) { + versions.push(ver); + } + } + + if (target.kind === "Namespace") { + let state = getNamespaceUseDependencyState(context.program, target); + if (!state) { + state = versions; + } else { + state.push(...versions); + } + context.program.stateMap(VersioningStateKeys.useDependencyNamespace).set(target, state); + } else if (target.kind === "EnumMember") { + const targetEnum = target.enum; + let state = context.program + .stateMap(VersioningStateKeys.useDependencyEnum) + .get(targetEnum) as Map; + if (!state) { + state = new Map(); + } + // get any existing versions and combine them + const currentVersions = state.get(target) ?? []; + currentVersions.push(...versions); + state.set(target, currentVersions); + context.program.stateMap(VersioningStateKeys.useDependencyEnum).set(targetEnum, state); + } +} + +function getNamespaceUseDependencyState( + program: Program, + target: Namespace +): Version[] | undefined { + return program.stateMap(VersioningStateKeys.useDependencyNamespace).get(target); +} + +export function getUseDependencies( + program: Program, + target: Namespace | Enum, + searchEnum: boolean = true +): Map | Version> | undefined { + const result = new Map | Version>(); + if (target.kind === "Namespace") { + let current: Namespace | undefined = target; + while (current) { + const data = getNamespaceUseDependencyState(program, current); + if (!data) { + // See if the namspace has a version enum + if (searchEnum) { + const versions = getVersion(program, current)?.getVersions(); + if (versions?.length) { + const enumDeps = getUseDependencies(program, versions[0].enumMember.enum); + if (enumDeps) { + return enumDeps; + } + } + } + current = current.namespace; + } else { + for (const v of data) { + result.set(v.namespace, v); + } + return result; + } + } + return undefined; + } else if (target.kind === "Enum") { + const data = program.stateMap(VersioningStateKeys.useDependencyEnum).get(target) as Map< + EnumMember, + Version[] + >; + if (!data) { + return undefined; + } + const resolved = resolveVersionDependency(program, data); + if (resolved instanceof Map) { + for (const [enumVer, value] of resolved) { + for (const val of value) { + const targetNamespace = val.enumMember.enum.namespace; + if (!targetNamespace) { + reportDiagnostic(program, { + code: "version-not-found", + target: val.enumMember.enum, + format: { version: val.enumMember.name, enumName: val.enumMember.enum.name }, + }); + return undefined; + } + let subMap = result.get(targetNamespace) as Map; + if (subMap) { + subMap.set(enumVer, val); + } else { + subMap = new Map([[enumVer, val]]); + } + result.set(targetNamespace, subMap); + } + } + } + } + return result; +} + +function resolveVersionDependency( + program: Program, + data: Map | Version[] +): Map | Version[] { + if (!(data instanceof Map)) { + return data; + } + const mapping = new Map(); + for (const [key, value] of data) { + const sourceVersion = getVersionForEnumMember(program, key); + if (sourceVersion !== undefined) { + mapping.set(sourceVersion, value); + } + } + return mapping; +} diff --git a/packages/versioning/src/index.ts b/packages/versioning/src/index.ts index 53d281d26..841f7bafa 100644 --- a/packages/versioning/src/index.ts +++ b/packages/versioning/src/index.ts @@ -1,3 +1,26 @@ +export { + $added, + $madeOptional, + $madeRequired, + $removed, + $renamedFrom, + $returnTypeChangedFrom, + $typeChangedFrom, + $useDependency, + $versioned, + VersionMap, + findVersionedNamespace, + getAddedOnVersions, + getMadeOptionalOn, + getRemovedOnVersions, + getRenamedFrom, + getRenamedFromVersions, + getReturnTypeChangedFrom, + getTypeChangedFrom, + getUseDependencies, + getVersion, +} from "./decorators.js"; +export { buildVersionProjections, type VersionProjections } from "./projection.js"; export * from "./types.js"; export * from "./validate.js"; export * from "./versioning.js"; diff --git a/packages/versioning/src/internal-projection-functions.ts b/packages/versioning/src/internal-projection-functions.ts new file mode 100644 index 000000000..59da4961d --- /dev/null +++ b/packages/versioning/src/internal-projection-functions.ts @@ -0,0 +1,125 @@ +import type { ObjectType, Program, Type } from "@typespec/compiler"; +import { + getMadeOptionalOn, + getMadeRequiredOn, + getRenamedFrom, + getReturnTypeChangedFrom, + getTypeChangedFrom, +} from "./decorators.js"; +import { VersioningStateKeys } from "./lib.js"; +import { TimelineMoment, VersioningTimeline } from "./versioning-timeline.js"; +import { Availability, getAvailabilityMapInTimeline } from "./versioning.js"; + +export const namespace = "TypeSpec.Versioning"; + +function getVersioningState( + program: Program, + versionKey: ObjectType +): { + timeline: VersioningTimeline; + projectingMoment: TimelineMoment; +} { + return program.stateMap(VersioningStateKeys.versionIndex).get(versionKey); +} + +/** + * @returns get old name if applicable. + */ +export function getNameAtVersion(p: Program, t: Type, versionKey: ObjectType): string { + const versioningState = getVersioningState(p, versionKey); + + const allValues = getRenamedFrom(p, t); + if (!allValues) return ""; + + for (const val of allValues) { + if (versioningState.timeline.isBefore(versioningState.projectingMoment, val.version)) { + return val.oldName; + } + } + return ""; +} + +/** + * @returns get old type if applicable. + */ +export function getTypeBeforeVersion( + p: Program, + t: Type, + versionKey: ObjectType +): Type | undefined { + const versioningState = getVersioningState(p, versionKey); + + const map = getTypeChangedFrom(p, t); + if (!map) return undefined; + + for (const [changedAtVersion, oldType] of map) { + if (versioningState.timeline.isBefore(versioningState.projectingMoment, changedAtVersion)) { + return oldType; + } + } + return undefined; +} + +/** + * @returns get old type if applicable. + */ +export function getReturnTypeBeforeVersion(p: Program, t: Type, versionKey: ObjectType): any { + const versioningState = getVersioningState(p, versionKey); + + const map = getReturnTypeChangedFrom(p, t); + if (!map) return ""; + + for (const [changedAtVersion, val] of map) { + if (versioningState.timeline.isBefore(versioningState.projectingMoment, changedAtVersion)) { + return val; + } + } + return ""; +} + +export function madeOptionalAfter(program: Program, type: Type, versionKey: ObjectType): boolean { + const versioningState = getVersioningState(program, versionKey); + + const madeOptionalAtVersion = getMadeOptionalOn(program, type); + if (madeOptionalAtVersion === undefined) { + return false; + } + return versioningState.timeline.isBefore(versioningState.projectingMoment, madeOptionalAtVersion); +} + +export function madeRequiredAfter(program: Program, type: Type, versionKey: ObjectType): boolean { + const versioningState = getVersioningState(program, versionKey); + + const madeRequiredAtVersion = getMadeRequiredOn(program, type); + if (madeRequiredAtVersion === undefined) { + return false; + } + + return versioningState.timeline.isBefore(versioningState.projectingMoment, madeRequiredAtVersion); +} + +export function existsAtVersion(p: Program, type: Type, versionKey: ObjectType): boolean { + const versioningState = getVersioningState(p, versionKey); + // if unversioned then everything exists + + const availability = getAvailabilityMapInTimeline(p, type, versioningState.timeline); + if (!availability) return true; + const isAvail = availability.get(versioningState.projectingMoment)!; + return isAvail === Availability.Added || isAvail === Availability.Available; +} + +export function hasDifferentNameAtVersion(p: Program, type: Type, version: ObjectType): boolean { + return getNameAtVersion(p, type, version) !== ""; +} + +export function hasDifferentTypeAtVersion(p: Program, type: Type, version: ObjectType): boolean { + return getTypeBeforeVersion(p, type, version) !== undefined; +} + +export function hasDifferentReturnTypeAtVersion( + p: Program, + type: Type, + version: ObjectType +): boolean { + return getReturnTypeBeforeVersion(p, type, version) !== ""; +} diff --git a/packages/versioning/src/lib.ts b/packages/versioning/src/lib.ts index 4193a8013..0470a4274 100644 --- a/packages/versioning/src/lib.ts +++ b/packages/versioning/src/lib.ts @@ -1,6 +1,10 @@ import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler"; -const libDef = { +export const { + reportDiagnostic, + createStateSymbol, + stateKeys: VersioningStateKeys, +} = createTypeSpecLibrary({ name: "@typespec/versioning", diagnostics: { "versioned-dependency-tuple": { @@ -102,5 +106,18 @@ const libDef = { }, }, }, -} as const; -export const { reportDiagnostic, createStateSymbol } = createTypeSpecLibrary(libDef); + state: { + versionIndex: { description: "Version index" }, + + addedOn: { description: "State for @addedOn decorator" }, + removedOn: { description: "State for @removedOn decorator" }, + versions: { description: "State for @versioned decorator" }, + useDependencyNamespace: { description: "State for @useDependency decorator on Namespaces" }, + useDependencyEnum: { description: "State for @useDependency decorator on Enums" }, + renamedFrom: { description: "State for @renamedFrom decorator" }, + madeOptional: { description: "State for @madeOptional decorator" }, + madeRequired: { description: "State for @madeRequired decorator" }, + typeChangedFrom: { description: "State for @typeChangedFrom decorator" }, + returnTypeChangedFrom: { description: "State for @returnTypeChangedFrom decorator" }, + }, +}); diff --git a/packages/versioning/src/projection.ts b/packages/versioning/src/projection.ts new file mode 100644 index 000000000..810470590 --- /dev/null +++ b/packages/versioning/src/projection.ts @@ -0,0 +1,58 @@ +import type { Namespace, ObjectType, Program, ProjectionApplication } from "@typespec/compiler"; +import { VersioningStateKeys } from "./lib.js"; +import { TimelineMoment, VersioningTimeline } from "./versioning-timeline.js"; +import { resolveVersions } from "./versioning.js"; + +/** + * Represent the set of projections used to project to that version. + */ +export interface VersionProjections { + version: string | undefined; + projections: ProjectionApplication[]; +} + +/** + * @internal + */ +export function indexTimeline( + program: Program, + timeline: VersioningTimeline, + projectingMoment: TimelineMoment +) { + const versionKey = program.checker.createType({ + kind: "Object", + properties: {}, + } as any); + program + .stateMap(VersioningStateKeys.versionIndex) + .set(versionKey, { timeline, projectingMoment }); + return versionKey; +} + +export function buildVersionProjections(program: Program, rootNs: Namespace): VersionProjections[] { + const resolutions = resolveVersions(program, rootNs); + const timeline = new VersioningTimeline( + program, + resolutions.map((x) => x.versions) + ); + return resolutions.map((resolution) => { + if (resolution.versions.size === 0) { + return { version: undefined, projections: [] }; + } else { + const versionKey = indexTimeline( + program, + timeline, + timeline.get(resolution.versions.values().next().value) + ); + return { + version: resolution.rootVersion?.value, + projections: [ + { + projectionName: "v", + arguments: [versionKey], + }, + ], + }; + } + }); +} diff --git a/packages/versioning/src/testing/index.ts b/packages/versioning/src/testing/index.ts index f76230d60..75fe32643 100644 --- a/packages/versioning/src/testing/index.ts +++ b/packages/versioning/src/testing/index.ts @@ -1,7 +1,7 @@ import { createTestLibrary, findTestPackageRoot, - TypeSpecTestLibrary, + type TypeSpecTestLibrary, } from "@typespec/compiler/testing"; export const VersioningTestLibrary: TypeSpecTestLibrary = createTestLibrary({ diff --git a/packages/versioning/src/types.ts b/packages/versioning/src/types.ts index 7dff9d3d0..7975ee4e8 100644 --- a/packages/versioning/src/types.ts +++ b/packages/versioning/src/types.ts @@ -1,4 +1,4 @@ -import { EnumMember, Namespace } from "@typespec/compiler"; +import type { EnumMember, Namespace } from "@typespec/compiler"; export interface Version { name: string; diff --git a/packages/versioning/src/validate.ts b/packages/versioning/src/validate.ts index 7149b2c93..57e1ca178 100644 --- a/packages/versioning/src/validate.ts +++ b/packages/versioning/src/validate.ts @@ -1,22 +1,18 @@ import { + NoTarget, getNamespaceFullName, getService, getTypeName, isTemplateInstance, isType, - Namespace, navigateProgram, - NoTarget, - Program, - Type, - TypeNameOptions, + type Namespace, + type Program, + type Type, + type TypeNameOptions, } from "@typespec/compiler"; -import { reportDiagnostic } from "./lib.js"; -import { Version } from "./types.js"; import { - Availability, findVersionedNamespace, - getAvailabilityMap, getMadeOptionalOn, getMadeRequiredOn, getRenamedFrom, @@ -24,6 +20,12 @@ import { getTypeChangedFrom, getUseDependencies, getVersion, +} from "./decorators.js"; +import { reportDiagnostic } from "./lib.js"; +import type { Version } from "./types.js"; +import { + Availability, + getAvailabilityMap, getVersionDependencies, getVersions, } from "./versioning.js"; diff --git a/packages/versioning/src/versioning-timeline.ts b/packages/versioning/src/versioning-timeline.ts index a64efeb87..814866517 100644 --- a/packages/versioning/src/versioning-timeline.ts +++ b/packages/versioning/src/versioning-timeline.ts @@ -1,5 +1,5 @@ -import { compilerAssert, getTypeName, Namespace, Program } from "@typespec/compiler"; -import { Version } from "./types.js"; +import { compilerAssert, getTypeName, type Namespace, type Program } from "@typespec/compiler"; +import type { Version } from "./types.js"; import { getVersions } from "./versioning.js"; /** diff --git a/packages/versioning/src/versioning.ts b/packages/versioning/src/versioning.ts index a26dab639..b08852a96 100644 --- a/packages/versioning/src/versioning.ts +++ b/packages/versioning/src/versioning.ts @@ -1,522 +1,17 @@ +import type { Enum, EnumMember, Namespace, Program, Type } from "@typespec/compiler"; +import { compilerAssert, getNamespaceFullName } from "@typespec/compiler"; import { - DecoratorContext, - DiagnosticTarget, - Enum, - EnumMember, - Interface, - Model, - ModelProperty, - Namespace, - ObjectType, - Operation, - Program, - ProjectionApplication, - Scalar, - Type, - Union, - UnionVariant, - compilerAssert, - getNamespaceFullName, -} from "@typespec/compiler"; -import { - AddedDecorator, - MadeOptionalDecorator, - MadeRequiredDecorator, - RenamedFromDecorator, - ReturnTypeChangedFromDecorator, - TypeChangedFromDecorator, - VersionedDecorator, -} from "../generated-defs/TypeSpec.Versioning.js"; -import { createStateSymbol, reportDiagnostic } from "./lib.js"; -import { Version, VersionResolution } from "./types.js"; + getAddedOnVersions, + getRemovedOnVersions, + getReturnTypeChangedFrom, + getTypeChangedFrom, + getUseDependencies, + getVersion, + type VersionMap, +} from "./decorators.js"; +import type { Version, VersionResolution } from "./types.js"; import { TimelineMoment, VersioningTimeline } from "./versioning-timeline.js"; -const addedOnKey = createStateSymbol("addedOn"); -const removedOnKey = createStateSymbol("removedOn"); -const versionsKey = createStateSymbol("versions"); -const versionDependencyKey = createStateSymbol("versionDependency"); -const useDependencyNamespaceKey = createStateSymbol("useDependencyNamespace"); -const useDependencyEnumKey = createStateSymbol("useDependencyEnum"); -const renamedFromKey = createStateSymbol("renamedFrom"); -const madeOptionalKey = createStateSymbol("madeOptional"); -const madeRequiredKey = createStateSymbol("madeRequired"); -const typeChangedFromKey = createStateSymbol("typeChangedFrom"); -const returnTypeChangedFromKey = createStateSymbol("returnTypeChangedFrom"); - -export const namespace = "TypeSpec.Versioning"; - -function checkIsVersion( - program: Program, - enumMember: EnumMember, - diagnosticTarget: DiagnosticTarget -): Version | undefined { - const version = getVersionForEnumMember(program, enumMember); - - if (!version) { - reportDiagnostic(program, { - code: "version-not-found", - target: diagnosticTarget, - format: { version: enumMember.name, enumName: enumMember.enum.name }, - }); - } - return version; -} - -export const $added: AddedDecorator = ( - context: DecoratorContext, - t: - | Model - | ModelProperty - | Operation - | Enum - | EnumMember - | Union - | UnionVariant - | Scalar - | Interface, - v: EnumMember -) => { - const { program } = context; - - const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); - if (!version) { - return; - } - - // retrieve statemap to update or create a new one - const record = program.stateMap(addedOnKey).get(t as Type) ?? new Array(); - record.push(version); - // ensure that records are stored in ascending order - (record as Version[]).sort((a, b) => a.index - b.index); - - program.stateMap(addedOnKey).set(t as Type, record); -}; - -export function $removed( - context: DecoratorContext, - t: - | Model - | ModelProperty - | Operation - | Enum - | EnumMember - | Union - | UnionVariant - | Scalar - | Interface, - v: EnumMember -) { - const { program } = context; - - const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); - if (!version) { - return; - } - - // retrieve statemap to update or create a new one - const record = program.stateMap(removedOnKey).get(t as Type) ?? new Array(); - record.push(version); - // ensure that records are stored in ascending order - (record as Version[]).sort((a, b) => a.index - b.index); - - program.stateMap(removedOnKey).set(t as Type, record); -} - -/** - * Returns the mapping of versions to old type values, if applicable - * @param p TypeSpec program - * @param t type to query - * @returns Map of versions to old types, if any - */ -export function getTypeChangedFrom(p: Program, t: Type): Map | undefined { - return p.stateMap(typeChangedFromKey).get(t) as Map; -} - -export const $typeChangedFrom: TypeChangedFromDecorator = ( - context: DecoratorContext, - prop: ModelProperty, - v: EnumMember, - oldType: any -) => { - const { program } = context; - - const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); - if (!version) { - return; - } - - // retrieve statemap to update or create a new one - let record = getTypeChangedFrom(program, prop) ?? new Map(); - record.set(version, oldType); - // ensure the map is sorted by version - record = new Map([...record.entries()].sort((a, b) => a[0].index - b[0].index)); - program.stateMap(typeChangedFromKey).set(prop, record); -}; - -/** - * Returns the mapping of versions to old return type values, if applicable - * @param p TypeSpec program - * @param t type to query - * @returns Map of versions to old types, if any - */ -export function getReturnTypeChangedFrom(p: Program, t: Type): Map | undefined { - return p.stateMap(returnTypeChangedFromKey).get(t) as Map; -} - -export const $returnTypeChangedFrom: ReturnTypeChangedFromDecorator = ( - context: DecoratorContext, - op: Operation, - v: EnumMember, - oldReturnType: Type -) => { - const { program } = context; - - const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); - if (!version) { - return; - } - - // retrieve statemap to update or create a new one - let record = getReturnTypeChangedFrom(program, op) ?? new Map(); - record.set(version, oldReturnType); - // ensure the map is sorted by version - record = new Map([...record.entries()].sort((a, b) => a[0].index - b[0].index)); - program.stateMap(returnTypeChangedFromKey).set(op, record); -}; - -interface RenamedFrom { - version: Version; - oldName: string; -} - -export const $renamedFrom: RenamedFromDecorator = ( - context: DecoratorContext, - t: - | Model - | ModelProperty - | Operation - | Enum - | EnumMember - | Union - | UnionVariant - | Scalar - | Interface, - v: EnumMember, - oldName: string -) => { - const { program } = context; - const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); - if (!version) { - return; - } - - if (oldName === "") { - reportDiagnostic(program, { - code: "invalid-renamed-from-value", - target: t as Type, - }); - } - - // retrieve statemap to update or create a new one - const record = getRenamedFrom(program, t as Type) ?? []; - record.push({ version: version, oldName: oldName }); - // ensure that records are stored in ascending order - record.sort((a, b) => a.version.index - b.version.index); - - program.stateMap(renamedFromKey).set(t as Type, record); -}; - -export const $madeOptional: MadeOptionalDecorator = ( - context: DecoratorContext, - t: ModelProperty, - v: EnumMember -) => { - const { program } = context; - const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); - if (!version) { - return; - } - program.stateMap(madeOptionalKey).set(t, version); -}; - -export const $madeRequired: MadeRequiredDecorator = ( - context: DecoratorContext, - t: ModelProperty, - v: EnumMember -) => { - const { program } = context; - const version = checkIsVersion(context.program, v, context.getArgumentTarget(0)!); - if (!version) { - return; - } - program.stateMap(madeRequiredKey).set(t, version); -}; - -/** - * @returns the array of RenamedFrom metadata if applicable. - */ -export function getRenamedFrom(p: Program, t: Type): Array | undefined { - return p.stateMap(renamedFromKey).get(t) as Array; -} - -/** - * @returns the list of versions for which this decorator has been applied - */ -export function getRenamedFromVersions(p: Program, t: Type): Version[] | undefined { - return getRenamedFrom(p, t)?.map((x) => x.version); -} - -/** - * @returns get old name if applicable. - */ -export function getNameAtVersion(p: Program, t: Type, versionKey: ObjectType): string { - const versioningState = getVersioningState(p, versionKey); - - const allValues = getRenamedFrom(p, t); - if (!allValues) return ""; - - for (const val of allValues) { - if (versioningState.timeline.isBefore(versioningState.projectingMoment, val.version)) { - return val.oldName; - } - } - return ""; -} - -/** - * @returns get old type if applicable. - */ -export function getTypeBeforeVersion( - p: Program, - t: Type, - versionKey: ObjectType -): Type | undefined { - const versioningState = getVersioningState(p, versionKey); - - const map = getTypeChangedFrom(p, t); - if (!map) return undefined; - - for (const [changedAtVersion, oldType] of map) { - if (versioningState.timeline.isBefore(versioningState.projectingMoment, changedAtVersion)) { - return oldType; - } - } - return undefined; -} - -/** - * @returns get old type if applicable. - */ -export function getReturnTypeBeforeVersion(p: Program, t: Type, versionKey: ObjectType): any { - const versioningState = getVersioningState(p, versionKey); - - const map = getReturnTypeChangedFrom(p, t); - if (!map) return ""; - - for (const [changedAtVersion, val] of map) { - if (versioningState.timeline.isBefore(versioningState.projectingMoment, changedAtVersion)) { - return val; - } - } - return ""; -} - -export function getAddedOnVersions(p: Program, t: Type): Version[] | undefined { - return p.stateMap(addedOnKey).get(t) as Version[]; -} - -export function getRemovedOnVersions(p: Program, t: Type): Version[] | undefined { - return p.stateMap(removedOnKey).get(t) as Version[]; -} - -/** - * @returns version when the given type was made optional if applicable. - */ -export function getMadeOptionalOn(p: Program, t: Type): Version | undefined { - return p.stateMap(madeOptionalKey).get(t); -} - -/** - * @returns version when the given type was made required if applicable. - */ -export function getMadeRequiredOn(p: Program, t: Type): Version | undefined { - return p.stateMap(madeRequiredKey).get(t); -} - -export class VersionMap { - private map = new Map(); - - constructor(namespace: Namespace, enumType: Enum) { - let index = 0; - for (const member of enumType.members.values()) { - this.map.set(member, { - name: member.name, - value: member.value?.toString() ?? member.name, - enumMember: member, - index, - namespace, - }); - index++; - } - } - - public getVersionForEnumMember(member: EnumMember): Version | undefined { - return this.map.get(member); - } - - public getVersions(): Version[] { - return [...this.map.values()]; - } - - public get size(): number { - return this.map.size; - } -} - -export const $versioned: VersionedDecorator = ( - context: DecoratorContext, - t: Namespace, - versions: Enum -) => { - context.program.stateMap(versionsKey).set(t, new VersionMap(t, versions)); -}; - -/** - * Get the version map of the namespace. - */ -export function getVersion(program: Program, namespace: Namespace): VersionMap | undefined { - return program.stateMap(versionsKey).get(namespace); -} - -export function findVersionedNamespace( - program: Program, - namespace: Namespace -): Namespace | undefined { - let current: Namespace | undefined = namespace; - - while (current) { - if (program.stateMap(versionsKey).has(current)) { - return current; - } - current = current.namespace; - } - - return undefined; -} - -export function $useDependency( - context: DecoratorContext, - target: EnumMember | Namespace, - ...versionRecords: EnumMember[] -) { - const versions: Array = []; - // ensure only valid versions are passed in - for (const record of versionRecords) { - const ver = checkIsVersion(context.program, record, context.getArgumentTarget(0)!); - if (ver) { - versions.push(ver); - } - } - - if (target.kind === "Namespace") { - let state = context.program.stateMap(useDependencyNamespaceKey).get(target) as Version[]; - if (!state) { - state = versions; - } else { - state.push(...versions); - } - context.program.stateMap(useDependencyNamespaceKey).set(target, state); - } else if (target.kind === "EnumMember") { - const targetEnum = target.enum; - let state = context.program.stateMap(useDependencyEnumKey).get(targetEnum) as Map< - EnumMember, - Version[] - >; - if (!state) { - state = new Map(); - } - // get any existing versions and combine them - const currentVersions = state.get(target) ?? []; - currentVersions.push(...versions); - state.set(target, currentVersions); - context.program.stateMap(useDependencyEnumKey).set(targetEnum, state); - } -} - -export function getUseDependencies( - program: Program, - target: Namespace | Enum, - searchEnum: boolean = true -): Map | Version> | undefined { - const result = new Map | Version>(); - if (target.kind === "Namespace") { - let current: Namespace | undefined = target; - while (current) { - const data = program.stateMap(useDependencyNamespaceKey).get(current) as Version[]; - if (!data) { - // See if the namspace has a version enum - if (searchEnum) { - const versions = getVersion(program, current)?.getVersions(); - if (versions?.length) { - const enumDeps = getUseDependencies(program, versions[0].enumMember.enum); - if (enumDeps) { - return enumDeps; - } - } - } - current = current.namespace; - } else { - for (const v of data) { - result.set(v.namespace, v); - } - return result; - } - } - return undefined; - } else if (target.kind === "Enum") { - const data = program.stateMap(useDependencyEnumKey).get(target) as Map; - if (!data) { - return undefined; - } - const resolved = resolveVersionDependency(program, data); - if (resolved instanceof Map) { - for (const [enumVer, value] of resolved) { - for (const val of value) { - const targetNamespace = val.enumMember.enum.namespace; - if (!targetNamespace) { - reportDiagnostic(program, { - code: "version-not-found", - target: val.enumMember.enum, - format: { version: val.enumMember.name, enumName: val.enumMember.enum.name }, - }); - return undefined; - } - let subMap = result.get(targetNamespace) as Map; - if (subMap) { - subMap.set(enumVer, val); - } else { - subMap = new Map([[enumVer, val]]); - } - result.set(targetNamespace, subMap); - } - } - } - } - return result; -} - -function findVersionDependencyForNamespace(program: Program, namespace: Namespace) { - let current: Namespace | undefined = namespace; - while (current) { - const data = program.stateMap(versionDependencyKey).get(current); - if (data) { - return data; - } - current = current.namespace; - } - return undefined; -} - export function getVersionDependencies( program: Program, namespace: Namespace @@ -526,32 +21,7 @@ export function getVersionDependencies( return useDeps; } - const data = findVersionDependencyForNamespace(program, namespace); - if (data === undefined) { - return undefined; - } - const result = new Map(); - for (const [key, value] of data) { - result.set(key, resolveVersionDependency(program, value)); - } - return result; -} - -function resolveVersionDependency( - program: Program, - data: Map | Version[] -): Map | Version[] { - if (!(data instanceof Map)) { - return data; - } - const mapping = new Map(); - for (const [key, value] of data) { - const sourceVersion = getVersionForEnumMember(program, key); - if (sourceVersion !== undefined) { - mapping.set(sourceVersion, value); - } - } - return mapping; + return undefined; } /** @@ -625,69 +95,6 @@ export function resolveVersions(program: Program, namespace: Namespace): Version } } -/** - * Represent the set of projections used to project to that version. - */ -export interface VersionProjections { - version: string | undefined; - projections: ProjectionApplication[]; -} - -/** - * @internal - */ -export function indexTimeline( - program: Program, - timeline: VersioningTimeline, - projectingMoment: TimelineMoment -) { - const versionKey = program.checker.createType({ - kind: "Object", - properties: {}, - } as any); - program.stateMap(versionIndexKey).set(versionKey, { timeline, projectingMoment }); - return versionKey; -} - -function getVersioningState( - program: Program, - versionKey: ObjectType -): { - timeline: VersioningTimeline; - projectingMoment: TimelineMoment; -} { - return program.stateMap(versionIndexKey).get(versionKey); -} - -const versionIndexKey = createStateSymbol("version-index"); -export function buildVersionProjections(program: Program, rootNs: Namespace): VersionProjections[] { - const resolutions = resolveVersions(program, rootNs); - const timeline = new VersioningTimeline( - program, - resolutions.map((x) => x.versions) - ); - return resolutions.map((resolution) => { - if (resolution.versions.size === 0) { - return { version: undefined, projections: [] }; - } else { - const versionKey = indexTimeline( - program, - timeline, - timeline.get(resolution.versions.values().next().value) - ); - return { - version: resolution.rootVersion?.value, - projections: [ - { - projectionName: "v", - arguments: [versionKey], - }, - ], - }; - } - }); -} - const versionCache = new WeakMap(); function cacheVersion(key: Type, versions: [Namespace, VersionMap] | []) { versionCache.set(key, versions); @@ -898,53 +305,6 @@ export function getAvailabilityMapInTimeline( return avail; } -export function existsAtVersion(p: Program, type: Type, versionKey: ObjectType): boolean { - const versioningState = getVersioningState(p, versionKey); - // if unversioned then everything exists - - const availability = getAvailabilityMapInTimeline(p, type, versioningState.timeline); - if (!availability) return true; - const isAvail = availability.get(versioningState.projectingMoment)!; - return isAvail === Availability.Added || isAvail === Availability.Available; -} - -export function hasDifferentNameAtVersion(p: Program, type: Type, version: ObjectType): boolean { - return getNameAtVersion(p, type, version) !== ""; -} - -export function madeOptionalAfter(program: Program, type: Type, versionKey: ObjectType): boolean { - const versioningState = getVersioningState(program, versionKey); - - const madeOptionalAtVersion = getMadeOptionalOn(program, type); - if (madeOptionalAtVersion === undefined) { - return false; - } - return versioningState.timeline.isBefore(versioningState.projectingMoment, madeOptionalAtVersion); -} - -export function madeRequiredAfter(program: Program, type: Type, versionKey: ObjectType): boolean { - const versioningState = getVersioningState(program, versionKey); - - const madeRequiredAtVersion = getMadeRequiredOn(program, type); - if (madeRequiredAtVersion === undefined) { - return false; - } - - return versioningState.timeline.isBefore(versioningState.projectingMoment, madeRequiredAtVersion); -} - -export function hasDifferentTypeAtVersion(p: Program, type: Type, version: ObjectType): boolean { - return getTypeBeforeVersion(p, type, version) !== undefined; -} - -export function hasDifferentReturnTypeAtVersion( - p: Program, - type: Type, - version: ObjectType -): boolean { - return getReturnTypeBeforeVersion(p, type, version) !== ""; -} - export function getVersionForEnumMember(program: Program, member: EnumMember): Version | undefined { // Always lookup for the original type. This ensure reference equality when comparing versions. member = (member.projectionBase as EnumMember) ?? member; diff --git a/packages/versioning/test/incompatible-versioning.test.ts b/packages/versioning/test/incompatible-versioning.test.ts index 371f365c1..6495d6b86 100644 --- a/packages/versioning/test/incompatible-versioning.test.ts +++ b/packages/versioning/test/incompatible-versioning.test.ts @@ -1,9 +1,9 @@ import { - BasicTestRunner, - TestHost, createTestWrapper, expectDiagnosticEmpty, expectDiagnostics, + type BasicTestRunner, + type TestHost, } from "@typespec/compiler/testing"; import { ok } from "assert"; import { beforeEach, describe, it } from "vitest"; diff --git a/packages/versioning/test/library-loading.test.ts b/packages/versioning/test/library-loading.test.ts index 6651e7da8..986e2641a 100644 --- a/packages/versioning/test/library-loading.test.ts +++ b/packages/versioning/test/library-loading.test.ts @@ -1,8 +1,8 @@ -import { Namespace, projectProgram } from "@typespec/compiler"; -import { BasicTestRunner, createTestWrapper } from "@typespec/compiler/testing"; +import { projectProgram, type Namespace } from "@typespec/compiler"; +import { createTestWrapper, type BasicTestRunner } from "@typespec/compiler/testing"; import { notStrictEqual, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { buildVersionProjections } from "../src/versioning.js"; +import { buildVersionProjections } from "../src/projection.js"; import { createVersioningTestHost } from "./test-host.js"; describe("versioning: library loading", () => { @@ -30,7 +30,7 @@ describe("versioning: library loading", () => { }; // Force loading a different version const { buildVersionProjections: buildVersionProjectionsDifferent } = await import( - "../src/versioning.js?different=1" as any + "../src/projection.js?different=1" as any ); notStrictEqual( buildVersionProjections, diff --git a/packages/versioning/test/test-host.ts b/packages/versioning/test/test-host.ts index 80a0df85c..098e1b76a 100644 --- a/packages/versioning/test/test-host.ts +++ b/packages/versioning/test/test-host.ts @@ -1,8 +1,8 @@ import { - BasicTestRunner, createTestHost, createTestWrapper, - TestHost, + type BasicTestRunner, + type TestHost, } from "@typespec/compiler/testing"; import { VersioningTestLibrary } from "../src/testing/index.js"; diff --git a/packages/versioning/test/utils.ts b/packages/versioning/test/utils.ts index 0bb82dd04..3d519e293 100644 --- a/packages/versioning/test/utils.ts +++ b/packages/versioning/test/utils.ts @@ -1,4 +1,4 @@ -import { Enum, Interface, Model, Union } from "@typespec/compiler"; +import type { Enum, Interface, Model, Union } from "@typespec/compiler"; import { ok, strictEqual } from "assert"; export function assertHasProperties(model: Model, props: string[]) { diff --git a/packages/versioning/test/versioned-dependencies.test.ts b/packages/versioning/test/versioned-dependencies.test.ts index 2497b3245..cdba5eabc 100644 --- a/packages/versioning/test/versioned-dependencies.test.ts +++ b/packages/versioning/test/versioned-dependencies.test.ts @@ -1,13 +1,14 @@ -import { Model, Namespace, Operation, Program, projectProgram } from "@typespec/compiler"; +import type { Model, Namespace, Operation, Program } from "@typespec/compiler"; +import { projectProgram } from "@typespec/compiler"; import { - BasicTestRunner, createTestWrapper, expectDiagnosticEmpty, expectDiagnostics, + type BasicTestRunner, } from "@typespec/compiler/testing"; import { ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { buildVersionProjections } from "../src/versioning.js"; +import { buildVersionProjections } from "../src/projection.js"; import { createVersioningTestHost, createVersioningTestRunner } from "./test-host.js"; import { assertHasProperties } from "./utils.js"; diff --git a/packages/versioning/test/versioning-timeline.test.ts b/packages/versioning/test/versioning-timeline.test.ts index 7bae4854e..74346329e 100644 --- a/packages/versioning/test/versioning-timeline.test.ts +++ b/packages/versioning/test/versioning-timeline.test.ts @@ -1,4 +1,4 @@ -import { Namespace } from "@typespec/compiler"; +import type { Namespace } from "@typespec/compiler"; import { deepStrictEqual } from "assert"; import { describe, it } from "vitest"; import { VersioningTimeline } from "../src/versioning-timeline.js"; diff --git a/packages/versioning/test/versioning.test.ts b/packages/versioning/test/versioning.test.ts index 102055e31..78ba856e2 100644 --- a/packages/versioning/test/versioning.test.ts +++ b/packages/versioning/test/versioning.test.ts @@ -1,28 +1,29 @@ import { - Enum, - Interface, - IntrinsicType, - Model, - Namespace, - Operation, - Program, - ProjectionApplication, - Scalar, - Type, - Union, projectProgram, + type Enum, + type Interface, + type IntrinsicType, + type Model, + type Namespace, + type Operation, + type Program, + type ProjectionApplication, + type Scalar, + type Type, + type Union, } from "@typespec/compiler"; import { - BasicTestRunner, createTestWrapper, expectDiagnosticEmpty, expectDiagnostics, + type BasicTestRunner, } from "@typespec/compiler/testing"; import { deepStrictEqual, fail, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Version } from "../src/types.js"; +import { buildVersionProjections, indexTimeline } from "../src/projection.js"; +import type { Version } from "../src/types.js"; import { VersioningTimeline } from "../src/versioning-timeline.js"; -import { buildVersionProjections, getVersions, indexTimeline } from "../src/versioning.js"; +import { getVersions } from "../src/versioning.js"; import { createVersioningTestHost } from "./test-host.js"; import { assertHasMembers, diff --git a/packages/versioning/tsconfig.json b/packages/versioning/tsconfig.json index b50f2b557..438eb4241 100644 --- a/packages/versioning/tsconfig.json +++ b/packages/versioning/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "outDir": "dist", "rootDir": ".", + "verbatimModuleSyntax": true, "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo" }, "include": ["src/**/*.ts", "generated-defs/**/*.ts", "test/**/*.ts"]