diff --git a/.chronus/changes/fix-versioning-sub-namespace-2024-4-2-22-46-51.md b/.chronus/changes/fix-versioning-sub-namespace-2024-4-2-22-46-51.md new file mode 100644 index 0000000000..64456ec7fb --- /dev/null +++ b/.chronus/changes/fix-versioning-sub-namespace-2024-4-2-22-46-51.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/versioning" +--- + +Fix crash when `@service` inside a versioned namespace diff --git a/packages/versioning/src/versioning.ts b/packages/versioning/src/versioning.ts index 9eefb0d044..68846d60a7 100644 --- a/packages/versioning/src/versioning.ts +++ b/packages/versioning/src/versioning.ts @@ -569,10 +569,10 @@ function resolveDependencyVersions( * @param program * @param rootNs Root namespace. */ -export function resolveVersions(program: Program, rootNs: Namespace): VersionResolution[] { - const versions = getVersion(program, rootNs); +export function resolveVersions(program: Program, namespace: Namespace): VersionResolution[] { + const [rootNs, versions] = getVersions(program, namespace); const dependencies = - getVersionDependencies(program, rootNs) ?? + (rootNs && getVersionDependencies(program, rootNs)) ?? new Map | Version>(); if (!versions) { if (dependencies.size === 0) { @@ -581,7 +581,7 @@ export function resolveVersions(program: Program, rootNs: Namespace): VersionRes const map = new Map(); for (const [dependencyNs, version] of dependencies) { if (version instanceof Map) { - const rootNsName = getNamespaceFullName(rootNs); + const rootNsName = getNamespaceFullName(namespace); const dependencyNsName = getNamespaceFullName(dependencyNs); throw new Error( `Unexpected error: Namespace ${rootNsName} version dependency to ${dependencyNsName} should be a picked version.` @@ -593,7 +593,7 @@ export function resolveVersions(program: Program, rootNs: Namespace): VersionRes } } else { return versions.getVersions().map((version) => { - const resolutions = resolveDependencyVersions(program, new Map([[rootNs, version]])); + const resolutions = resolveDependencyVersions(program, new Map([[rootNs!, version]])); return { rootVersion: version, versions: resolutions, @@ -686,51 +686,62 @@ export function getVersionsForEnum(program: Program, en: Enum): [Namespace, Vers } export function getVersions(p: Program, t: Type): [Namespace, VersionMap] | [] { - if (versionCache.has(t)) { - return versionCache.get(t)!; + const existing = versionCache.get(t); + if (existing) { + return existing; + } + + switch (t.kind) { + case "Namespace": + return resolveVersionsForNamespace(p, t); + case "Operation": + case "Interface": + case "Model": + case "Union": + case "Scalar": + case "Enum": + if (t.namespace) { + return cacheVersion(t, getVersions(p, t.namespace) || []); + } else if (t.kind === "Operation" && t.interface) { + return cacheVersion(t, getVersions(p, t.interface) || []); + } else { + return cacheVersion(t, []); + } + case "ModelProperty": + if (t.sourceProperty) { + return getVersions(p, t.sourceProperty); + } else if (t.model) { + return getVersions(p, t.model); + } else { + return cacheVersion(t, []); + } + case "EnumMember": + return cacheVersion(t, getVersions(p, t.enum) || []); + case "UnionVariant": + return cacheVersion(t, getVersions(p, t.union) || []); + default: + return cacheVersion(t, []); } +} - if (t.kind === "Namespace") { - const nsVersion = getVersion(p, t); +function resolveVersionsForNamespace( + program: Program, + namespace: Namespace +): [Namespace, VersionMap] | [] { + const nsVersion = getVersion(program, namespace); - if (nsVersion !== undefined) { - return cacheVersion(t, [t, nsVersion]); - } else if (getUseDependencies(p, t) !== undefined) { - return cacheVersion(t, [t, undefined!]); - } else if (t.namespace) { - return cacheVersion(t, getVersions(p, t.namespace)); - } else { - return cacheVersion(t, [t, undefined!]); - } - } else if ( - t.kind === "Operation" || - t.kind === "Interface" || - t.kind === "Model" || - t.kind === "Union" || - t.kind === "Scalar" || - t.kind === "Enum" - ) { - if (t.namespace) { - return cacheVersion(t, getVersions(p, t.namespace) || []); - } else if (t.kind === "Operation" && t.interface) { - return cacheVersion(t, getVersions(p, t.interface) || []); - } else { - return cacheVersion(t, []); - } - } else if (t.kind === "ModelProperty") { - if (t.sourceProperty) { - return getVersions(p, t.sourceProperty); - } else if (t.model) { - return getVersions(p, t.model); - } else { - return cacheVersion(t, []); - } - } else if (t.kind === "EnumMember") { - return cacheVersion(t, getVersions(p, t.enum) || []); - } else if (t.kind === "UnionVariant") { - return cacheVersion(t, getVersions(p, t.union) || []); + if (nsVersion !== undefined) { + return cacheVersion(namespace, [namespace, nsVersion]); + } + + const parentNamespaceVersion = + namespace.namespace && getVersions(program, namespace.namespace)[1]; + const hasDependencies = getUseDependencies(program, namespace); + + if (parentNamespaceVersion || hasDependencies) { + return cacheVersion(namespace, [namespace, parentNamespaceVersion!]); } else { - return cacheVersion(t, []); + return cacheVersion(namespace, [namespace, undefined!]); } } diff --git a/packages/versioning/test/versioned-dependencies.test.ts b/packages/versioning/test/versioned-dependencies.test.ts index 54e6130a9c..652609c00d 100644 --- a/packages/versioning/test/versioned-dependencies.test.ts +++ b/packages/versioning/test/versioned-dependencies.test.ts @@ -790,6 +790,32 @@ describe("versioning: dependencies", () => { ok(v1.projectedTypes.get(MyService)); }); + // Regression test for https://github.com/microsoft/typespec/issues/3263 + it("service is a nested namespace inside a versioned namespace", async () => { + const { MyService } = (await runner.compile(` + @versioned(Versions) + namespace My.Service { + enum Versions { + @useDependency(Lib.Versions.v1) v1 + } + + @service + @test("MyService") + namespace Sub { + model Foo is Lib.Bar; + } + } + @versioned(Versions) + namespace Lib { + enum Versions { v1 } + model Bar {} + } + `)) as { MyService: Namespace }; + + const [v1] = runProjections(runner.program, MyService); + ok(v1.projectedTypes.get(MyService)); + }); + // Test for https://github.com/microsoft/typespec/issues/786 it("have a nested service namespace and libraries sharing common parent namespace", async () => { const { MyService } = (await runner.compile(`