From f83dcb0b34011d0fd438fb7a22604d7ac77afaae Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 5 Mar 2026 12:24:57 -0500 Subject: [PATCH 1/2] Fix running multiple versioning mutators --- packages/versioning/src/versioning.ts | 21 +++++++- .../apply-snapshot-versioning.test.ts | 52 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/versioning/src/versioning.ts b/packages/versioning/src/versioning.ts index 5089fb2f0d7..0999228273f 100644 --- a/packages/versioning/src/versioning.ts +++ b/packages/versioning/src/versioning.ts @@ -391,13 +391,30 @@ export function getAvailabilityMapInTimeline( ) return undefined; - added = resolveWhenFirstAdded(added, removed, parentAdded); + // Only keep versioning info related to this timeline + const timelineAdded = added.filter((x) => timeline.getIndex(x) !== -1); + const timelineRemoved = removed.filter((x) => timeline.getIndex(x) !== -1); + const hasTypeChangedInTimeline = + typeChanged !== undefined && [...typeChanged.keys()].some((v) => timeline.getIndex(v) !== -1); + const hasReturnTypeChangedInTimeline = + returnTypeChanged !== undefined && + [...returnTypeChanged.keys()].some((v) => timeline.getIndex(v) !== -1); + + if ( + !timelineAdded.length && + !timelineRemoved.length && + !hasTypeChangedInTimeline && + !hasReturnTypeChangedInTimeline + ) + return undefined; + + added = resolveWhenFirstAdded(timelineAdded, timelineRemoved, parentAdded); // something isn't available by default let isAvail = false; for (const [index, moment] of timeline.entries()) { const add = added.find((x) => timeline.getIndex(x) === index); - const rem = removed.find((x) => timeline.getIndex(x) === index); + const rem = timelineRemoved.find((x) => timeline.getIndex(x) === index); if (rem) { isAvail = false; avail.set(moment, Availability.Removed); diff --git a/packages/versioning/test/mutations/apply-snapshot-versioning.test.ts b/packages/versioning/test/mutations/apply-snapshot-versioning.test.ts index 421f43ebc65..9c997ca69ea 100644 --- a/packages/versioning/test/mutations/apply-snapshot-versioning.test.ts +++ b/packages/versioning/test/mutations/apply-snapshot-versioning.test.ts @@ -1,5 +1,6 @@ import type { Namespace, Scalar, Type } from "@typespec/compiler"; import { unsafe_mutateSubgraphWithNamespace } from "@typespec/compiler/experimental"; +import { t } from "@typespec/compiler/testing"; import { strictEqual } from "assert"; import { describe, expect, it } from "vitest"; import { getVersioningMutators } from "../../src/mutator.js"; @@ -211,3 +212,54 @@ describe("operations in interface", () => { (decorators) => `interface Test { ${decorators} A(): void; }`, ); }); + +describe("apply multiple versioning mutators", () => { + // https://github.com/microsoft/typespec/issues/9927 + it("properties with @added from different services are preserved", async () => { + const { ServiceA, ServiceB, program } = await Tester.compile(t.code` + @versioned(VersionsA) + namespace ${t.namespace("ServiceA")} { + enum VersionsA { av1, av2 } + model Foo { + name: string; + @added(VersionsA.av2) + description?: string; + } + } + + @versioned(VersionsB) + namespace ${t.namespace("ServiceB")} { + enum VersionsB { bv1, bv2 } + model Bar { + id: int32; + @added(VersionsB.bv2) + value?: string; + } + } + `); + + const serviceAMutators = getVersioningMutators(program, ServiceA); + const serviceBMutators = getVersioningMutators(program, ServiceB); + + strictEqual(serviceAMutators?.kind, "versioned"); + strictEqual(serviceBMutators?.kind, "versioned"); + + const serviceAV2 = serviceAMutators.snapshots[1].mutator; + const serviceBV2 = serviceBMutators.snapshots[1].mutator; + + const globalNs = program.getGlobalNamespaceType(); + const result = unsafe_mutateSubgraphWithNamespace(program, [serviceAV2, serviceBV2], globalNs); + + const mutatedGlobal = result.type as Namespace; + const serviceA = mutatedGlobal.namespaces.get("ServiceA")!; + const serviceB = mutatedGlobal.namespaces.get("ServiceB")!; + + const foo = serviceA.models.get("Foo")!; + expect(foo.properties.has("name")).toBe(true); + expect(foo.properties.has("description")).toBe(true); + + const bar = serviceB.models.get("Bar")!; + expect(bar.properties.has("id")).toBe(true); + expect(bar.properties.has("value")).toBe(true); + }); +}); From 454c7096ab4091f34bf6f22f9333017288b73e59 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 5 Mar 2026 12:25:56 -0500 Subject: [PATCH 2/2] m --- .../fix-multi-mutators-versioning-2026-2-5-12-25-51.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/fix-multi-mutators-versioning-2026-2-5-12-25-51.md diff --git a/.chronus/changes/fix-multi-mutators-versioning-2026-2-5-12-25-51.md b/.chronus/changes/fix-multi-mutators-versioning-2026-2-5-12-25-51.md new file mode 100644 index 00000000000..e0b309a0c03 --- /dev/null +++ b/.chronus/changes/fix-multi-mutators-versioning-2026-2-5-12-25-51.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/versioning" +--- + +[API] Fix running multiple versioning mutators together \ No newline at end of file