diff --git a/.chronus/changes/fix-apiversion-clientdefaultvalue-multi-service-2026-2-4-8-54-0.md b/.chronus/changes/fix-apiversion-clientdefaultvalue-multi-service-2026-2-4-8-54-0.md new file mode 100644 index 0000000000..c2b9c25fb2 --- /dev/null +++ b/.chronus/changes/fix-apiversion-clientdefaultvalue-multi-service-2026-2-4-8-54-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Fixed apiVersion parameter clientDefaultValue missing when an operation group contains operations from different services. diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 33e249ba0e..0323d03bf9 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -289,28 +289,75 @@ export function parseEmitterName( return diagnostics.wrap(language); } +/** + * Find the service namespace that contains the given operation. + * @param services Array of service namespaces + * @param operation The operation to find the service for + * @returns The service namespace that contains the operation + */ +function findServiceForOperation(services: Namespace[], operation: Operation): Namespace { + let namespace = operation.namespace; + while (namespace) { + if (services.includes(namespace)) { + return namespace; + } + namespace = namespace.namespace; + } + // Fallback to the first service. This can happen when an operation is defined outside + // of any service namespace (e.g., in Azure.ResourceManager or other shared namespaces) + // and is imported into a client that combines multiple services. In such cases, + // we use the first service's api version as the default. + return services[0]; +} + /** * * @param context * @param type The type that we are adding api version information onto + * @param client The client or operation group that contains the operation + * @param operation The operation that contains the api version parameter (needed for multi-service operation groups) * @returns Whether the type is the api version parameter and the default value for the client */ export function updateWithApiVersionInformation( context: TCGCContext, type: ModelProperty, client?: SdkClient | SdkOperationGroup, + operation?: Operation, ): { isApiVersionParam: boolean; clientDefaultValue?: string; } { const isApiVersionParam = isApiVersion(context, type); - return { - isApiVersionParam, - clientDefaultValue: - isApiVersionParam && client - ? context.__clientApiVersionDefaultValueCache.get(client) - : undefined, - }; + if (!isApiVersionParam || !client) { + return { isApiVersionParam, clientDefaultValue: undefined }; + } + + // For single-service clients, use the cached value + if (client.services.length <= 1) { + return { + isApiVersionParam, + clientDefaultValue: context.__clientApiVersionDefaultValueCache.get(client), + }; + } + + // For multi-service clients/operation groups, we need to find the api version + // from the operation's specific service + if (operation) { + const service = findServiceForOperation(client.services, operation); + const packageVersions = context.getPackageVersions(); + const versions = filterApiVersionsWithDecorators( + context, + type, + packageVersions.get(service) || [], + ); + return { + isApiVersionParam, + clientDefaultValue: versions.length > 0 ? versions[versions.length - 1] : undefined, + }; + } + + // No operation provided for multi-service client, return undefined + return { isApiVersionParam, clientDefaultValue: undefined }; } export function filterApiVersionsWithDecorators( diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index b27016db7c..40ad41ee29 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -1300,6 +1300,7 @@ export function getSdkModelPropertyTypeBase( context, type, operation ? context.getClientForOperation(operation) : undefined, + operation, ), onClient, crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, type, operation), diff --git a/packages/typespec-client-generator-core/test/clients/structure.test.ts b/packages/typespec-client-generator-core/test/clients/structure.test.ts index f69d3231cf..82434ce0e9 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -1732,6 +1732,22 @@ it("client location to new operation group with multiple services", async () => ok(aTestMethod); const bTestMethod = newOpGroup.methods.find((m) => m.name === "bTest"); ok(bTestMethod); + + // Check operation-level api version parameters have correct clientDefaultValue + // This is the fix for the bug - previously these were undefined + strictEqual(aTestMethod.kind, "basic"); + const aOperation = aTestMethod.operation; + const aOperationApiVersionParam = aOperation.parameters.find((p) => p.isApiVersionParam); + ok(aOperationApiVersionParam); + // Operation from ServiceA should have ServiceA's latest api version as default + strictEqual(aOperationApiVersionParam.clientDefaultValue, "av2"); + + strictEqual(bTestMethod.kind, "basic"); + const bOperation = bTestMethod.operation; + const bOperationApiVersionParam = bOperation.parameters.find((p) => p.isApiVersionParam); + ok(bOperationApiVersionParam); + // Operation from ServiceB should have ServiceB's latest api version as default + strictEqual(bOperationApiVersionParam.clientDefaultValue, "bv2"); }); it("one client from multiple services with operation group name conflict - merged", async () => { @@ -1805,6 +1821,21 @@ it("one client from multiple services with operation group name conflict - merge ok(aTestMethod); const bTestMethod = operations.methods.find((m) => m.name === "bTest"); ok(bTestMethod); + + // Check operation-level api version parameters have correct clientDefaultValue + strictEqual(aTestMethod.kind, "basic"); + const aOperation = aTestMethod.operation; + const aOperationApiVersionParam = aOperation.parameters.find((p) => p.isApiVersionParam); + ok(aOperationApiVersionParam); + // Operation from ServiceA should have ServiceA's latest api version as default + strictEqual(aOperationApiVersionParam.clientDefaultValue, "av2"); + + strictEqual(bTestMethod.kind, "basic"); + const bOperation = bTestMethod.operation; + const bOperationApiVersionParam = bOperation.parameters.find((p) => p.isApiVersionParam); + ok(bOperationApiVersionParam); + // Operation from ServiceB should have ServiceB's latest api version as default + strictEqual(bOperationApiVersionParam.clientDefaultValue, "bv2"); }); it("client location to existing operation group from different service", async () => {