diff --git a/.chronus/changes/warn-unused-client-init-params-2026-01-13.md b/.chronus/changes/warn-unused-client-init-params-2026-01-13.md new file mode 100644 index 0000000000..9bd75dd05a --- /dev/null +++ b/.chronus/changes/warn-unused-client-init-params-2026-01-13.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Add warning diagnostic for unused client initialization parameters. If `clientInitialization.parameters` contains values that aren't used in any routes (client or its sub-clients), a diagnostic warning is now produced. diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index ba17212419..829909f465 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -482,6 +482,12 @@ export const $lib = createTypeSpecLibrary({ default: "All services must have the same server and auth definitions.", }, }, + "unused-client-initialization-parameter": { + severity: "warning", + messages: { + default: paramMessage`Client initialization parameter '${"parameterName"}' is not used in any operations of client '${"clientName"}' or its sub-clients.`, + }, + }, }, emitter: { options: TCGCEmitterOptionsSchema, diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index c733aacc25..5219280aeb 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -24,6 +24,7 @@ import { getActualClientType, getTypeDecorators, } from "./internal-utils.js"; +import { reportDiagnostic } from "./lib.js"; import { getLicenseInfo } from "./license.js"; import { getCrossLanguagePackageId, getNamespaceFromType } from "./public-utils.js"; import { getAllReferencedTypes, handleAllTypes } from "./types.js"; @@ -72,6 +73,7 @@ export function createSdkPackage( }, }; organizeNamespaces(context, sdkPackage); + validateClientInitializationParameters(context, sdkPackage); return diagnostics.wrap(sdkPackage); } @@ -165,3 +167,113 @@ function populateApiVersionInformation(context: TCGCContext): void { } } } + +/** + * Validates that all client initialization parameters are actually used in at least one operation + * of the client or its sub-clients. + */ +function validateClientInitializationParameters( + context: TCGCContext, + sdkPackage: SdkPackage, +): void { + // Process all top-level clients - don't recurse since each client already checks its sub-clients + for (const client of sdkPackage.clients) { + validateClientInitializationParametersForClient(context, client); + } +} + +/** + * Validates client initialization parameters for a single client (including its sub-clients' operations). + */ +function validateClientInitializationParametersForClient< + TServiceOperation extends SdkServiceOperation, +>(context: TCGCContext, client: SdkClientType): void { + // Only validate when there's a customized @clientInitialization decorator with parameters + // Skip validation for default/auto-generated client initialization + if (!client.clientInitialization.__raw) { + return; + } + + // Get custom parameters to validate (exclude built-in parameters like endpoint and credential) + const customParams = client.clientInitialization.parameters.filter( + (param) => param.kind !== "endpoint" && param.kind !== "credential", + ); + + // Skip expensive operation parameter collection if there are no custom parameters to validate + if (customParams.length === 0) { + return; + } + + // Collect all operation parameters from this client and all sub-clients + const allOperationParameterNames = new Set(); + collectOperationParameterNames(client, allOperationParameterNames); + + // Check each custom client initialization parameter + for (const param of customParams) { + // Check if this parameter is used in any operation + if (!allOperationParameterNames.has(param.name)) { + // Get the raw entity (Namespace or Interface) to report diagnostics on + // Use the raw type if available, otherwise use the raw model from clientInitialization + const target = + client.__raw.type || + client.clientInitialization.__raw || + context.program.getGlobalNamespaceType(); + reportDiagnostic(context.program, { + code: "unused-client-initialization-parameter", + target: target, + format: { + parameterName: param.name, + clientName: client.name, + }, + }); + } + } +} + +/** + * Collects all parameter names used in operations of a client and all its sub-clients. + */ +function collectOperationParameterNames( + client: SdkClientType, + parameterNames: Set, +): void { + // Collect parameters from all methods in this client + for (const method of client.methods) { + // Check operation parameters + if (method.operation && method.operation.kind === "http") { + for (const param of method.operation.parameters) { + // Check methodParameterSegments to find the client initialization parameter + if (param.methodParameterSegments) { + for (const path of param.methodParameterSegments) { + for (const methodParam of path) { + if (methodParam.kind === "method" && methodParam.onClient) { + parameterNames.add(methodParam.name); + } + } + } + } + } + + // Also check body parameter + if (method.operation.bodyParam) { + const bodyParam = method.operation.bodyParam; + if (bodyParam.methodParameterSegments) { + for (const path of bodyParam.methodParameterSegments) { + for (const methodParam of path) { + if (methodParam.kind === "method" && methodParam.onClient) { + parameterNames.add(methodParam.name); + } + } + } + } + } + } + } + + // Recursively collect from sub-clients + if (client.children) { + for (const child of client.children) { + collectOperationParameterNames(child, parameterNames); + } + } +} diff --git a/packages/typespec-client-generator-core/test/decorators/client-initialization.test.ts b/packages/typespec-client-generator-core/test/decorators/client-initialization.test.ts index 7dcb19964b..1814a3b294 100644 --- a/packages/typespec-client-generator-core/test/decorators/client-initialization.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/client-initialization.test.ts @@ -729,3 +729,215 @@ it("wrong initializedBy value type", async () => { code: "invalid-argument", }); }); + +it("should warn on unused client initialization parameter", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + unusedParam: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + @route("/test") + op testOp(@query query: string): void; + } + `); + await createSdkContextForTester(program); + + expectDiagnostics(program.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/unused-client-initialization-parameter", + }); +}); + +it("should not warn when client initialization parameter is used", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + blobName: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + @route("/test") + op testOp(@path blobName: string): void; + } + `); + const context = await createSdkContextForTester(program); + + expectDiagnostics(context.diagnostics, []); +}); + +it("should warn on multiple unused client initialization parameters", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + unusedParam1: string; + unusedParam2: string; + usedParam: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + @route("/test") + op testOp(@path usedParam: string): void; + } + `); + await createSdkContextForTester(program); + + expectDiagnostics(program.diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/unused-client-initialization-parameter", + }, + { + code: "@azure-tools/typespec-client-generator-core/unused-client-initialization-parameter", + }, + ]); +}); + +it("should not warn when parameter is used in subclient with @operationGroup", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + blobName: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + @operationGroup + interface SubClient { + @route("/blob") + op download(@path blobName: string): void; + } + + @route("/main") + op mainOp(@query query: string): void; + } + `); + const context = await createSdkContextForTester(program); + + expectDiagnostics(context.diagnostics, []); +}); + +it("should not warn when parameter is used in subclient with @client", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + blobName: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + @client({name: "SubClient"}) + namespace SubNamespace { + @route("/blob") + op download(@path blobName: string): void; + } + + @route("/main") + op mainOp(@query query: string): void; + } + `); + const context = await createSdkContextForTester(program); + + expectDiagnostics(context.diagnostics, []); +}); + +it("should warn when parameter is not used in subclient operations", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + unusedParam: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + @operationGroup + interface SubClient { + @route("/blob") + op download(@query query: string): void; + } + + @route("/main") + op mainOp(@query query: string): void; + } + `); + await createSdkContextForTester(program); + + expectDiagnostics(program.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/unused-client-initialization-parameter", + }); +}); + +it("should work with @clientLocation decorator", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + blobName: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + model BlobParams { + @path blobName: string; + } + + @route("/blob") + op download(...BlobParams): void; + + @@clientLocation(download, MyService); + } + `); + const context = await createSdkContextForTester(program); + + expectDiagnostics(context.diagnostics, []); +}); + +it("should not warn when client initialization parameter is used via @paramAlias", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + @paramAlias("blob") + blobName: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + @route("/download") + op download(@path blob: string): void; + } + `); + const context = await createSdkContextForTester(program); + + expectDiagnostics(context.diagnostics, []); +}); + +it("should warn when client initialization parameter with @paramAlias is not used", async () => { + const { program } = await SimpleTester.compile(` + @service + namespace MyService { + model ClientInitialization { + @paramAlias("blob") + blobName: string; + } + + @@clientInitialization(MyService, {parameters: ClientInitialization}); + + @route("/test") + op testOp(@query query: string): void; + } + `); + await createSdkContextForTester(program); + + expectDiagnostics(program.diagnostics, { + code: "@azure-tools/typespec-client-generator-core/unused-client-initialization-parameter", + }); +}); diff --git a/packages/typespec-client-generator-core/test/decorators/client-location.test.ts b/packages/typespec-client-generator-core/test/decorators/client-location.test.ts index 0f6bd68fd5..0eb741e3d1 100644 --- a/packages/typespec-client-generator-core/test/decorators/client-location.test.ts +++ b/packages/typespec-client-generator-core/test/decorators/client-location.test.ts @@ -334,9 +334,9 @@ describe("Operation", () => { strictEqual(a2Method.parameters.length, 0); strictEqual(a2Method.operation.parameters.length, 1); strictEqual(a2Method.operation.parameters[0].name, "apiVersion"); - strictEqual(a2Method.operation.parameters[0].correspondingMethodParams.length, 1); + strictEqual(a2Method.operation.parameters[0].methodParameterSegments.length, 1); strictEqual( - a2Method.operation.parameters[0].correspondingMethodParams[0], + a2Method.operation.parameters[0].methodParameterSegments[0][0], bClientApiVersionParam, ); }); @@ -382,9 +382,9 @@ describe("Operation", () => { strictEqual(a2Method.parameters.length, 0); strictEqual(a2Method.operation.parameters.length, 1); strictEqual(a2Method.operation.parameters[0].name, "apiVersion"); - strictEqual(a2Method.operation.parameters[0].correspondingMethodParams.length, 1); + strictEqual(a2Method.operation.parameters[0].methodParameterSegments.length, 1); strictEqual( - a2Method.operation.parameters[0].correspondingMethodParams[0], + a2Method.operation.parameters[0].methodParameterSegments[0][0], bClientApiVersionParam, ); }); @@ -428,9 +428,9 @@ describe("Operation", () => { strictEqual(a2Method.parameters.length, 0); strictEqual(a2Method.operation.parameters.length, 1); strictEqual(a2Method.operation.parameters[0].name, "apiVersion"); - strictEqual(a2Method.operation.parameters[0].correspondingMethodParams.length, 1); + strictEqual(a2Method.operation.parameters[0].methodParameterSegments.length, 1); strictEqual( - a2Method.operation.parameters[0].correspondingMethodParams[0], + a2Method.operation.parameters[0].methodParameterSegments[0][0], rootClientApiVersionParam, ); }); @@ -524,8 +524,8 @@ describe("Parameter", () => { // But the HTTP operation should still reference the client parameter const httpApiKeyParam = aMethod.operation.parameters.find((p) => p.name === "apiKey"); ok(httpApiKeyParam); - strictEqual(httpApiKeyParam.correspondingMethodParams.length, 1); - strictEqual(httpApiKeyParam.correspondingMethodParams[0], clientApiKeyParam); + strictEqual(httpApiKeyParam.methodParameterSegments.length, 1); + strictEqual(httpApiKeyParam.methodParameterSegments[0][0], clientApiKeyParam); }); it("detect parameter name conflict when moving to client", async () => { @@ -628,8 +628,8 @@ describe("Parameter", () => { strictEqual(testOperation.parameters.length, 3); const subIdOperationParam = testOperation.parameters.find((p) => p.name === "subscriptionId"); ok(subIdOperationParam); - strictEqual(subIdOperationParam.correspondingMethodParams.length, 1); - strictEqual(subIdOperationParam.correspondingMethodParams[0], subIdMethodParam); + strictEqual(subIdOperationParam.methodParameterSegments.length, 1); + strictEqual(subIdOperationParam.methodParameterSegments[0][0], subIdMethodParam); ok(testOperation.parameters.some((p) => p.name === "contentType")); ok(testOperation.parameters.some((p) => p.name === "apiVersion")); }); @@ -696,8 +696,8 @@ describe("Parameter", () => { strictEqual(getOperation.parameters.length, 4); const subIdOperationParam = getOperation.parameters.find((p) => p.name === "subscriptionId"); ok(subIdOperationParam); - strictEqual(subIdOperationParam.correspondingMethodParams.length, 1); - strictEqual(subIdOperationParam.correspondingMethodParams[0], subIdMethodParam); + strictEqual(subIdOperationParam.methodParameterSegments.length, 1); + strictEqual(subIdOperationParam.methodParameterSegments[0][0], subIdMethodParam); const putMethod = client.methods.find((m) => m.name === "put"); ok(putMethod); @@ -713,8 +713,8 @@ describe("Parameter", () => { strictEqual(putOperation.parameters.length, 5); const putSubIdOperationParam = putOperation.parameters.find((p) => p.name === "subscriptionId"); ok(putSubIdOperationParam); - strictEqual(putSubIdOperationParam.correspondingMethodParams.length, 1); - strictEqual(putSubIdOperationParam.correspondingMethodParams[0], subIdMethodParam); + strictEqual(putSubIdOperationParam.methodParameterSegments.length, 1); + strictEqual(putSubIdOperationParam.methodParameterSegments[0][0], subIdMethodParam); const deleteMethod = client.methods.find((m) => m.name === "delete"); ok(deleteMethod); @@ -728,8 +728,8 @@ describe("Parameter", () => { (p) => p.name === "subscriptionId", ); ok(deleteSubIdOperationParam); - strictEqual(deleteSubIdOperationParam.correspondingMethodParams.length, 1); - strictEqual(deleteSubIdOperationParam.correspondingMethodParams[0], subIdClientParam); + strictEqual(deleteSubIdOperationParam.methodParameterSegments.length, 1); + strictEqual(deleteSubIdOperationParam.methodParameterSegments[0][0], subIdClientParam); }); it("move to `@clientInitialization` for grandparent client", async () => { @@ -915,7 +915,7 @@ describe("Parameter", () => { ok(subIdParam); const subIdMethodParam = method.parameters.find((p) => p.name === "subscriptionId"); ok(subIdMethodParam); - strictEqual(subIdParam.correspondingMethodParams.length, 1); - strictEqual(subIdParam.correspondingMethodParams[0], subIdMethodParam); + strictEqual(subIdParam.methodParameterSegments.length, 1); + strictEqual(subIdParam.methodParameterSegments[0][0], subIdMethodParam); }); });