Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
112 changes: 112 additions & 0 deletions packages/typespec-client-generator-core/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -72,6 +73,7 @@ export function createSdkPackage<TServiceOperation extends SdkServiceOperation>(
},
};
organizeNamespaces(context, sdkPackage);
validateClientInitializationParameters(context, sdkPackage);
return diagnostics.wrap(sdkPackage);
}

Expand Down Expand Up @@ -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<TServiceOperation extends SdkServiceOperation>(
context: TCGCContext,
sdkPackage: SdkPackage<TServiceOperation>,
): 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<TServiceOperation>): void {
// Only validate when there's a customized @clientInitialization decorator with parameters
// Skip validation for default/auto-generated client initialization
if (!client.clientInitialization.__raw) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I just found a problem for this skip. This will also skip the check for nested clients with client initialization customization.

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<string>();
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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use instance check directly since the methodParameterSegments stores the direct reference, which could resolve potential naming and alias issue.

// 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<TServiceOperation extends SdkServiceOperation>(
client: SdkClientType<TServiceOperation>,
parameterNames: Set<string>,
): 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not add name, but just add instance.

}
}
}
}
}

// 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});
});
Loading
Loading