From afd3ca5e724a8a2c45aeda6b200befd2bc9b6c8f Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 6 Aug 2026 11:05:25 -0400 Subject: [PATCH 01/11] feat(tcgc): support overriding method responses Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- ...ace-client-response-2026-08-06-10-49-09.md | 7 + .../Azure.ClientGenerator.Core.ts | 21 ++ .../lib/functions.tsp | 21 ++ .../src/functions.ts | 42 ++- .../src/methods.ts | 268 +++++++++++++----- .../src/tsp-index.ts | 15 +- .../test/functions/replace-response.test.ts | 62 ++++ 7 files changed, 364 insertions(+), 72 deletions(-) create mode 100644 .chronus/changes/replace-client-response-2026-08-06-10-49-09.md create mode 100644 packages/typespec-client-generator-core/test/functions/replace-response.test.ts diff --git a/.chronus/changes/replace-client-response-2026-08-06-10-49-09.md b/.chronus/changes/replace-client-response-2026-08-06-10-49-09.md new file mode 100644 index 0000000000..a86bdb6ef1 --- /dev/null +++ b/.chronus/changes/replace-client-response-2026-08-06-10-49-09.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Allow `@override` to replace a client method response and add the `replaceResponse` customization function. diff --git a/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts b/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts index e634926399..1416990b49 100644 --- a/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts +++ b/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts @@ -1260,6 +1260,26 @@ export type ReorderParametersFunctionImplementation = ( order: readonly string[], ) => Operation; +/** + * Replace the method response type of an operation. + * This preserves the HTTP response metadata and only changes the return type + * of the generated client method when used with `@@override`. + * + * @param operation The operation to transform. + * @param response The replacement method response type. + * @returns A new operation with the response type replaced. + * @example Replace a response with void + * ```typespec + * alias DeleteResponse = replaceResponse(MyService.delete, void); + * @@override(MyService.delete, DeleteResponse); + * ``` + */ +export type ReplaceResponseFunctionImplementation = ( + context: FunctionContext, + operation: Operation, + response: Type, +) => Operation; + /** * Mark a client name as exact, preventing language emitters from applying * their usual casing transformations (e.g., snake_case for Python, camelCase for JavaScript). @@ -1287,5 +1307,6 @@ export type AzureClientGeneratorCoreFunctions = { removeParameter: RemoveParameterFunctionImplementation; addParameter: AddParameterFunctionImplementation; reorderParameters: ReorderParametersFunctionImplementation; + replaceResponse: ReplaceResponseFunctionImplementation; exact: ExactFunctionImplementation; }; diff --git a/packages/typespec-client-generator-core/lib/functions.tsp b/packages/typespec-client-generator-core/lib/functions.tsp index 849aea16e1..99f5b439c6 100644 --- a/packages/typespec-client-generator-core/lib/functions.tsp +++ b/packages/typespec-client-generator-core/lib/functions.tsp @@ -130,6 +130,27 @@ extern fn reorderParameters( order: valueof string[] ): Reflection.Operation; +/** + * Replace the method response type of an operation. + * This preserves the HTTP response metadata and only changes the return type + * of the generated client method when used with `@@override`. + * + * @param operation The operation to transform. + * @param response The replacement method response type. + * @returns A new operation with the response type replaced. + * + * @example Replace a response with void + * ```typespec + * alias DeleteResponse = replaceResponse(MyService.delete, void); + * @@override(MyService.delete, DeleteResponse); + * ``` + */ +#suppress "experimental-feature" "replaceResponse uses extern fn which is experimental but provides essential response transformation functionality" +extern fn replaceResponse( + operation: Reflection.Operation, + response: unknown +): Reflection.Operation; + /** * Mark a client name as exact, preventing language emitters from applying * their usual casing transformations (e.g., snake_case for Python, camelCase for JavaScript). diff --git a/packages/typespec-client-generator-core/src/functions.ts b/packages/typespec-client-generator-core/src/functions.ts index f04832f4e6..10433e5938 100644 --- a/packages/typespec-client-generator-core/src/functions.ts +++ b/packages/typespec-client-generator-core/src/functions.ts @@ -1,4 +1,9 @@ -import type { FunctionContext, ModelProperty, Operation, Type } from "@typespec/compiler"; +import type { + FunctionContext, + ModelProperty, + Operation, + Type, +} from "@typespec/compiler"; import { $ } from "@typespec/compiler/typekit"; import { EXACT_NAME_PREFIX } from "./internal-utils.js"; import { reportDiagnostic } from "./lib.js"; @@ -14,7 +19,9 @@ function cloneOperation( ): Operation { const newOp = tk.operation.create({ name: operation.name, - parameters: options.parameters ?? [...operation.parameters.properties.values()], + parameters: options.parameters ?? [ + ...operation.parameters.properties.values(), + ], returnType: options.returnType ?? operation.returnType, }); @@ -32,7 +39,10 @@ function cloneOperation( } // Helper function to clone a model property -function cloneModelProperty(tk: ReturnType, prop: ModelProperty): ModelProperty { +function cloneModelProperty( + tk: ReturnType, + prop: ModelProperty, +): ModelProperty { const clonedProp = tk.modelProperty.create({ name: prop.name, type: prop.type, @@ -240,6 +250,27 @@ export function reorderParameters( return cloneOperation(tk, operation, { parameters: newProperties }); } +/** + * Replace the method response type of an operation. + * + * The operation's HTTP response metadata is preserved; only the client method + * return type is changed when the operation is used with `@override`. + * + * @param context The function context provided by TypeSpec + * @param operation The operation to transform + * @param response The replacement method response type + * @returns A new operation with the response type replaced + */ +export function replaceResponse( + context: FunctionContext, + operation: Operation, + response: Type, +): Operation { + return cloneOperation($(context.program), operation, { + returnType: response, + }); +} + /** * Mark a client name as exact, preventing language emitters from applying * their usual casing transformations. @@ -269,7 +300,10 @@ export function hasExactNameMarker(name: string): boolean { * @param name The name to normalize * @returns An object with the clean name and whether it was marked as exact */ -export function normalizeExactName(name: string): { name: string; isExactName: boolean } { +export function normalizeExactName(name: string): { + name: string; + isExactName: boolean; +} { if (name.startsWith(EXACT_NAME_PREFIX)) { return { name: name.slice(EXACT_NAME_PREFIX.length), isExactName: true }; } diff --git a/packages/typespec-client-generator-core/src/methods.ts b/packages/typespec-client-generator-core/src/methods.ts index c0655074b3..a5e83ee4e8 100644 --- a/packages/typespec-client-generator-core/src/methods.ts +++ b/packages/typespec-client-generator-core/src/methods.ts @@ -115,20 +115,28 @@ function getSdkServiceOperation( ); return diagnostics.wrap(undefined as any); } -function getSdkLroPagingServiceMethod( +function getSdkLroPagingServiceMethod< + TServiceOperation extends SdkServiceOperation, +>( context: TCGCContext, operation: Operation, client: SdkClientType, ): [SdkLroPagingServiceMethod, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); return diagnostics.wrap({ - ...diagnostics.pipe(getSdkLroServiceMethod(context, operation, client)), - ...diagnostics.pipe(getSdkPagingServiceMethod(context, operation, client)), + ...diagnostics.pipe( + getSdkLroServiceMethod(context, operation, client), + ), + ...diagnostics.pipe( + getSdkPagingServiceMethod(context, operation, client), + ), kind: "lropaging", }); } -function getPageSizeParameterSegments( +function getPageSizeParameterSegments< + TServiceOperation extends SdkServiceOperation, +>( baseServiceMethod: SdkServiceMethod, ): (SdkModelPropertyType | SdkMethodParameter)[] { function recurseToFindPageSizeParameterInModel( @@ -136,7 +144,10 @@ function getPageSizeParameterSegments d.definition?.name === "@pageSize")) { + if ( + prop.__raw && + prop.__raw.decorators.find((d) => d.definition?.name === "@pageSize") + ) { return [param, prop]; } if (prop.type.kind === "model") { @@ -149,7 +160,10 @@ function getPageSizeParameterSegments d.definition?.name === "@pageSize")) { + if ( + p.__raw && + p.__raw.decorators.find((d) => d.definition?.name === "@pageSize") + ) { return [p]; } if (p.type.kind === "model") { @@ -159,7 +173,9 @@ function getPageSizeParameterSegments( +function getSdkPagingServiceMethod< + TServiceOperation extends SdkServiceOperation, +>( context: TCGCContext, operation: Operation, client: SdkClientType, @@ -183,7 +199,9 @@ function getSdkPagingServiceMethod - context.__modelPropertyCache.get(resultSegment)!, + baseServiceMethod.response.resultSegments = resultSegments?.map( + (resultSegment) => context.__modelPropertyCache.get(resultSegment)!, ); context.__pagedResultSet.add(responseType); @@ -226,16 +244,21 @@ function getSdkPagingServiceMethod context.__methodParameterCache.get(r) ?? context.__modelPropertyCache.get(r)!, - ), - continuationTokenResponseSegments: continuationTokenResponseSegments?.map( - (segment) => - context.__responseHeaderCache.get(segment) ?? - context.__modelPropertyCache.get(segment)!, - ), + continuationTokenParameterSegments: + pagingMetadata.input.continuationToken?.path.map( + (r) => + context.__methodParameterCache.get(r) ?? + context.__modelPropertyCache.get(r)!, + ), + continuationTokenResponseSegments: + continuationTokenResponseSegments?.map( + (segment) => + context.__responseHeaderCache.get(segment) ?? + context.__modelPropertyCache.get(segment)!, + ), pageItemsSegments: baseServiceMethod.response.resultSegments, - pageSizeParameterSegments: getPageSizeParameterSegments(baseServiceMethod), + pageSizeParameterSegments: + getPageSizeParameterSegments(baseServiceMethod), nextLinkReInjectedParametersSegments: pagingMetadata.output.nextLink?.property.type.kind === "Scalar" ? ( @@ -248,7 +271,8 @@ function getSdkPagingServiceMethod p.__raw?.kind === "ModelProperty" && - findRootSourceProperty(p.__raw) === findRootSourceProperty(t), + findRootSourceProperty(p.__raw) === + findRootSourceProperty(t), )!, ) : undefined, @@ -258,7 +282,11 @@ function getSdkPagingServiceMethod 0 && responseModel) { @@ -311,7 +347,8 @@ function mapFirstSegmentForResultSegments( for (const property of current.properties ?? []) { if ( property.__raw && - findRootSourceProperty(property.__raw) === findRootSourceProperty(segment) + findRootSourceProperty(property.__raw) === + findRootSourceProperty(segment) ) { return [property.__raw, ...resultSegments.slice(i + 1)]; } @@ -327,16 +364,24 @@ export function getPropertySegmentsFromModelOrParameters( source: SdkModelType | SdkMethodParameter[], predicate: (property: SdkMethodParameter | SdkModelPropertyType) => boolean, ): (SdkMethodParameter | SdkModelPropertyType)[] | undefined { - const queue: { model: SdkModelType; path: (SdkMethodParameter | SdkModelPropertyType)[] }[] = []; + const queue: { + model: SdkModelType; + path: (SdkMethodParameter | SdkModelPropertyType)[]; + }[] = []; if (!Array.isArray(source)) { if (source.baseModel) { - const baseResult = getPropertySegmentsFromModelOrParameters(source.baseModel, predicate); + const baseResult = getPropertySegmentsFromModelOrParameters( + source.baseModel, + predicate, + ); if (baseResult) return baseResult; } } - for (const prop of Array.isArray(source) ? source : source.properties.values()) { + for (const prop of Array.isArray(source) + ? source + : source.properties.values()) { if (predicate(prop)) { return [prop]; } @@ -367,13 +412,16 @@ function getSdkLroServiceMethod( client: SdkClientType, ): [SdkLroServiceMethod, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const metadata = diagnostics.pipe(getServiceMethodLroMetadata(context, operation, client))!; + const metadata = diagnostics.pipe( + getServiceMethodLroMetadata(context, operation, client), + )!; const baseServiceMethod = diagnostics.pipe( getSdkBasicServiceMethod(context, operation, client), ); baseServiceMethod.response.type = metadata.finalResponse?.result; - baseServiceMethod.response.resultSegments = metadata.finalResponse?.resultSegments; + baseServiceMethod.response.resultSegments = + metadata.finalResponse?.resultSegments; return diagnostics.wrap({ ...baseServiceMethod, @@ -391,7 +439,9 @@ function getSdkLroServiceMethod( }); } -function getServiceMethodLroMetadata( +function getServiceMethodLroMetadata< + TServiceOperation extends SdkServiceOperation, +>( context: TCGCContext, operation: Operation, client: SdkClientType, @@ -412,14 +462,23 @@ function getServiceMethodLroMetadata( + function getSdkOperationReference< + TServiceOperation extends SdkServiceOperation, + >( context: TCGCContext, reference: OperationReference, client: SdkClientType, @@ -518,11 +592,14 @@ function getServiceMethodLroMetadata, ): SdkMethodResponse { const responses = sdkOperation.responses; + const overriddenOperation = getOverriddenClientMethod(context, operation); + const responseOverride = overriddenOperation?.returnType; const allResponseBodies: SdkType[] = []; let containsResponseWithoutBody = false; @@ -620,9 +708,17 @@ function getSdkMethodResponse( } }); - const responseTypes = new Set(allResponseBodies.map((x) => getHashForType(x))); + const responseTypes = new Set( + allResponseBodies.map((x) => getHashForType(x)), + ); let type: SdkType | undefined = undefined; - if (getResponseAsBool(context, operation)) { + if (responseOverride && isNeverOrVoidType(responseOverride)) { + type = undefined; + } else if (responseOverride) { + type = ignoreDiagnostics( + getClientTypeWithDiagnostics(context, responseOverride, operation), + ); + } else if (getResponseAsBool(context, operation)) { type = getSdkBuiltInType(context, $(context.program).builtin.boolean); } else { if (responseTypes.size > 1) { @@ -674,7 +770,9 @@ function getSdkMethodResponse( }; } -export function getSdkBasicServiceMethod( +export function getSdkBasicServiceMethod< + TServiceOperation extends SdkServiceOperation, +>( context: TCGCContext, operation: Operation, client: SdkClientType, @@ -702,7 +800,9 @@ export function getSdkBasicServiceMethod(context, operation, methodParameters, client), + getSdkServiceOperation( + context, + operation, + methodParameters, + client, + ), + ); + const response = getSdkMethodResponse( + context, + operation, + serviceOperation, + client, ); - const response = getSdkMethodResponse(context, operation, serviceOperation, client); const name = getLibraryName(context, operation); return diagnostics.wrap({ __raw: operation, @@ -754,15 +864,32 @@ function getSdkServiceMethod( const pagingDisabled = getDisablePageable(context, operation); const paging = !pagingDisabled && - (isList(context.program, operation) || getMarkAsPageable(context, operation)); + (isList(context.program, operation) || + getMarkAsPageable(context, operation)); if (lro && paging) { - return getSdkLroPagingServiceMethod(context, operation, client); + return getSdkLroPagingServiceMethod( + context, + operation, + client, + ); } else if (paging) { - return getSdkPagingServiceMethod(context, operation, client); + return getSdkPagingServiceMethod( + context, + operation, + client, + ); } else if (lro) { - return getSdkLroServiceMethod(context, operation, client); + return getSdkLroServiceMethod( + context, + operation, + client, + ); } - return getSdkBasicServiceMethod(context, operation, client); + return getSdkBasicServiceMethod( + context, + operation, + client, + ); } export function getSdkMethodParameter( @@ -777,12 +904,19 @@ export function getSdkMethodParameter( if (!property) { // for parameter that has elevated to client or parent client, we will use the client parameter directly if (operation) { - const correspondingClientParam = getCorrespondingClientParam(context, type, operation); - if (correspondingClientParam) return diagnostics.wrap(correspondingClientParam); + const correspondingClientParam = getCorrespondingClientParam( + context, + type, + operation, + ); + if (correspondingClientParam) + return diagnostics.wrap(correspondingClientParam); } property = { - ...diagnostics.pipe(getSdkModelPropertyTypeBase(context, type, operation)), + ...diagnostics.pipe( + getSdkModelPropertyTypeBase(context, type, operation), + ), kind: "method", }; @@ -800,7 +934,13 @@ export function createSdkMethods( const retval: SdkMethod[] = []; for (const operation of listOperationsInClient(context, client)) { retval.push( - diagnostics.pipe(getSdkServiceMethod(context, operation, sdkClientType)), + diagnostics.pipe( + getSdkServiceMethod( + context, + operation, + sdkClientType, + ), + ), ); } return diagnostics.wrap(retval); diff --git a/packages/typespec-client-generator-core/src/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index eb907ec382..7ac3b695ca 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -38,6 +38,7 @@ import { exact, removeParameter, reorderParameters, + replaceResponse, replaceParameter, } from "./functions.js"; @@ -87,10 +88,16 @@ export const $functions: Record = { // Note: The generated AzureClientGeneratorCoreFunctions type simplifies function signatures. // The actual implementation has the full parameter signature required by TypeSpec. "Azure.ClientGenerator.Core": { - replaceParameter: replaceParameter as AzureClientGeneratorCoreFunctions["replaceParameter"], - removeParameter: removeParameter as AzureClientGeneratorCoreFunctions["removeParameter"], - addParameter: addParameter as AzureClientGeneratorCoreFunctions["addParameter"], - reorderParameters: reorderParameters as AzureClientGeneratorCoreFunctions["reorderParameters"], + replaceParameter: + replaceParameter as AzureClientGeneratorCoreFunctions["replaceParameter"], + removeParameter: + removeParameter as AzureClientGeneratorCoreFunctions["removeParameter"], + addParameter: + addParameter as AzureClientGeneratorCoreFunctions["addParameter"], + reorderParameters: + reorderParameters as AzureClientGeneratorCoreFunctions["reorderParameters"], + replaceResponse: + replaceResponse as AzureClientGeneratorCoreFunctions["replaceResponse"], exact: exact as AzureClientGeneratorCoreFunctions["exact"], }, }; diff --git a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts new file mode 100644 index 0000000000..4cad4b4260 --- /dev/null +++ b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts @@ -0,0 +1,62 @@ +import { ok, strictEqual } from "assert"; +import { it } from "vitest"; +import { + createSdkContextForTester, + SimpleTesterWithService, +} from "../tester.js"; +import { getServiceMethodOfClient } from "../utils.js"; + +it("replaces the generated method response without changing HTTP responses", async () => { + const { program } = await SimpleTesterWithService.compile(` + @error + model Error { + code: string; + } + + model Widget { + name: string; + } + + @post op create(): Widget | Error; + + alias CustomizedCreate = replaceResponse(TestService.create, void); + @@override(TestService.create, CustomizedCreate); + `); + + const context = await createSdkContextForTester(program); + const method = getServiceMethodOfClient(context.sdkPackage); + + strictEqual(method.response.type, undefined); + strictEqual(method.operation.responses.length, 1); + const response = method.operation.responses[0]; + ok(response.type); + strictEqual(response.type.kind, "model"); + strictEqual(response.type.name, "Widget"); + strictEqual(method.operation.exceptions.length, 1); +}); + +it("uses a different response type supplied by @override", async () => { + const { program } = await SimpleTesterWithService.compile(` + model Widget { + name: string; + } + + model DeleteResult { + deleted: boolean; + } + + @post op create(): Widget; + + op customizedCreate(): DeleteResult; + @@override(TestService.create, TestService.customizedCreate); + `); + + const context = await createSdkContextForTester(program); + const method = getServiceMethodOfClient(context.sdkPackage); + + ok(method.response.type); + strictEqual(method.response.type.kind, "model"); + strictEqual(method.response.type.name, "DeleteResult"); + ok(method.operation.responses[0].type); + strictEqual(method.operation.responses[0].type.name, "Widget"); +}); From 899e12ab59556ade3fbb96f80e1605688b50aa5d Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 6 Aug 2026 11:08:38 -0400 Subject: [PATCH 02/11] style: format response override changes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- .../lib/functions.tsp | 5 +- .../src/functions.ts | 16 +- .../src/methods.ts | 257 +++++------------- .../src/tsp-index.ts | 17 +- .../test/functions/replace-response.test.ts | 5 +- 5 files changed, 74 insertions(+), 226 deletions(-) diff --git a/packages/typespec-client-generator-core/lib/functions.tsp b/packages/typespec-client-generator-core/lib/functions.tsp index 99f5b439c6..e7f6a064e1 100644 --- a/packages/typespec-client-generator-core/lib/functions.tsp +++ b/packages/typespec-client-generator-core/lib/functions.tsp @@ -146,10 +146,7 @@ extern fn reorderParameters( * ``` */ #suppress "experimental-feature" "replaceResponse uses extern fn which is experimental but provides essential response transformation functionality" -extern fn replaceResponse( - operation: Reflection.Operation, - response: unknown -): Reflection.Operation; +extern fn replaceResponse(operation: Reflection.Operation, response: unknown): Reflection.Operation; /** * Mark a client name as exact, preventing language emitters from applying diff --git a/packages/typespec-client-generator-core/src/functions.ts b/packages/typespec-client-generator-core/src/functions.ts index 10433e5938..4711c80568 100644 --- a/packages/typespec-client-generator-core/src/functions.ts +++ b/packages/typespec-client-generator-core/src/functions.ts @@ -1,9 +1,4 @@ -import type { - FunctionContext, - ModelProperty, - Operation, - Type, -} from "@typespec/compiler"; +import type { FunctionContext, ModelProperty, Operation, Type } from "@typespec/compiler"; import { $ } from "@typespec/compiler/typekit"; import { EXACT_NAME_PREFIX } from "./internal-utils.js"; import { reportDiagnostic } from "./lib.js"; @@ -19,9 +14,7 @@ function cloneOperation( ): Operation { const newOp = tk.operation.create({ name: operation.name, - parameters: options.parameters ?? [ - ...operation.parameters.properties.values(), - ], + parameters: options.parameters ?? [...operation.parameters.properties.values()], returnType: options.returnType ?? operation.returnType, }); @@ -39,10 +32,7 @@ function cloneOperation( } // Helper function to clone a model property -function cloneModelProperty( - tk: ReturnType, - prop: ModelProperty, -): ModelProperty { +function cloneModelProperty(tk: ReturnType, prop: ModelProperty): ModelProperty { const clonedProp = tk.modelProperty.create({ name: prop.name, type: prop.type, diff --git a/packages/typespec-client-generator-core/src/methods.ts b/packages/typespec-client-generator-core/src/methods.ts index a5e83ee4e8..97012dbe00 100644 --- a/packages/typespec-client-generator-core/src/methods.ts +++ b/packages/typespec-client-generator-core/src/methods.ts @@ -115,28 +115,20 @@ function getSdkServiceOperation( ); return diagnostics.wrap(undefined as any); } -function getSdkLroPagingServiceMethod< - TServiceOperation extends SdkServiceOperation, ->( +function getSdkLroPagingServiceMethod( context: TCGCContext, operation: Operation, client: SdkClientType, ): [SdkLroPagingServiceMethod, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); return diagnostics.wrap({ - ...diagnostics.pipe( - getSdkLroServiceMethod(context, operation, client), - ), - ...diagnostics.pipe( - getSdkPagingServiceMethod(context, operation, client), - ), + ...diagnostics.pipe(getSdkLroServiceMethod(context, operation, client)), + ...diagnostics.pipe(getSdkPagingServiceMethod(context, operation, client)), kind: "lropaging", }); } -function getPageSizeParameterSegments< - TServiceOperation extends SdkServiceOperation, ->( +function getPageSizeParameterSegments( baseServiceMethod: SdkServiceMethod, ): (SdkModelPropertyType | SdkMethodParameter)[] { function recurseToFindPageSizeParameterInModel( @@ -144,10 +136,7 @@ function getPageSizeParameterSegments< model: SdkModelType, ): (SdkModelPropertyType | SdkMethodParameter)[] { for (const prop of model.properties) { - if ( - prop.__raw && - prop.__raw.decorators.find((d) => d.definition?.name === "@pageSize") - ) { + if (prop.__raw && prop.__raw.decorators.find((d) => d.definition?.name === "@pageSize")) { return [param, prop]; } if (prop.type.kind === "model") { @@ -160,10 +149,7 @@ function getPageSizeParameterSegments< return []; } for (const p of baseServiceMethod.parameters) { - if ( - p.__raw && - p.__raw.decorators.find((d) => d.definition?.name === "@pageSize") - ) { + if (p.__raw && p.__raw.decorators.find((d) => d.definition?.name === "@pageSize")) { return [p]; } if (p.type.kind === "model") { @@ -173,9 +159,7 @@ function getPageSizeParameterSegments< return []; } -function getSdkPagingServiceMethod< - TServiceOperation extends SdkServiceOperation, ->( +function getSdkPagingServiceMethod( context: TCGCContext, operation: Operation, client: SdkClientType, @@ -199,9 +183,7 @@ function getSdkPagingServiceMethod< ); compilerAssert( - responseType?.__raw?.kind === "Model" && - responseType.kind === "model" && - !!pagingMetadata, + responseType?.__raw?.kind === "Model" && responseType.kind === "model" && !!pagingMetadata, "The response object for the pageable operation is either not a paging model, or is not correctly decorated with @nextLink and @pageItems.", operation, ); @@ -219,8 +201,8 @@ function getSdkPagingServiceMethod< baseServiceMethod.response, ); - baseServiceMethod.response.resultSegments = resultSegments?.map( - (resultSegment) => context.__modelPropertyCache.get(resultSegment)!, + baseServiceMethod.response.resultSegments = resultSegments?.map((resultSegment) => + context.__modelPropertyCache.get(resultSegment)!, ); context.__pagedResultSet.add(responseType); @@ -244,21 +226,16 @@ function getSdkPagingServiceMethod< context.__modelPropertyCache.get(segment)!, ), nextLinkVerb: getNextLinkVerb(context, operation), - continuationTokenParameterSegments: - pagingMetadata.input.continuationToken?.path.map( - (r) => - context.__methodParameterCache.get(r) ?? - context.__modelPropertyCache.get(r)!, - ), - continuationTokenResponseSegments: - continuationTokenResponseSegments?.map( - (segment) => - context.__responseHeaderCache.get(segment) ?? - context.__modelPropertyCache.get(segment)!, - ), + continuationTokenParameterSegments: pagingMetadata.input.continuationToken?.path.map( + (r) => context.__methodParameterCache.get(r) ?? context.__modelPropertyCache.get(r)!, + ), + continuationTokenResponseSegments: continuationTokenResponseSegments?.map( + (segment) => + context.__responseHeaderCache.get(segment) ?? + context.__modelPropertyCache.get(segment)!, + ), pageItemsSegments: baseServiceMethod.response.resultSegments, - pageSizeParameterSegments: - getPageSizeParameterSegments(baseServiceMethod), + pageSizeParameterSegments: getPageSizeParameterSegments(baseServiceMethod), nextLinkReInjectedParametersSegments: pagingMetadata.output.nextLink?.property.type.kind === "Scalar" ? ( @@ -271,8 +248,7 @@ function getSdkPagingServiceMethod< baseServiceMethod.parameters, (p) => p.__raw?.kind === "ModelProperty" && - findRootSourceProperty(p.__raw) === - findRootSourceProperty(t), + findRootSourceProperty(p.__raw) === findRootSourceProperty(t), )!, ) : undefined, @@ -282,11 +258,7 @@ function getSdkPagingServiceMethod< const markAsPageableInfo = getMarkAsPageable(context, operation); if (markAsPageableInfo) { const itemsProperty = diagnostics.pipe( - getSdkModelPropertyType( - context, - markAsPageableInfo.itemsProperty, - operation, - ), + getSdkModelPropertyType(context, markAsPageableInfo.itemsProperty, operation), ); // Set resultSegments to match the behavior of normal paging operations @@ -297,11 +269,7 @@ function getSdkPagingServiceMethod< } // tcgc will let all paging method return a list of items baseServiceMethod.response.type = diagnostics.pipe( - getClientTypeWithDiagnostics( - context, - markAsPageableInfo.itemsProperty.type, - operation, - ), + getClientTypeWithDiagnostics(context, markAsPageableInfo.itemsProperty.type, operation), ); return diagnostics.wrap({ @@ -313,10 +281,7 @@ function getSdkPagingServiceMethod< }, }); } else { - compilerAssert( - false, - "Unexpected operation should be paged if calling this function", - ); + compilerAssert(false, "Unexpected operation should be paged if calling this function"); } } } @@ -335,8 +300,7 @@ function mapFirstSegmentForResultSegments( const responseModel = response.type?.kind === "model" ? response.type - : response.type?.kind === "nullable" && - response.type.type.kind === "model" + : response.type?.kind === "nullable" && response.type.type.kind === "model" ? response.type.type : undefined; if (resultSegments.length > 0 && responseModel) { @@ -347,8 +311,7 @@ function mapFirstSegmentForResultSegments( for (const property of current.properties ?? []) { if ( property.__raw && - findRootSourceProperty(property.__raw) === - findRootSourceProperty(segment) + findRootSourceProperty(property.__raw) === findRootSourceProperty(segment) ) { return [property.__raw, ...resultSegments.slice(i + 1)]; } @@ -371,17 +334,12 @@ export function getPropertySegmentsFromModelOrParameters( if (!Array.isArray(source)) { if (source.baseModel) { - const baseResult = getPropertySegmentsFromModelOrParameters( - source.baseModel, - predicate, - ); + const baseResult = getPropertySegmentsFromModelOrParameters(source.baseModel, predicate); if (baseResult) return baseResult; } } - for (const prop of Array.isArray(source) - ? source - : source.properties.values()) { + for (const prop of Array.isArray(source) ? source : source.properties.values()) { if (predicate(prop)) { return [prop]; } @@ -412,16 +370,13 @@ function getSdkLroServiceMethod( client: SdkClientType, ): [SdkLroServiceMethod, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const metadata = diagnostics.pipe( - getServiceMethodLroMetadata(context, operation, client), - )!; + const metadata = diagnostics.pipe(getServiceMethodLroMetadata(context, operation, client))!; const baseServiceMethod = diagnostics.pipe( getSdkBasicServiceMethod(context, operation, client), ); baseServiceMethod.response.type = metadata.finalResponse?.result; - baseServiceMethod.response.resultSegments = - metadata.finalResponse?.resultSegments; + baseServiceMethod.response.resultSegments = metadata.finalResponse?.resultSegments; return diagnostics.wrap({ ...baseServiceMethod, @@ -439,9 +394,7 @@ function getSdkLroServiceMethod( }); } -function getServiceMethodLroMetadata< - TServiceOperation extends SdkServiceOperation, ->( +function getServiceMethodLroMetadata( context: TCGCContext, operation: Operation, client: SdkClientType, @@ -462,23 +415,14 @@ function getServiceMethodLroMetadata< pollingStep: { responseBody: rawMetadata.pollingInfo.responseModel ? (diagnostics.pipe( - getClientTypeWithDiagnostics( - context, - rawMetadata.pollingInfo.responseModel, - ), + getClientTypeWithDiagnostics(context, rawMetadata.pollingInfo.responseModel), ) as SdkModelType) : undefined, }, - operation: diagnostics.pipe( - getSdkBasicServiceMethod(context, rawMetadata.operation, client), - ).operation, - logicalResult: diagnostics.pipe( - getSdkModelWithDiagnostics(context, rawMetadata.logicalResult), - ), - statusMonitorStep: getStatusMonitorStep( - context, - rawMetadata.statusMonitorStep, - ), + operation: diagnostics.pipe(getSdkBasicServiceMethod(context, rawMetadata.operation, client)) + .operation, + logicalResult: diagnostics.pipe(getSdkModelWithDiagnostics(context, rawMetadata.logicalResult)), + statusMonitorStep: getStatusMonitorStep(context, rawMetadata.statusMonitorStep), pollingInfo: getPollingInfo(context, rawMetadata.pollingInfo), envelopeResult: diagnostics.pipe( getSdkModelWithDiagnostics(context, rawMetadata.envelopeResult), @@ -515,13 +459,9 @@ function getServiceMethodLroMetadata< getClientTypeWithDiagnostics(context, step.responseModel), ) as SdkBuiltInType) : getSdkModel(context, step.responseModel), - target: diagnostics.pipe( - getSdkModelPropertyType(context, step.target), - ), + target: diagnostics.pipe(getSdkModelPropertyType(context, step.target)), sourceProperty: step.sourceProperty - ? diagnostics.pipe( - getSdkModelPropertyType(context, step.sourceProperty), - ) + ? diagnostics.pipe(getSdkModelPropertyType(context, step.sourceProperty)) : undefined, }; } @@ -553,30 +493,19 @@ function getServiceMethodLroMetadata< responseModel: diagnostics.pipe( getSdkModelWithDiagnostics(context, statusMonitorStep.responseModel), ), - target: getSdkOperationReference( - context, - statusMonitorStep.target, - client, - ), + target: getSdkOperationReference(context, statusMonitorStep.target, client), }; } - function getSdkOperationLink( - context: TCGCContext, - link: OperationLink, - ): SdkOperationLink { + function getSdkOperationLink(context: TCGCContext, link: OperationLink): SdkOperationLink { return { kind: "link", location: link.location, - property: diagnostics.pipe( - getSdkModelPropertyType(context, link.property), - ), + property: diagnostics.pipe(getSdkModelPropertyType(context, link.property)), }; } - function getSdkOperationReference< - TServiceOperation extends SdkServiceOperation, - >( + function getSdkOperationReference( context: TCGCContext, reference: OperationReference, client: SdkClientType, @@ -592,14 +521,11 @@ function getServiceMethodLroMetadata< return { kind: "reference", // since these operations may not be included in the client customization, we ignore diagnostics here - operation: ignoreDiagnostics( - getSdkBasicServiceMethod(context, reference.operation, client), - ).operation, + operation: ignoreDiagnostics(getSdkBasicServiceMethod(context, reference.operation, client)) + .operation, parameterMap: reference.parameterMap, parameters, - link: reference.link - ? getSdkOperationLink(context, reference.link) - : undefined, + link: reference.link ? getSdkOperationLink(context, reference.link) : undefined, }; } @@ -608,24 +534,17 @@ function getServiceMethodLroMetadata< pollingInfo: PollingOperationStep, ): SdkPollingOperationStep { const resultProperty = pollingInfo.resultProperty - ? diagnostics.pipe( - getSdkModelPropertyType(context, pollingInfo.resultProperty), - ) + ? diagnostics.pipe(getSdkModelPropertyType(context, pollingInfo.resultProperty)) : undefined; const errorProperty = pollingInfo.errorProperty - ? diagnostics.pipe( - getSdkModelPropertyType(context, pollingInfo.errorProperty), - ) + ? diagnostics.pipe(getSdkModelPropertyType(context, pollingInfo.errorProperty)) : undefined; return { kind: "pollingOperationStep", responseModel: diagnostics.pipe( getSdkModelWithDiagnostics(context, pollingInfo.responseModel), ), - terminationStatus: getTerminationStatus( - context, - pollingInfo.terminationStatus, - ), + terminationStatus: getTerminationStatus(context, pollingInfo.terminationStatus), resultProperty, errorProperty, }; @@ -641,9 +560,7 @@ function getServiceMethodLroMetadata< case "model-property": return { ...terminationStatus, - property: diagnostics.pipe( - getSdkModelPropertyType(context, terminationStatus.property), - ), + property: diagnostics.pipe(getSdkModelPropertyType(context, terminationStatus.property)), }; } } @@ -708,16 +625,12 @@ function getSdkMethodResponse( } }); - const responseTypes = new Set( - allResponseBodies.map((x) => getHashForType(x)), - ); + const responseTypes = new Set(allResponseBodies.map((x) => getHashForType(x))); let type: SdkType | undefined = undefined; if (responseOverride && isNeverOrVoidType(responseOverride)) { type = undefined; } else if (responseOverride) { - type = ignoreDiagnostics( - getClientTypeWithDiagnostics(context, responseOverride, operation), - ); + type = ignoreDiagnostics(getClientTypeWithDiagnostics(context, responseOverride, operation)); } else if (getResponseAsBool(context, operation)) { type = getSdkBuiltInType(context, $(context.program).builtin.boolean); } else { @@ -770,9 +683,7 @@ function getSdkMethodResponse( }; } -export function getSdkBasicServiceMethod< - TServiceOperation extends SdkServiceOperation, ->( +export function getSdkBasicServiceMethod( context: TCGCContext, operation: Operation, client: SdkClientType, @@ -800,9 +711,7 @@ export function getSdkBasicServiceMethod< if (isNeverOrVoidType(param.type)) continue; // Skip parameters that are not in scope for this emitter if (!isInScope(context, param)) continue; - const sdkMethodParam = diagnostics.pipe( - getSdkMethodParameter(context, param, operation), - ); + const sdkMethodParam = diagnostics.pipe(getSdkMethodParameter(context, param, operation)); if (sdkMethodParam.onClient) { // add API version and subscription ID parameters to the client parameters if (sdkMethodParam.isApiVersionParam) { @@ -820,19 +729,9 @@ export function getSdkBasicServiceMethod< } const serviceOperation = diagnostics.pipe( - getSdkServiceOperation( - context, - operation, - methodParameters, - client, - ), - ); - const response = getSdkMethodResponse( - context, - operation, - serviceOperation, - client, + getSdkServiceOperation(context, operation, methodParameters, client), ); + const response = getSdkMethodResponse(context, operation, serviceOperation, client); const name = getLibraryName(context, operation); return diagnostics.wrap({ __raw: operation, @@ -864,32 +763,15 @@ function getSdkServiceMethod( const pagingDisabled = getDisablePageable(context, operation); const paging = !pagingDisabled && - (isList(context.program, operation) || - getMarkAsPageable(context, operation)); + (isList(context.program, operation) || getMarkAsPageable(context, operation)); if (lro && paging) { - return getSdkLroPagingServiceMethod( - context, - operation, - client, - ); + return getSdkLroPagingServiceMethod(context, operation, client); } else if (paging) { - return getSdkPagingServiceMethod( - context, - operation, - client, - ); + return getSdkPagingServiceMethod(context, operation, client); } else if (lro) { - return getSdkLroServiceMethod( - context, - operation, - client, - ); + return getSdkLroServiceMethod(context, operation, client); } - return getSdkBasicServiceMethod( - context, - operation, - client, - ); + return getSdkBasicServiceMethod(context, operation, client); } export function getSdkMethodParameter( @@ -904,19 +786,12 @@ export function getSdkMethodParameter( if (!property) { // for parameter that has elevated to client or parent client, we will use the client parameter directly if (operation) { - const correspondingClientParam = getCorrespondingClientParam( - context, - type, - operation, - ); - if (correspondingClientParam) - return diagnostics.wrap(correspondingClientParam); + const correspondingClientParam = getCorrespondingClientParam(context, type, operation); + if (correspondingClientParam) return diagnostics.wrap(correspondingClientParam); } property = { - ...diagnostics.pipe( - getSdkModelPropertyTypeBase(context, type, operation), - ), + ...diagnostics.pipe(getSdkModelPropertyTypeBase(context, type, operation)), kind: "method", }; @@ -934,13 +809,7 @@ export function createSdkMethods( const retval: SdkMethod[] = []; for (const operation of listOperationsInClient(context, client)) { retval.push( - diagnostics.pipe( - getSdkServiceMethod( - context, - operation, - sdkClientType, - ), - ), + diagnostics.pipe(getSdkServiceMethod(context, operation, sdkClientType)), ); } return diagnostics.wrap(retval); diff --git a/packages/typespec-client-generator-core/src/tsp-index.ts b/packages/typespec-client-generator-core/src/tsp-index.ts index 7ac3b695ca..d96923a57c 100644 --- a/packages/typespec-client-generator-core/src/tsp-index.ts +++ b/packages/typespec-client-generator-core/src/tsp-index.ts @@ -38,8 +38,8 @@ import { exact, removeParameter, reorderParameters, - replaceResponse, replaceParameter, + replaceResponse, } from "./functions.js"; export { $lib } from "./lib.js"; @@ -88,16 +88,11 @@ export const $functions: Record = { // Note: The generated AzureClientGeneratorCoreFunctions type simplifies function signatures. // The actual implementation has the full parameter signature required by TypeSpec. "Azure.ClientGenerator.Core": { - replaceParameter: - replaceParameter as AzureClientGeneratorCoreFunctions["replaceParameter"], - removeParameter: - removeParameter as AzureClientGeneratorCoreFunctions["removeParameter"], - addParameter: - addParameter as AzureClientGeneratorCoreFunctions["addParameter"], - reorderParameters: - reorderParameters as AzureClientGeneratorCoreFunctions["reorderParameters"], - replaceResponse: - replaceResponse as AzureClientGeneratorCoreFunctions["replaceResponse"], + replaceParameter: replaceParameter as AzureClientGeneratorCoreFunctions["replaceParameter"], + removeParameter: removeParameter as AzureClientGeneratorCoreFunctions["removeParameter"], + addParameter: addParameter as AzureClientGeneratorCoreFunctions["addParameter"], + reorderParameters: reorderParameters as AzureClientGeneratorCoreFunctions["reorderParameters"], + replaceResponse: replaceResponse as AzureClientGeneratorCoreFunctions["replaceResponse"], exact: exact as AzureClientGeneratorCoreFunctions["exact"], }, }; diff --git a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts index 4cad4b4260..be832dde01 100644 --- a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts +++ b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts @@ -1,9 +1,6 @@ import { ok, strictEqual } from "assert"; import { it } from "vitest"; -import { - createSdkContextForTester, - SimpleTesterWithService, -} from "../tester.js"; +import { createSdkContextForTester, SimpleTesterWithService } from "../tester.js"; import { getServiceMethodOfClient } from "../utils.js"; it("replaces the generated method response without changing HTTP responses", async () => { From edab14e29c39beb6e29e1575be2180f17d784c04 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 6 Aug 2026 11:11:41 -0400 Subject: [PATCH 03/11] test(tcgc): cover response override scenarios Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- .../test/functions/replace-response.test.ts | 116 +++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts index be832dde01..74aa737926 100644 --- a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts +++ b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts @@ -1,6 +1,11 @@ import { ok, strictEqual } from "assert"; -import { it } from "vitest"; -import { createSdkContextForTester, SimpleTesterWithService } from "../tester.js"; +import { describe, it } from "vitest"; +import { + createClientCustomizationInput, + createSdkContextForTester, + SimpleBaseTester, + SimpleTesterWithService, +} from "../tester.js"; import { getServiceMethodOfClient } from "../utils.js"; it("replaces the generated method response without changing HTTP responses", async () => { @@ -57,3 +62,110 @@ it("uses a different response type supplied by @override", async () => { ok(method.operation.responses[0].type); strictEqual(method.operation.responses[0].type.name, "Widget"); }); + +it("replaces a response with bytes", async () => { + const { program } = await SimpleTesterWithService.compile(` + model Metadata { + name: string; + } + + @get op download(): Metadata; + + #suppress "experimental-feature" "testing replaceResponse" + @@override(TestService.download, replaceResponse(TestService.download, bytes)); + `); + + const context = await createSdkContextForTester(program); + const method = getServiceMethodOfClient(context.sdkPackage); + + ok(method.response.type); + strictEqual(method.response.type.kind, "bytes"); + strictEqual(method.response.type.encode, "base64"); + strictEqual(method.operation.responses[0].type?.kind, "model"); +}); + +it("composes response replacement with other operation transformations", async () => { + const { program } = await SimpleBaseTester.compile( + createClientCustomizationInput( + ` + @service + namespace TestService; + + model Widget { + name: string; + } + + op create(@query name?: string): Widget; + `, + ` + model CreateResult { + id: string; + } + + #suppress "experimental-feature" "testing replaceParameter" + alias WithRequiredName = replaceParameter(TestService.create, "name", CreateResult.id); + #suppress "experimental-feature" "testing replaceResponse" + @@override(TestService.create, replaceResponse(WithRequiredName, CreateResult)); + `, + ), + ); + + const context = await createSdkContextForTester(program); + const method = getServiceMethodOfClient(context.sdkPackage); + + strictEqual(method.parameters[0].name, "id"); + strictEqual(method.parameters[0].optional, false); + ok(method.response.type); + strictEqual(method.response.type.name, "CreateResult"); + strictEqual(method.operation.responses[0].type?.name, "Widget"); +}); + +describe("scoped response replacement", () => { + const mainCode = ` + @service + namespace TestService; + + model Widget { + name: string; + } + + model CreateResult { + id: string; + } + + op create(): Widget; + `; + + const customizationCode = ` + model CreateResult { + id: string; + } + + #suppress "experimental-feature" "testing replaceResponse" + @@override(TestService.create, replaceResponse(TestService.create, CreateResult), "python"); + `; + + it("applies the response replacement in the selected scope", async () => { + const { program } = await SimpleBaseTester.compile( + createClientCustomizationInput(mainCode, customizationCode), + ); + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-python", + }); + const method = getServiceMethodOfClient(context.sdkPackage); + + strictEqual(method.response.type?.name, "CreateResult"); + }); + + it("does not apply the response replacement outside the selected scope", async () => { + const { program } = await SimpleBaseTester.compile( + createClientCustomizationInput(mainCode, customizationCode), + ); + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-csharp", + }); + const method = getServiceMethodOfClient(context.sdkPackage); + + strictEqual(method.response.type?.name, "Widget"); + }); +}); From df69c95e9451b94d93c162eea0d37d436e92891b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 6 Aug 2026 15:47:18 -0400 Subject: [PATCH 04/11] fix(tcgc): honor pageable overrides Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- .../src/methods.ts | 7 ++--- .../test/functions/replace-response.test.ts | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/typespec-client-generator-core/src/methods.ts b/packages/typespec-client-generator-core/src/methods.ts index 97012dbe00..10704b8021 100644 --- a/packages/typespec-client-generator-core/src/methods.ts +++ b/packages/typespec-client-generator-core/src/methods.ts @@ -758,12 +758,13 @@ function getSdkServiceMethod( operation: Operation, client: SdkClientType, ): [SdkServiceMethod, readonly Diagnostic[]] { - const lro = getTcgcLroMetadata(context, operation, client); + const clientOperation = getOverriddenClientMethod(context, operation) ?? operation; + const lro = getTcgcLroMetadata(context, clientOperation, client); // `@disablePageable` disables paging even for operations with @list - const pagingDisabled = getDisablePageable(context, operation); + const pagingDisabled = getDisablePageable(context, clientOperation); const paging = !pagingDisabled && - (isList(context.program, operation) || getMarkAsPageable(context, operation)); + (isList(context.program, clientOperation) || getMarkAsPageable(context, clientOperation)); if (lro && paging) { return getSdkLroPagingServiceMethod(context, operation, client); } else if (paging) { diff --git a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts index 74aa737926..ae3e8ca33e 100644 --- a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts +++ b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts @@ -84,6 +84,33 @@ it("replaces a response with bytes", async () => { strictEqual(method.operation.responses[0].type?.kind, "model"); }); +it("removes pageable behavior when overriding a list operation with bytes", async () => { + const { program } = await SimpleTesterWithService.compile(` + model BlobPage { + @pageItems + items: string[]; + } + + @get + @list + op listBlobs(): BlobPage; + + @route("/bytes") + op listBlobsAsBytes(): bytes; + @@override(TestService.listBlobs, TestService.listBlobsAsBytes, "rust"); + `); + + const context = await createSdkContextForTester(program, { + emitterName: "@azure-tools/typespec-rust", + }); + const method = getServiceMethodOfClient(context.sdkPackage); + + strictEqual(method.kind, "basic"); + ok(method.response.type); + strictEqual(method.response.type.kind, "bytes"); + strictEqual(method.operation.responses[0].type?.kind, "model"); +}); + it("composes response replacement with other operation transformations", async () => { const { program } = await SimpleBaseTester.compile( createClientCustomizationInput( From f7cbf0b361b44980b18dd003ba06d3ff79803b3c Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 7 Aug 2026 12:38:42 -0400 Subject: [PATCH 05/11] feat(tcgc): cover anonymous bytes response overrides Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- ...ace-client-response-2026-08-06-10-49-09.md | 2 +- .../test/functions/replace-response.test.ts | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.chronus/changes/replace-client-response-2026-08-06-10-49-09.md b/.chronus/changes/replace-client-response-2026-08-06-10-49-09.md index a86bdb6ef1..77d2456dfa 100644 --- a/.chronus/changes/replace-client-response-2026-08-06-10-49-09.md +++ b/.chronus/changes/replace-client-response-2026-08-06-10-49-09.md @@ -1,5 +1,5 @@ --- -changeKind: fix +changeKind: feature packages: - "@azure-tools/typespec-client-generator-core" --- diff --git a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts index ae3e8ca33e..c28d232d59 100644 --- a/packages/typespec-client-generator-core/test/functions/replace-response.test.ts +++ b/packages/typespec-client-generator-core/test/functions/replace-response.test.ts @@ -84,6 +84,31 @@ it("replaces a response with bytes", async () => { strictEqual(method.operation.responses[0].type?.kind, "model"); }); +it("replaces a response with an anonymous bytes body", async () => { + const { program } = await SimpleTesterWithService.compile(` + model Metadata { + name: string; + } + + @get op download(): Metadata; + + alias BytesResponse = { + @body body: bytes; + }; + + #suppress "experimental-feature" "testing replaceResponse" + @@override(TestService.download, replaceResponse(TestService.download, BytesResponse)); + `); + + const context = await createSdkContextForTester(program); + const method = getServiceMethodOfClient(context.sdkPackage); + + ok(method.response.type); + strictEqual(method.response.type.kind, "model"); + strictEqual(method.response.type.properties[0].type.kind, "bytes"); + strictEqual(method.operation.responses[0].type?.kind, "model"); +}); + it("removes pageable behavior when overriding a list operation with bytes", async () => { const { program } = await SimpleTesterWithService.compile(` model BlobPage { From e626bddffc3269e365d1b15fd8c99c6fe95df655 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 7 Aug 2026 13:05:17 -0400 Subject: [PATCH 06/11] fix: preserve wire response for pageable overrides Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- .../src/methods.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/typespec-client-generator-core/src/methods.ts b/packages/typespec-client-generator-core/src/methods.ts index 10704b8021..728f12b7fb 100644 --- a/packages/typespec-client-generator-core/src/methods.ts +++ b/packages/typespec-client-generator-core/src/methods.ts @@ -167,7 +167,7 @@ function getSdkPagingServiceMethod(context, operation, client), + getSdkBasicServiceMethod(context, operation, client, false), ); // If the response body type itself is nullable (e.g., {@body body: Type | null}), unwrap it for paging/LRO processing @@ -610,10 +610,12 @@ function getSdkMethodResponse( operation: Operation, sdkOperation: SdkServiceOperation, client: SdkClientType, + useResponseOverride = true, ): SdkMethodResponse { const responses = sdkOperation.responses; - const overriddenOperation = getOverriddenClientMethod(context, operation); - const responseOverride = overriddenOperation?.returnType; + const responseOverride = useResponseOverride + ? getOverriddenClientMethod(context, operation)?.returnType + : undefined; const allResponseBodies: SdkType[] = []; let containsResponseWithoutBody = false; @@ -687,6 +689,7 @@ export function getSdkBasicServiceMethod, + useResponseOverride = true, ): [SdkServiceMethod, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); const methodParameters: SdkMethodParameter[] = []; @@ -731,7 +734,13 @@ export function getSdkBasicServiceMethod(context, operation, methodParameters, client), ); - const response = getSdkMethodResponse(context, operation, serviceOperation, client); + const response = getSdkMethodResponse( + context, + operation, + serviceOperation, + client, + useResponseOverride, + ); const name = getLibraryName(context, operation); return diagnostics.wrap({ __raw: operation, From 31234b16f88d77fba21ec94a1c424c525fb745c9 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 10 Aug 2026 15:02:55 -0400 Subject: [PATCH 07/11] test(java): suppress intentional duplicate body Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- packages/typespec-java/emitter-tests/tsp/method-override.tsp | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typespec-java/emitter-tests/tsp/method-override.tsp b/packages/typespec-java/emitter-tests/tsp/method-override.tsp index 2b86adb76f..7196ff2aa8 100644 --- a/packages/typespec-java/emitter-tests/tsp/method-override.tsp +++ b/packages/typespec-java/emitter-tests/tsp/method-override.tsp @@ -122,6 +122,7 @@ namespace Customization { options?: TspTest.MethodOverride.GroupPartETagOptions, ...ApiVersionParameter, ): void; + #suppress "@typespec/http/duplicate-body" "The body is intentionally preserved while regrouping query parameters." op groupExcludeBodyCustomization( ...TspTest.MethodOverride.GroupExcludeBody, options?: TspTest.MethodOverride.GroupQueryOptions, From b104e23361f95770c94d211ac91292b961852046 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 10 Aug 2026 15:15:32 -0400 Subject: [PATCH 08/11] fix(java): mark overridden query options as query Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- packages/typespec-java/emitter-tests/tsp/method-override.tsp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/typespec-java/emitter-tests/tsp/method-override.tsp b/packages/typespec-java/emitter-tests/tsp/method-override.tsp index 7196ff2aa8..8d912d6bbe 100644 --- a/packages/typespec-java/emitter-tests/tsp/method-override.tsp +++ b/packages/typespec-java/emitter-tests/tsp/method-override.tsp @@ -122,10 +122,9 @@ namespace Customization { options?: TspTest.MethodOverride.GroupPartETagOptions, ...ApiVersionParameter, ): void; - #suppress "@typespec/http/duplicate-body" "The body is intentionally preserved while regrouping query parameters." op groupExcludeBodyCustomization( ...TspTest.MethodOverride.GroupExcludeBody, - options?: TspTest.MethodOverride.GroupQueryOptions, + @query options?: TspTest.MethodOverride.GroupQueryOptions, ...ApiVersionParameter, ): void; op groupNoneCustomization( From 35b427dca73132da5e43d9428c3f2d3e51f389c5 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 10 Aug 2026 15:39:47 -0400 Subject: [PATCH 09/11] Revert "fix(java): mark overridden query options as query" This reverts commit b104e23361f95770c94d211ac91292b961852046. --- packages/typespec-java/emitter-tests/tsp/method-override.tsp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typespec-java/emitter-tests/tsp/method-override.tsp b/packages/typespec-java/emitter-tests/tsp/method-override.tsp index 8d912d6bbe..7196ff2aa8 100644 --- a/packages/typespec-java/emitter-tests/tsp/method-override.tsp +++ b/packages/typespec-java/emitter-tests/tsp/method-override.tsp @@ -122,9 +122,10 @@ namespace Customization { options?: TspTest.MethodOverride.GroupPartETagOptions, ...ApiVersionParameter, ): void; + #suppress "@typespec/http/duplicate-body" "The body is intentionally preserved while regrouping query parameters." op groupExcludeBodyCustomization( ...TspTest.MethodOverride.GroupExcludeBody, - @query options?: TspTest.MethodOverride.GroupQueryOptions, + options?: TspTest.MethodOverride.GroupQueryOptions, ...ApiVersionParameter, ): void; op groupNoneCustomization( From 6e462d710a7b4c265e3733203b144e5d157f689d Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 10 Aug 2026 15:39:47 -0400 Subject: [PATCH 10/11] Revert "test(java): suppress intentional duplicate body" This reverts commit 31234b16f88d77fba21ec94a1c424c525fb745c9. --- packages/typespec-java/emitter-tests/tsp/method-override.tsp | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/typespec-java/emitter-tests/tsp/method-override.tsp b/packages/typespec-java/emitter-tests/tsp/method-override.tsp index 7196ff2aa8..2b86adb76f 100644 --- a/packages/typespec-java/emitter-tests/tsp/method-override.tsp +++ b/packages/typespec-java/emitter-tests/tsp/method-override.tsp @@ -122,7 +122,6 @@ namespace Customization { options?: TspTest.MethodOverride.GroupPartETagOptions, ...ApiVersionParameter, ): void; - #suppress "@typespec/http/duplicate-body" "The body is intentionally preserved while regrouping query parameters." op groupExcludeBodyCustomization( ...TspTest.MethodOverride.GroupExcludeBody, options?: TspTest.MethodOverride.GroupQueryOptions, From 07be39c03f924680819a595e4f0c23f4abda03ad Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 20 Aug 2026 12:46:01 -0400 Subject: [PATCH 11/11] fix(java): mark overridden query options as query Re-applies the fix reverted in 35b427dca. Verified this duplicate-body failure is introduced by this PR's pageable/response-override classification changes (confirmed clean compile on main, failing on this branch), not a pre-existing Java fixture bug, so the fix belongs here. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8aca8827-bea2-4bf5-ae80-c87bebeedcbd --- packages/typespec-java/emitter-tests/tsp/method-override.tsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typespec-java/emitter-tests/tsp/method-override.tsp b/packages/typespec-java/emitter-tests/tsp/method-override.tsp index 2b86adb76f..8d912d6bbe 100644 --- a/packages/typespec-java/emitter-tests/tsp/method-override.tsp +++ b/packages/typespec-java/emitter-tests/tsp/method-override.tsp @@ -124,7 +124,7 @@ namespace Customization { ): void; op groupExcludeBodyCustomization( ...TspTest.MethodOverride.GroupExcludeBody, - options?: TspTest.MethodOverride.GroupQueryOptions, + @query options?: TspTest.MethodOverride.GroupQueryOptions, ...ApiVersionParameter, ): void; op groupNoneCustomization(