From d7e40be20a01c897d88f95f51f547efdae2e601b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 14 Jan 2026 16:06:01 -0500 Subject: [PATCH 01/19] initial commit --- .../src/internal-utils.ts | 97 +++++ .../src/package.ts | 6 + .../src/validations/types.ts | 198 +++++++++- .../test/clients/structure.test.ts | 58 +-- .../test/validations/types.test.ts | 353 +++++++++++++++++- 5 files changed, 657 insertions(+), 55 deletions(-) diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 30cd583fa6..17ae20fd8f 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -1132,3 +1132,100 @@ export function isSameAuth(left: Authentication, right: Authentication): boolean } return true; } + +/** + * Validates that there are no duplicate type names across namespaces when the namespace flag + * is used (which flattens all namespaces to a single namespace). + * + * This should be called during package creation when the namespaceFlag is set. + * + * @param context The TCGC context + * @param diagnostics The diagnostic collector to add errors to + */ +export function validateCrossNamespaceNamesWithFlag( + context: TCGCContext, + diagnostics: ReturnType, +): void { + // Only relevant when namespaceFlag flattens namespaces + if (!context.namespaceFlag) return; + + // Track all type names globally: name -> types with that name + const allTypeNames = new Map(); + + // Collect all types across all USER-DEFINED namespaces only + const userNamespaces = listAllUserDefinedNamespaces(context); + for (const ns of userNamespaces) { + collectTypesFromNamespace(context.program, ns, allTypeNames); + } + + // Report duplicates + for (const [name, types] of allTypeNames) { + if (types.length > 1) { + for (const type of types) { + diagnostics.add( + createDiagnostic({ + code: "duplicate-client-name", + messageId: "nonDecorator", + format: { name, scope: "AllScopes" }, + target: type, + }), + ); + } + } + } +} + +/** + * Collect top-level types from a single namespace (non-recursive). + * Only collects user-defined types. + */ +function collectTypesFromNamespace( + program: Program, + namespace: Namespace, + allTypeNames: Map, +): void { + // Collect models + for (const model of namespace.models.values()) { + if (model.name && $(program).type.isUserDefined(model)) { + const existing = allTypeNames.get(model.name) ?? []; + existing.push(model); + allTypeNames.set(model.name, existing); + } + } + + // Collect enums + for (const enumType of namespace.enums.values()) { + if (enumType.name && $(program).type.isUserDefined(enumType)) { + const existing = allTypeNames.get(enumType.name) ?? []; + existing.push(enumType); + allTypeNames.set(enumType.name, existing); + } + } + + // Collect unions + for (const unionType of namespace.unions.values()) { + if (unionType.name && $(program).type.isUserDefined(unionType)) { + const existing = allTypeNames.get(unionType.name) ?? []; + existing.push(unionType); + allTypeNames.set(unionType.name, existing); + } + } + + // Collect interfaces + for (const iface of namespace.interfaces.values()) { + if (iface.name && $(program).type.isUserDefined(iface)) { + const existing = allTypeNames.get(iface.name) ?? []; + existing.push(iface); + allTypeNames.set(iface.name, existing); + } + } + + // Collect scalars + for (const scalar of namespace.scalars.values()) { + if (scalar.name && $(program).type.isUserDefined(scalar)) { + const existing = allTypeNames.get(scalar.name) ?? []; + existing.push(scalar); + allTypeNames.set(scalar.name, existing); + } + } +} diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index da6c710407..dd41aef494 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -18,6 +18,7 @@ import { filterApiVersionsWithDecorators, getActualClientType, getTypeDecorators, + validateCrossNamespaceNamesWithFlag, } from "./internal-utils.js"; import { getLicenseInfo } from "./license.js"; import { getCrossLanguagePackageId, getNamespaceFromType } from "./public-utils.js"; @@ -27,6 +28,11 @@ export function createSdkPackage( context: TCGCContext, ): [SdkPackage, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); + + // Validate cross-namespace names if namespace flag is set (flattens namespaces) + // Can't validate in $onValidate bc we don't have access to the namespace flag + validateCrossNamespaceNamesWithFlag(context, diagnostics); + populateApiVersionInformation(context); diagnostics.pipe(handleAllTypes(context)); const crossLanguagePackageId = diagnostics.pipe(getCrossLanguagePackageId(context)); diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 017e9666ae..21bd2cbc74 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -16,9 +16,10 @@ import { AugmentDecoratorStatementNode, DecoratorExpressionNode } from "@typespe import { unsafe_Realm } from "@typespec/compiler/experimental"; import { DuplicateTracker } from "@typespec/compiler/utils"; import { getClientNameOverride } from "../decorators.js"; -import { TCGCContext } from "../interfaces.js"; +import { SdkClient, TCGCContext } from "../interfaces.js"; import { AllScopes, + clientKey, clientLocationKey, clientNameKey, hasExplicitClientOrOperationGroup, @@ -30,11 +31,31 @@ export function validateTypes(context: TCGCContext) { validateClientNames(context); } +/** + * Get all service namespaces from multi-service clients. + * Uses listScopedDecoratorData directly instead of listClients to avoid + * the cache cleanup that removes empty multi-service clients. + * Returns empty array if no multi-service clients exist. + */ +function getMultiServiceNamespaces(context: TCGCContext): Namespace[] { + const namespaceSet = new Set(); + // Directly query @client decorator data to find multi-service clients + listScopedDecoratorData(context, clientKey).forEach((clientData: SdkClient) => { + if (clientData.services && clientData.services.length > 1) { + for (const ns of clientData.services) { + namespaceSet.add(ns); + } + } + }); + return [...namespaceSet]; +} + /** * Validate naming with `@clientName` and `@clientLocation` decorators. * * This function checks for duplicate client names for types considering the impact of `@clientName` for all possible scopes. * It also handles the movement of operations to new clients based on the `@clientLocation` decorators. + * For multi-service clients, it validates names across ALL service namespaces together. * * @param tcgcContext The context for the TypeSpec Client Generator. */ @@ -42,6 +63,17 @@ function validateClientNames(tcgcContext: TCGCContext) { const languageScopes = getDefinedLanguageScopes(tcgcContext.program); // If no `@client` or `@operationGroup` decorators are defined, we consider `@clientLocation` const needToConsiderClientLocation = !hasExplicitClientOrOperationGroup(tcgcContext); + + // Detect multi-service scenario + const multiServiceNamespaces = getMultiServiceNamespaces(tcgcContext); + const isMultiService = multiServiceNamespaces.length > 0; + + // Ensure we always run validation at least once (with AllScopes) for multi-service scenarios + // even if no @clientName/@clientLocation decorators are defined + if (languageScopes.size === 0 && isMultiService) { + languageScopes.add(AllScopes); + } + // Check all possible language scopes for (const scope of languageScopes) { // Gather all moved operations and their targets @@ -80,14 +112,21 @@ function validateClientNames(tcgcContext: TCGCContext) { } } - // Validate client names for the current scope - validateClientNamesPerNamespace( - tcgcContext, - scope, - moved, - movedTo, - tcgcContext.program.getGlobalNamespaceType(), - ); + if (isMultiService) { + // For multi-service: validate types across ALL service namespaces together + // because they will be generated into the same namespace during multi-service generation. + // Same-named types across different services will collide. + validateClientNamesAcrossNamespaces(tcgcContext, scope, moved, movedTo, multiServiceNamespaces); + } else { + // For single-service: existing per-namespace validation + validateClientNamesPerNamespace( + tcgcContext, + scope, + moved, + movedTo, + tcgcContext.program.getGlobalNamespaceType(), + ); + } // Validate client names for new client's operations [...newClients.values()].map((operations) => { @@ -191,6 +230,116 @@ function validateClientNamesPerNamespace( } } +/** + * Collect all types from a namespace and its nested namespaces recursively. + */ +function collectTypesFromNamespace( + namespace: Namespace, + models: Model[], + enums: Enum[], + unions: Union[], + scalars: Scalar[], +) { + models.push(...namespace.models.values()); + enums.push(...namespace.enums.values()); + unions.push(...namespace.unions.values()); + scalars.push(...namespace.scalars.values()); + + // Recursively collect from nested namespaces + for (const nestedNs of namespace.namespaces.values()) { + collectTypesFromNamespace(nestedNs, models, enums, unions, scalars); + } +} + +/** + * Validate client names across multiple service namespaces for multi-service clients. + * Types with the same name across different services will collide when generated + * into the same namespace. + */ +function validateClientNamesAcrossNamespaces( + tcgcContext: TCGCContext, + scope: string | typeof AllScopes, + moved: Set, + movedTo: Map, + serviceNamespaces: Namespace[], +) { + // Collect all types from all service namespaces + const allModels: Model[] = []; + const allEnums: Enum[] = []; + const allUnions: Union[] = []; + const allScalars: Scalar[] = []; + + for (const serviceNs of serviceNamespaces) { + collectTypesFromNamespace(serviceNs, allModels, allEnums, allUnions, allScalars); + } + + // Validate models, enums, and unions together across all services + validateClientNamesCore(tcgcContext, scope, [...allModels, ...allEnums, ...allUnions]); + + // Validate scalars across all services + validateClientNamesCore(tcgcContext, scope, allScalars); + + // Also validate within each service namespace for operations, interfaces, properties, etc. + // These are scoped to their containers and don't need cross-service validation + for (const serviceNs of serviceNamespaces) { + validateClientNamesPerNamespaceOperationsOnly(tcgcContext, scope, moved, movedTo, serviceNs); + } +} + +/** + * Validate only operations and their containers within a namespace. + * Used for multi-service validation where types are validated separately across all services. + */ +function validateClientNamesPerNamespaceOperationsOnly( + tcgcContext: TCGCContext, + scope: string | typeof AllScopes, + moved: Set, + movedTo: Map, + namespace: Namespace, +) { + // Check for duplicate client names for operations + validateClientNamesCore( + tcgcContext, + scope, + adjustOperations(namespace.operations.values(), moved, movedTo, namespace), + ); + + // Check for duplicate client names for operations in interfaces + for (const item of namespace.interfaces.values()) { + validateClientNamesCore( + tcgcContext, + scope, + adjustOperations(item.operations.values(), moved, movedTo, item), + ); + } + + // Check for duplicate client names for interfaces + validateClientNamesCore(tcgcContext, scope, namespace.interfaces.values()); + + // Check for duplicate client names for namespaces + validateClientNamesCore(tcgcContext, scope, namespace.namespaces.values()); + + // Check for duplicate client names for model properties (within each model) + for (const model of namespace.models.values()) { + validateClientNamesCore(tcgcContext, scope, model.properties.values()); + } + + // Check for duplicate client names for enum members (within each enum) + for (const item of namespace.enums.values()) { + validateClientNamesCore(tcgcContext, scope, item.members.values()); + } + + // Check for duplicate client names for union variants (within each union) + for (const item of namespace.unions.values()) { + validateClientNamesCore(tcgcContext, scope, item.variants.values()); + } + + // Recurse into nested namespaces + for (const item of namespace.namespaces.values()) { + validateClientNamesPerNamespaceOperationsOnly(tcgcContext, scope, moved, movedTo, item); + } +} + function validateClientNamesCore( tcgcContext: TCGCContext, scope: string | typeof AllScopes, @@ -212,6 +361,35 @@ function validateClientNamesCore( Type | [Type, DecoratorExpressionNode | AugmentDecoratorStatementNode] >(); + trackItemsInDuplicateTracker(tcgcContext, scope, items, duplicateTracker); + + reportDuplicateClientNames(tcgcContext.program, duplicateTracker, scope); +} + +/** + * Track items in the duplicate tracker. + * This is extracted so it can be reused for cross-namespace validation. + */ +function trackItemsInDuplicateTracker( + tcgcContext: TCGCContext, + scope: string | typeof AllScopes, + items: Iterable< + | Namespace + | Scalar + | Operation + | Interface + | Model + | Enum + | Union + | ModelProperty + | EnumMember + | UnionVariant + >, + duplicateTracker: DuplicateTracker< + string, + Type | [Type, DecoratorExpressionNode | AugmentDecoratorStatementNode] + >, +) { for (const item of items) { const clientName = getClientNameOverride(tcgcContext, item, scope); if (clientName !== undefined) { @@ -225,8 +403,6 @@ function validateClientNamesCore( } } } - - reportDuplicateClientNames(tcgcContext.program, duplicateTracker, scope); } function reportDuplicateClientNames( 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 4bd0450ab8..f25acee889 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -1497,12 +1497,10 @@ it("one client from multiple services with different useDependency versions", as strictEqual(biApiVersionParam.clientDefaultValue, "bv3"); }); -it("one client from multiple services with models shared across services", async () => { - const runnerWithVersion = await createSdkTestRunner({ - "api-version": "latest", - emitterName: "@azure-tools/typespec-python", - }); - await runnerWithVersion.compileWithCustomization( +it("error: duplicate model names across services in multi-service client", async () => { + // Models with the same name across different services will collide when generated + // into the same namespace during multi-service generation + const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( ` @service @versioned(VersionsA) @@ -1553,42 +1551,18 @@ it("one client from multiple services with models shared across services", async namespace CombineClient; `, ); - const sdkPackage = runnerWithVersion.context.sdkPackage; - strictEqual(sdkPackage.clients.length, 1); - const client = sdkPackage.clients[0]; - strictEqual(client.name, "CombineClient"); - - // Both SharedModel types should exist - one from each service - const models = sdkPackage.models; - strictEqual(models.length, 2); - - const sharedModelA = models.find((m) => m.namespace === "ServiceA"); - ok(sharedModelA); - strictEqual(sharedModelA.name, "SharedModel"); - strictEqual(sharedModelA.properties.length, 2); - const nameProperty = sharedModelA.properties.find((p) => p.name === "name"); - ok(nameProperty); - const descriptionProperty = sharedModelA.properties.find((p) => p.name === "description"); - ok(descriptionProperty); - - const sharedModelB = models.find((m) => m.namespace === "ServiceB"); - ok(sharedModelB); - strictEqual(sharedModelB.name, "SharedModel"); - strictEqual(sharedModelB.properties.length, 2); - const idProperty = sharedModelB.properties.find((p) => p.name === "id"); - ok(idProperty); - const valueProperty = sharedModelB.properties.find((p) => p.name === "value"); - ok(valueProperty); - - const aiClient = client.children!.find((c) => c.name === "AI"); - ok(aiClient); - strictEqual(aiClient.apiVersions.length, 2); - deepStrictEqual(aiClient.apiVersions, ["av1", "av2"]); - - const biClient = client.children!.find((c) => c.name === "BI"); - ok(biClient); - strictEqual(biClient.apiVersions.length, 2); - deepStrictEqual(biClient.apiVersions, ["bv1", "bv2"]); + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "SharedModel" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "SharedModel" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); }); it("error: multiple explicit clients with multiple services", async () => { diff --git a/packages/typespec-client-generator-core/test/validations/types.test.ts b/packages/typespec-client-generator-core/test/validations/types.test.ts index ff1ad53f13..650f5344a3 100644 --- a/packages/typespec-client-generator-core/test/validations/types.test.ts +++ b/packages/typespec-client-generator-core/test/validations/types.test.ts @@ -1,5 +1,5 @@ import { expectDiagnosticEmpty, expectDiagnostics } from "@typespec/compiler/testing"; -import { beforeEach, it } from "vitest"; +import { beforeEach, describe, it } from "vitest"; import { createSdkTestRunner, SdkTestRunner } from "../test-host.js"; let runner: SdkTestRunner; @@ -8,6 +8,273 @@ beforeEach(async () => { runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python" }); }); +describe("multi-service duplicate name validation", () => { + // In multi-service scenarios, models/enums/unions with the same name in different services + // ARE duplicates because they will be generated into the same namespace during multi-service generation. + + it("error for same model name across services in multi-service client", async () => { + // Same-named models in different services will collide when generated + const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { v1 } + model Foo { a: string; } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { v1 } + model Foo { b: string; } + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.VersionsA.v1, ServiceB.VersionsB.v1) + namespace CombineClient; + `, + ); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Foo" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Foo" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); + }); + + it("error for same enum name across services in multi-service client", async () => { + // Same-named enums in different services will collide when generated + const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { v1 } + enum Status { Active, Inactive } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { v1 } + enum Status { Pending, Complete } + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.VersionsA.v1, ServiceB.VersionsB.v1) + namespace CombineClient; + `, + ); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Status" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Status" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); + }); + + it("error for same union name across services in multi-service client", async () => { + // Same-named unions in different services will collide when generated + const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { v1 } + union MyUnion { string, int32 } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { v1 } + union MyUnion { boolean, float32 } + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.VersionsA.v1, ServiceB.VersionsB.v1) + namespace CombineClient; + `, + ); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "MyUnion" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "MyUnion" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); + }); + + it("no error for different names across services", async () => { + const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { v1 } + model FooA { a: string; } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { v1 } + model FooB { b: string; } + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.VersionsA.v1, ServiceB.VersionsB.v1) + namespace CombineClient; + `, + ); + + expectDiagnosticEmpty(diagnostics); + }); + + it("error for @clientName same name across services in multi-service client", async () => { + // @clientName causing same name across services will collide when generated + const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { v1 } + @clientName("SharedName") + model ModelA { a: string; } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { v1 } + @clientName("SharedName") + model ModelB { b: string; } + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.VersionsA.v1, ServiceB.VersionsB.v1) + namespace CombineClient; + `, + ); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: 'Client name: "SharedName" is duplicated in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: 'Client name: "SharedName" is duplicated in language scope: "AllScopes"', + }, + ]); + }); + + it("error for nested namespace type with same name in multi-service client", async () => { + // Nested namespaces in different services will also collide when generated + const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { v1 } + namespace Sub { + model Nested { a: string; } + } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { v1 } + namespace Sub { + model Nested { b: string; } + } + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.VersionsA.v1, ServiceB.VersionsB.v1) + namespace CombineClient; + `, + ); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Nested" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Nested" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); + }); + + it("no error for same model name in single-service (different namespaces)", async () => { + // In single-service mode, same names in different namespaces are OK + const diagnostics = await runner.diagnose( + ` + @service + namespace MyService { + namespace SubA { + model Foo { a: string; } + } + namespace SubB { + model Foo { b: string; } + } + } + `, + ); + + expectDiagnosticEmpty(diagnostics); + }); + + it("error for duplicate model name within same service namespace", async () => { + // Within the same service namespace, duplicate names ARE an error + const diagnostics = await runner.diagnose( + ` + @service + namespace MyService { + model Foo { a: string; } + @clientName("Foo") + model Bar { b: string; } + } + `, + ); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + }, + ]); + }); +}); + it("no duplicate operation with @clientLocation", async () => { const diagnostics = await runner.diagnose( ` @@ -192,7 +459,7 @@ it("duplicate operation error for other languages", async () => { ` @service namespace StorageService; - + interface StorageTasks { @route("/list") op list(): void; @@ -216,3 +483,85 @@ it("duplicate operation error for other languages", async () => { }, ]); }); + +describe("namespace flag duplicate name validation", () => { + it("cross-namespace collision with namespace flag", async () => { + const runnerWithNamespace = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-python", + namespace: "Flattened", + }); + await runnerWithNamespace.compile(` + @service + namespace MyService { + namespace SubA { + model Foo { a: string; } + } + namespace SubB { + model Foo { b: string; } + } + } + `); + + // When namespace flag is set, cross-namespace collisions should be reported + expectDiagnostics(runnerWithNamespace.context.diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Foo" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Foo" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); + }); + + it("no collision without namespace flag", async () => { + // Without namespace flag, same names in different namespaces are OK + await runner.compile(` + @service + namespace MyService { + namespace SubA { + model Foo { a: string; } + } + namespace SubB { + model Foo { b: string; } + } + } + `); + + expectDiagnosticEmpty(runner.context.diagnostics); + }); + + it("cross-namespace enum collision with namespace flag", async () => { + const runnerWithNamespace = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-python", + namespace: "Flattened", + }); + await runnerWithNamespace.compile(` + @service + namespace MyService { + namespace SubA { + enum Status { Active } + } + namespace SubB { + enum Status { Pending } + } + } + `); + + expectDiagnostics(runnerWithNamespace.context.diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Status" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "Status" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); + }); +}); From b4d0c13d84b328b650633a5ad757305a620b8160 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 14 Jan 2026 16:14:22 -0500 Subject: [PATCH 02/19] add fix with client name --- .../test/clients/structure.test.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) 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 b75d903945..0d1a6f1ce9 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -1565,6 +1565,86 @@ it("error: duplicate model names across services in multi-service client", async ]); }); +it("fix duplicate model names across services using @clientName in client.tsp", async () => { + // When models have the same name across services, use @clientName in client.tsp + // to rename one of them and avoid the naming conflict + const runnerWithVersion = await createSdkTestRunner({ + "api-version": "latest", + emitterName: "@azure-tools/typespec-python", + }); + await runnerWithVersion.compileWithCustomization( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { + av1, + av2, + } + + model SharedModel { + name: string; + @added(VersionsA.av2) + description?: string; + } + + interface AI { + @route("/aTest") + aTest(@body body: SharedModel, @query("api-version") apiVersion: VersionsA): void; + } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { + bv1, + bv2, + } + + model SharedModel { + id: int32; + @added(VersionsB.bv2) + value?: string; + } + + interface BI { + @route("/bTest") + bTest(@body body: SharedModel, @query("api-version") apiVersion: VersionsB): void; + } + }`, + ` + @client( + { + name: "CombineClient", + service: [ServiceA, ServiceB], + } + ) + @useDependency(ServiceA.VersionsA.av2, ServiceB.VersionsB.bv2) + namespace CombineClient; + + // Rename ServiceB's SharedModel to avoid naming conflict + @@clientName(ServiceB.SharedModel, "SharedModelB"); + `, + ); + + const sdkPackage = runnerWithVersion.context.sdkPackage; + strictEqual(sdkPackage.clients.length, 1); + const client = sdkPackage.clients[0]; + strictEqual(client.name, "CombineClient"); + + // Both models should exist with different names + const models = sdkPackage.models; + strictEqual(models.length, 2); + + const sharedModelA = models.find((m) => m.name === "SharedModel"); + ok(sharedModelA); + strictEqual(sharedModelA.namespace, "ServiceA"); + + const sharedModelB = models.find((m) => m.name === "SharedModelB"); + ok(sharedModelB); + strictEqual(sharedModelB.namespace, "ServiceB"); +}); + it("error: multiple explicit clients with multiple services", async () => { const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( ` From 319300ce288c80f6175d8b640e874e9e4cee1287 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 14 Jan 2026 16:16:30 -0500 Subject: [PATCH 03/19] format --- .../src/validations/types.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 21bd2cbc74..ab5dab7a06 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -116,7 +116,13 @@ function validateClientNames(tcgcContext: TCGCContext) { // For multi-service: validate types across ALL service namespaces together // because they will be generated into the same namespace during multi-service generation. // Same-named types across different services will collide. - validateClientNamesAcrossNamespaces(tcgcContext, scope, moved, movedTo, multiServiceNamespaces); + validateClientNamesAcrossNamespaces( + tcgcContext, + scope, + moved, + movedTo, + multiServiceNamespaces, + ); } else { // For single-service: existing per-namespace validation validateClientNamesPerNamespace( From 903aa2f1d255454a177d25f5e742e44818fd3d53 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 14 Jan 2026 16:17:21 -0500 Subject: [PATCH 04/19] add changeset --- .../changes/tcgc-duplicateModelNames-2026-0-14-16-17-15.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/tcgc-duplicateModelNames-2026-0-14-16-17-15.md diff --git a/.chronus/changes/tcgc-duplicateModelNames-2026-0-14-16-17-15.md b/.chronus/changes/tcgc-duplicateModelNames-2026-0-14-16-17-15.md new file mode 100644 index 0000000000..4e961b092f --- /dev/null +++ b/.chronus/changes/tcgc-duplicateModelNames-2026-0-14-16-17-15.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Correctly detect name collisions with `--namespace` flag and or multi service usage \ No newline at end of file From 73e3567ba309369616a9ad545bf77972672d2524 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 15 Jan 2026 13:42:23 -0500 Subject: [PATCH 05/19] make sure operations are caught --- .../src/validations/types.ts | 30 +++++++-- .../test/validations/types.test.ts | 67 ++++++++++++++++++- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index ab5dab7a06..7b23b33018 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -23,6 +23,7 @@ import { clientLocationKey, clientNameKey, hasExplicitClientOrOperationGroup, + listAllUserDefinedNamespaces, listScopedDecoratorData, } from "../internal-utils.js"; import { reportDiagnostic } from "../lib.js"; @@ -74,6 +75,14 @@ function validateClientNames(tcgcContext: TCGCContext) { languageScopes.add(AllScopes); } + // Build a map of namespace names to their types for resolving string targets + const namedNamespaces = new Map(); + for (const ns of listAllUserDefinedNamespaces(tcgcContext)) { + if (ns.name) { + namedNamespaces.set(ns.name, ns); + } + } + // Check all possible language scopes for (const scope of languageScopes) { // Gather all moved operations and their targets @@ -94,14 +103,25 @@ function validateClientNames(tcgcContext: TCGCContext) { if (type.kind === "Operation") { moved.add(type); if (typeof target === "string") { - // Move to new clients - if (!newClients.has(target)) { - newClients.set(target, [type]); + // Check if the string target matches an existing namespace + const existingNamespace = namedNamespaces.get(target); + if (existingNamespace) { + // Move to existing namespace referenced by name + if (!movedTo.has(existingNamespace)) { + movedTo.set(existingNamespace, [type]); + } else { + movedTo.get(existingNamespace)!.push(type); + } } else { - newClients.get(target)!.push(type); + // Move to new clients (string doesn't match any existing namespace) + if (!newClients.has(target)) { + newClients.set(target, [type]); + } else { + newClients.get(target)!.push(type); + } } } else { - // Move to existing clients + // Move to existing clients (target is already a Namespace or Interface) if (!movedTo.has(target)) { movedTo.set(target, [type]); } else { diff --git a/packages/typespec-client-generator-core/test/validations/types.test.ts b/packages/typespec-client-generator-core/test/validations/types.test.ts index 650f5344a3..2158f541ef 100644 --- a/packages/typespec-client-generator-core/test/validations/types.test.ts +++ b/packages/typespec-client-generator-core/test/validations/types.test.ts @@ -354,12 +354,77 @@ it("duplicate operation with @clientLocation to existed clients", async () => { ]); }); +it("duplicate operation with @clientLocation string to existing namespace", async () => { + // When using a string that matches an existing namespace name, + // the operation should be moved to that namespace for validation + const diagnostics = await runner.diagnose( + ` + @service + namespace Contoso.WidgetManager; + + interface A { + @clientLocation("Test") + @route("/a") + op a(): void; + + @route("/b") + op b(): void; + } + + namespace Test { + @route("/c") + @clientName("a") + op c(): void; + } + `, + ); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: 'Client name: "a" is duplicated in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "a" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); +}); + +it("no duplicate operation with @clientLocation string to existing namespace", async () => { + // When using a string that matches an existing namespace name, + // the operation should be moved to that namespace - no conflict if names are different + const diagnostics = await runner.diagnose( + ` + @service + namespace Contoso.WidgetManager; + + interface A { + @clientLocation("Test") + @route("/a") + op a(): void; + + @route("/b") + op b(): void; + } + + namespace Test { + @route("/c") + op c(): void; + } + `, + ); + + expectDiagnosticEmpty(diagnostics); +}); + it("duplicate operation with @clientLocation to existed clients with scope", async () => { const diagnostics = await runner.diagnose( ` @service namespace Contoso.WidgetManager; - + interface A { @clientLocation(B, "go") @route("/a") From 80ccbf361747d2eca1c0d58cf4ef0056f8511ee9 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 15 Jan 2026 13:56:22 -0500 Subject: [PATCH 06/19] don't check for scalars --- .../src/validations/types.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 7b23b33018..1945ae6127 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -229,9 +229,6 @@ function validateClientNamesPerNamespace( // Check for duplicate client names for interfaces validateClientNamesCore(tcgcContext, scope, namespace.interfaces.values()); - // Check for duplicate client names for scalars - validateClientNamesCore(tcgcContext, scope, namespace.scalars.values()); - // Check for duplicate client names for namespaces validateClientNamesCore(tcgcContext, scope, namespace.namespaces.values()); @@ -264,16 +261,14 @@ function collectTypesFromNamespace( models: Model[], enums: Enum[], unions: Union[], - scalars: Scalar[], ) { models.push(...namespace.models.values()); enums.push(...namespace.enums.values()); unions.push(...namespace.unions.values()); - scalars.push(...namespace.scalars.values()); // Recursively collect from nested namespaces for (const nestedNs of namespace.namespaces.values()) { - collectTypesFromNamespace(nestedNs, models, enums, unions, scalars); + collectTypesFromNamespace(nestedNs, models, enums, unions); } } @@ -293,18 +288,14 @@ function validateClientNamesAcrossNamespaces( const allModels: Model[] = []; const allEnums: Enum[] = []; const allUnions: Union[] = []; - const allScalars: Scalar[] = []; for (const serviceNs of serviceNamespaces) { - collectTypesFromNamespace(serviceNs, allModels, allEnums, allUnions, allScalars); + collectTypesFromNamespace(serviceNs, allModels, allEnums, allUnions); } // Validate models, enums, and unions together across all services validateClientNamesCore(tcgcContext, scope, [...allModels, ...allEnums, ...allUnions]); - // Validate scalars across all services - validateClientNamesCore(tcgcContext, scope, allScalars); - // Also validate within each service namespace for operations, interfaces, properties, etc. // These are scoped to their containers and don't need cross-service validation for (const serviceNs of serviceNamespaces) { From f06aa7862fc66ec50e16b0f94aa9c712af499682 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 15 Jan 2026 14:08:28 -0500 Subject: [PATCH 07/19] add todo --- .../typespec-client-generator-core/src/validations/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 1945ae6127..4286980ce5 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -255,6 +255,11 @@ function validateClientNamesPerNamespace( /** * Collect all types from a namespace and its nested namespaces recursively. + * + * TODO: This currently collects ALL types in the namespace, including types that may not + * be used by the client. Ideally we should only validate types that are actually referenced + * by the client's operations. This would require running after SDK type resolution or + * implementing usage tracking at the TypeSpec level. */ function collectTypesFromNamespace( namespace: Namespace, From fa956282d2e018d9a0e6d1fca85dd7ed6518156b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 15 Jan 2026 14:10:29 -0500 Subject: [PATCH 08/19] don't extract function --- .../src/validations/types.ts | 31 ++----------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 4286980ce5..2547005d88 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -383,35 +383,6 @@ function validateClientNamesCore( Type | [Type, DecoratorExpressionNode | AugmentDecoratorStatementNode] >(); - trackItemsInDuplicateTracker(tcgcContext, scope, items, duplicateTracker); - - reportDuplicateClientNames(tcgcContext.program, duplicateTracker, scope); -} - -/** - * Track items in the duplicate tracker. - * This is extracted so it can be reused for cross-namespace validation. - */ -function trackItemsInDuplicateTracker( - tcgcContext: TCGCContext, - scope: string | typeof AllScopes, - items: Iterable< - | Namespace - | Scalar - | Operation - | Interface - | Model - | Enum - | Union - | ModelProperty - | EnumMember - | UnionVariant - >, - duplicateTracker: DuplicateTracker< - string, - Type | [Type, DecoratorExpressionNode | AugmentDecoratorStatementNode] - >, -) { for (const item of items) { const clientName = getClientNameOverride(tcgcContext, item, scope); if (clientName !== undefined) { @@ -425,6 +396,8 @@ function trackItemsInDuplicateTracker( } } } + + reportDuplicateClientNames(tcgcContext.program, duplicateTracker, scope); } function reportDuplicateClientNames( From 3fcfecdfcfda14f0384591fb593638e839d79d12 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 15:29:25 -0500 Subject: [PATCH 09/19] temp --- .../src/validations/types.ts | 58 +++++++++- .../test/validations/types.test.ts | 103 ++++++++++++++++++ 2 files changed, 155 insertions(+), 6 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 2547005d88..a3f125beb6 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -69,9 +69,14 @@ function validateClientNames(tcgcContext: TCGCContext) { const multiServiceNamespaces = getMultiServiceNamespaces(tcgcContext); const isMultiService = multiServiceNamespaces.length > 0; - // Ensure we always run validation at least once (with AllScopes) for multi-service scenarios - // even if no @clientName/@clientLocation decorators are defined - if (languageScopes.size === 0 && isMultiService) { + // Check if Azure.ResourceManager namespace exists (ARM library is being used) + const hasArmNamespace = + getAzureResourceManagerNamespace(tcgcContext.program) !== undefined; + + // Ensure we always run validation at least once (with AllScopes) for: + // - Multi-service scenarios (types across services may collide) + // - ARM scenarios (user types may conflict with ARM library types) + if (languageScopes.size === 0 && (isMultiService || hasArmNamespace)) { languageScopes.add(AllScopes); } @@ -203,12 +208,35 @@ function validateClientNamesPerNamespace( movedTo: Map, namespace: Namespace, ) { - // Check for duplicate client names for models, enums, and unions - validateClientNamesCore(tcgcContext, scope, [ + // Collect types from the namespace + const typesToValidate: (Model | Enum | Union)[] = [ ...namespace.models.values(), ...namespace.enums.values(), ...namespace.unions.values(), - ]); + ]; + + // Also collect types from Azure.ResourceManager namespace if it exists + // to detect naming conflicts with ARM library types + const armNamespace = getAzureResourceManagerNamespace(tcgcContext.program); + if (armNamespace) { + typesToValidate.push(...armNamespace.models.values()); + typesToValidate.push(...armNamespace.enums.values()); + typesToValidate.push(...armNamespace.unions.values()); + } + + // DEBUG + // eslint-disable-next-line no-console + console.log( + `DEBUG validateClientNamesPerNamespace: ns=${namespace.name}, types=${typesToValidate.length}, scope=${String(scope)}`, + ); + // eslint-disable-next-line no-console + console.log( + `DEBUG types:`, + typesToValidate.map((t) => t.name), + ); + + // Check for duplicate client names for models, enums, and unions + validateClientNamesCore(tcgcContext, scope, typesToValidate); // Check for duplicate client names for operations validateClientNamesCore( @@ -277,6 +305,18 @@ function collectTypesFromNamespace( } } +/** + * Get the Azure.ResourceManager namespace if it exists in the program. + * This is used to detect naming conflicts between user-defined types + * and ARM library types. + */ +function getAzureResourceManagerNamespace(program: Program): Namespace | undefined { + const globalNs = program.getGlobalNamespaceType(); + const azureNs = globalNs.namespaces.get("Azure"); + if (!azureNs) return undefined; + return azureNs.namespaces.get("ResourceManager"); +} + /** * Validate client names across multiple service namespaces for multi-service clients. * Types with the same name across different services will collide when generated @@ -385,6 +425,12 @@ function validateClientNamesCore( for (const item of items) { const clientName = getClientNameOverride(tcgcContext, item, scope); + // eslint-disable-next-line no-console + if (item.name === "ExtensionResource" || item.name === "MyExtensionResource") { + console.log( + `DEBUG validateClientNamesCore: name=${item.name}, clientName=${clientName}, scope=${String(scope)}`, + ); + } if (clientName !== undefined) { const clientNameDecorator = item.decorators.find((x) => x.definition?.name === "@clientName"); if (clientNameDecorator?.node !== undefined) { diff --git a/packages/typespec-client-generator-core/test/validations/types.test.ts b/packages/typespec-client-generator-core/test/validations/types.test.ts index 2158f541ef..9771928282 100644 --- a/packages/typespec-client-generator-core/test/validations/types.test.ts +++ b/packages/typespec-client-generator-core/test/validations/types.test.ts @@ -1,4 +1,7 @@ +import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing"; +import { AzureResourceManagerTestLibrary } from "@azure-tools/typespec-azure-resource-manager/testing"; import { expectDiagnosticEmpty, expectDiagnostics } from "@typespec/compiler/testing"; +import { OpenAPITestLibrary } from "@typespec/openapi/testing"; import { beforeEach, describe, it } from "vitest"; import { createSdkTestRunner, SdkTestRunner } from "../test-host.js"; @@ -273,6 +276,106 @@ describe("multi-service duplicate name validation", () => { }, ]); }); + + it("error for user-defined type conflicting with Azure.ResourceManager type", async () => { + // User-defined types should not conflict with ARM library types like ExtensionResource. + // Both the user's ExtensionResource and ARM's ExtensionResource would be generated + // into the SDK, causing naming conflicts. + const runnerWithArm = await createSdkTestRunner({ + librariesToAdd: [AzureResourceManagerTestLibrary, AzureCoreTestLibrary, OpenAPITestLibrary], + autoUsings: ["Azure.ResourceManager", "Azure.Core"], + emitterName: "@azure-tools/typespec-java", + }); + + const diagnostics = await runnerWithArm.diagnose(` + @armProviderNamespace("My.Service") + @server("http://localhost:3000", "endpoint") + @service(#{title: "My.Service"}) + @versioned(Versions) + @armCommonTypesVersion(CommonTypes.Versions.v5) + namespace My.Service; + + /** Api versions */ + enum Versions { + /** 2024-04-01-preview api version */ + V2024_04_01_PREVIEW: "2024-04-01-preview", + } + + // User defines a model with the same name as ARM's ExtensionResource + // This will conflict with ARM's ExtensionResource in generated code + // Adding @clientName to force validation to run with a language scope + @clientName("ExtensionResource") + model MyExtensionResource { + name: string; + value: int32; + } + + model TestTrackedResource is TrackedResource { + ...ResourceNameParameter; + } + + model TestTrackedResourceProperties { + description?: string; + // Reference the user's ExtensionResource + extension?: MyExtensionResource; + } + + @armResourceOperations + interface Tests { + get is ArmResourceRead; + } + `); + + // Debug: Print all diagnostics + console.log("Diagnostics count:", diagnostics.length); + for (const d of diagnostics) { + console.log("Diagnostic:", d.code, d.message); + } + + // Check for Azure.ResourceManager namespace + const globalNs = runnerWithArm.program.getGlobalNamespaceType(); + const azureNs = globalNs.namespaces.get("Azure"); + console.log("Azure namespace exists:", !!azureNs); + if (azureNs) { + const armNs = azureNs.namespaces.get("ResourceManager"); + console.log("Azure.ResourceManager namespace exists:", !!armNs); + if (armNs) { + console.log("ARM models count:", armNs.models.size); + const armModelNames = [...armNs.models.keys()]; + console.log("Has ExtensionResource in ARM:", armModelNames.includes("ExtensionResource")); + } + } + + // Check for My.Service namespace and user's ExtensionResource + const myNs = globalNs.namespaces.get("My"); + console.log("My namespace exists:", !!myNs); + if (myNs) { + const serviceNs = myNs.namespaces.get("Service"); + console.log("My.Service namespace exists:", !!serviceNs); + if (serviceNs) { + console.log("Service models count:", serviceNs.models.size); + console.log("Service model names:", [...serviceNs.models.keys()]); + console.log( + "Has ExtensionResource in My.Service:", + serviceNs.models.has("ExtensionResource"), + ); + } + } + + // Should report duplicate because user's ExtensionResource conflicts with ARM's ExtensionResource + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "ExtensionResource" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: + 'Client name: "ExtensionResource" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + }, + ]); + }); }); it("no duplicate operation with @clientLocation", async () => { From 14cd0ffb05d71c282cd367d444b61dab52eb395e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 16:17:19 -0500 Subject: [PATCH 10/19] only validate in createSdkContext --- .../src/context.ts | 4 + .../src/validate.ts | 4 +- .../src/validations/types.ts | 114 +++++++++++++----- .../test/test-host.ts | 15 ++- .../test/validations/types.test.ts | 50 +------- 5 files changed, 105 insertions(+), 82 deletions(-) diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index 5b828610be..b856ede004 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -21,6 +21,7 @@ import { stringify } from "yaml"; import { prepareClientAndOperationCache } from "./cache.js"; import { defaultDecoratorsAllowList } from "./configs.js"; import { handleClientExamples } from "./example.js"; +import { validateTypes } from "./validations/types.js"; import { getKnownScalars, SdkArrayType, @@ -217,6 +218,9 @@ export async function createSdkContext< } sdkContext.diagnostics = sdkContext.diagnostics.concat(diagnostics.diagnostics); + // Validate type names for duplicates (done here to have access to emitter options/flags) + validateTypes(sdkContext); + if (options?.exportTCGCoutput) { await exportTCGCOutput(sdkContext); } diff --git a/packages/typespec-client-generator-core/src/validate.ts b/packages/typespec-client-generator-core/src/validate.ts index 038a7ee202..d62b07dd27 100644 --- a/packages/typespec-client-generator-core/src/validate.ts +++ b/packages/typespec-client-generator-core/src/validate.ts @@ -4,7 +4,6 @@ import { validateClients } from "./validations/clients.js"; import { validateHttp } from "./validations/http.js"; import { validateMethods } from "./validations/methods.js"; import { validatePackage } from "./validations/package.js"; -import { validateTypes } from "./validations/types.js"; export function $onValidate(program: Program) { const tcgcContext = createTCGCContext(program, "@azure-tools/typespec-client-generator-core", { @@ -15,5 +14,6 @@ export function $onValidate(program: Program) { validateClients(tcgcContext); validateMethods(tcgcContext); validateHttp(tcgcContext); - validateTypes(tcgcContext); + // Note: Type name validation (validateTypes) is now done in createSdkContext + // where we have access to emitter options/flags } diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index a3f125beb6..32cc349f0a 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -39,16 +39,20 @@ export function validateTypes(context: TCGCContext) { * Returns empty array if no multi-service clients exist. */ function getMultiServiceNamespaces(context: TCGCContext): Namespace[] { - const namespaceSet = new Set(); + // Use a map keyed by namespace name to deduplicate, as versioning can create + // different namespace instances with the same name + const namespaceMap = new Map(); // Directly query @client decorator data to find multi-service clients listScopedDecoratorData(context, clientKey).forEach((clientData: SdkClient) => { if (clientData.services && clientData.services.length > 1) { for (const ns of clientData.services) { - namespaceSet.add(ns); + if (ns.name && !namespaceMap.has(ns.name)) { + namespaceMap.set(ns.name, ns); + } } } }); - return [...namespaceSet]; + return [...namespaceMap.values()]; } /** @@ -215,29 +219,19 @@ function validateClientNamesPerNamespace( ...namespace.unions.values(), ]; - // Also collect types from Azure.ResourceManager namespace if it exists - // to detect naming conflicts with ARM library types + // Check for duplicate client names for models, enums, and unions + validateClientNamesCore(tcgcContext, scope, typesToValidate); + + // Check for duplicate client names for scalars + validateClientNamesCore(tcgcContext, scope, namespace.scalars.values()); + + // Check for ARM type conflicts separately + // Only check if user types with @clientName conflict with ARM library types const armNamespace = getAzureResourceManagerNamespace(tcgcContext.program); if (armNamespace) { - typesToValidate.push(...armNamespace.models.values()); - typesToValidate.push(...armNamespace.enums.values()); - typesToValidate.push(...armNamespace.unions.values()); + validateArmTypeConflicts(tcgcContext, scope, typesToValidate, armNamespace); } - // DEBUG - // eslint-disable-next-line no-console - console.log( - `DEBUG validateClientNamesPerNamespace: ns=${namespace.name}, types=${typesToValidate.length}, scope=${String(scope)}`, - ); - // eslint-disable-next-line no-console - console.log( - `DEBUG types:`, - typesToValidate.map((t) => t.name), - ); - - // Check for duplicate client names for models, enums, and unions - validateClientNamesCore(tcgcContext, scope, typesToValidate); - // Check for duplicate client names for operations validateClientNamesCore( tcgcContext, @@ -317,6 +311,76 @@ function getAzureResourceManagerNamespace(program: Program): Namespace | undefin return azureNs.namespaces.get("ResourceManager"); } +/** + * Collect all ARM type names from the Azure.ResourceManager namespace. + * This includes models, enums, and unions from all nested namespaces. + */ +function collectArmTypeNames(namespace: Namespace): Set { + const names = new Set(); + + for (const model of namespace.models.values()) { + if (model.name) { + names.add(model.name); + } + } + for (const enumType of namespace.enums.values()) { + if (enumType.name) { + names.add(enumType.name); + } + } + for (const union of namespace.unions.values()) { + if (union.name) { + names.add(union.name); + } + } + + // Recursively collect from nested namespaces + for (const nestedNs of namespace.namespaces.values()) { + const nestedNames = collectArmTypeNames(nestedNs); + for (const name of nestedNames) { + names.add(name); + } + } + + return names; +} + +/** + * Validate that user-defined types with @clientName don't conflict with ARM library types. + * This is a targeted check that only reports conflicts when: + * 1. A user type has an explicit @clientName decorator + * 2. The client name matches an ARM library type name + * + * This avoids false positives from ARM's own internal type duplicates. + */ +function validateArmTypeConflicts( + tcgcContext: TCGCContext, + scope: string | typeof AllScopes, + userTypes: (Model | Enum | Union)[], + armNamespace: Namespace, +) { + const armTypeNames = collectArmTypeNames(armNamespace); + + for (const userType of userTypes) { + // Only check types that have an explicit @clientName decorator + const clientName = getClientNameOverride(tcgcContext, userType, scope); + if (clientName !== undefined && armTypeNames.has(clientName)) { + // User type with @clientName conflicts with an ARM type + const clientNameDecorator = userType.decorators.find( + (x) => x.definition?.name === "@clientName", + ); + if (clientNameDecorator?.node !== undefined) { + const scopeStr = scope === AllScopes ? "AllScopes" : scope; + reportDiagnostic(tcgcContext.program, { + code: "duplicate-client-name", + format: { name: clientName, scope: scopeStr }, + target: clientNameDecorator.node, + }); + } + } + } +} + /** * Validate client names across multiple service namespaces for multi-service clients. * Types with the same name across different services will collide when generated @@ -425,12 +489,6 @@ function validateClientNamesCore( for (const item of items) { const clientName = getClientNameOverride(tcgcContext, item, scope); - // eslint-disable-next-line no-console - if (item.name === "ExtensionResource" || item.name === "MyExtensionResource") { - console.log( - `DEBUG validateClientNamesCore: name=${item.name}, clientName=${clientName}, scope=${String(scope)}`, - ); - } if (clientName !== undefined) { const clientNameDecorator = item.decorators.find((x) => x.definition?.name === "@clientName"); if (clientNameDecorator?.node !== undefined) { diff --git a/packages/typespec-client-generator-core/test/test-host.ts b/packages/typespec-client-generator-core/test/test-host.ts index 1dbafd264a..1ecb43e854 100644 --- a/packages/typespec-client-generator-core/test/test-host.ts +++ b/packages/typespec-client-generator-core/test/test-host.ts @@ -77,24 +77,26 @@ export async function createSdkTestRunner( // diagnose sdkTestRunner.diagnose = async (code, compileOptions?) => { - const result = await baseDiagnose(code, compileOptions); + await baseDiagnose(code, compileOptions); sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption, ); - return result; + // Return all diagnostics from the program (includes validation from createSdkContext) + return sdkTestRunner.program.diagnostics; }; // compile and diagnose sdkTestRunner.compileAndDiagnose = async (code, compileOptions?) => { - const result = await baseCompileAndDiagnose(code, compileOptions); + const [types] = await baseCompileAndDiagnose(code, compileOptions); sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption, ); - return result; + // Return all diagnostics from the program (includes validation from createSdkContext) + return [types, sdkTestRunner.program.diagnostics]; }; // compile with dummy service definition @@ -227,13 +229,14 @@ export async function createSdkTestRunner( sdkTestRunner.compileAndDiagnoseWithCustomization = async (mainCode, clientCode) => { host.addTypeSpecFile("./main.tsp", `${mainAutoCode}${mainCode}`); host.addTypeSpecFile("./client.tsp", `${clientAutoCode}${clientCode}`); - const result = await host.compileAndDiagnose("./client.tsp"); + const [types] = await host.compileAndDiagnose("./client.tsp"); sdkTestRunner.context = await createSdkContextTestHelper( sdkTestRunner.program, options, sdkContextOption, ); - return result; + // Return all diagnostics from the program (includes validation from createSdkContext) + return [types, sdkTestRunner.program.diagnostics]; }; return sdkTestRunner; diff --git a/packages/typespec-client-generator-core/test/validations/types.test.ts b/packages/typespec-client-generator-core/test/validations/types.test.ts index 9771928282..78fc24ebb4 100644 --- a/packages/typespec-client-generator-core/test/validations/types.test.ts +++ b/packages/typespec-client-generator-core/test/validations/types.test.ts @@ -301,9 +301,7 @@ describe("multi-service duplicate name validation", () => { V2024_04_01_PREVIEW: "2024-04-01-preview", } - // User defines a model with the same name as ARM's ExtensionResource - // This will conflict with ARM's ExtensionResource in generated code - // Adding @clientName to force validation to run with a language scope + // User defines a model with @clientName that conflicts with ARM's ExtensionResource @clientName("ExtensionResource") model MyExtensionResource { name: string; @@ -326,53 +324,13 @@ describe("multi-service duplicate name validation", () => { } `); - // Debug: Print all diagnostics - console.log("Diagnostics count:", diagnostics.length); - for (const d of diagnostics) { - console.log("Diagnostic:", d.code, d.message); - } - - // Check for Azure.ResourceManager namespace - const globalNs = runnerWithArm.program.getGlobalNamespaceType(); - const azureNs = globalNs.namespaces.get("Azure"); - console.log("Azure namespace exists:", !!azureNs); - if (azureNs) { - const armNs = azureNs.namespaces.get("ResourceManager"); - console.log("Azure.ResourceManager namespace exists:", !!armNs); - if (armNs) { - console.log("ARM models count:", armNs.models.size); - const armModelNames = [...armNs.models.keys()]; - console.log("Has ExtensionResource in ARM:", armModelNames.includes("ExtensionResource")); - } - } - - // Check for My.Service namespace and user's ExtensionResource - const myNs = globalNs.namespaces.get("My"); - console.log("My namespace exists:", !!myNs); - if (myNs) { - const serviceNs = myNs.namespaces.get("Service"); - console.log("My.Service namespace exists:", !!serviceNs); - if (serviceNs) { - console.log("Service models count:", serviceNs.models.size); - console.log("Service model names:", [...serviceNs.models.keys()]); - console.log( - "Has ExtensionResource in My.Service:", - serviceNs.models.has("ExtensionResource"), - ); - } - } - - // Should report duplicate because user's ExtensionResource conflicts with ARM's ExtensionResource + // Should report a single diagnostic because user's @clientName("ExtensionResource") + // conflicts with ARM's ExtensionResource type expectDiagnostics(diagnostics, [ { code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", message: - 'Client name: "ExtensionResource" is defined somewhere causing naming conflicts in language scope: "AllScopes"', - }, - { - code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", - message: - 'Client name: "ExtensionResource" is defined somewhere causing naming conflicts in language scope: "AllScopes"', + 'Client name: "ExtensionResource" is duplicated in language scope: "AllScopes"', }, ]); }); From e454e3f9a995d30312cb31083cb2b897b6e4068a Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 16:35:59 -0500 Subject: [PATCH 11/19] additional fixes --- .../src/validations/types.ts | 42 ++++++++++--------- .../test/clients/structure.test.ts | 3 +- .../test/validations/types.test.ts | 13 +++--- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 32cc349f0a..4688f9359c 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -16,7 +16,7 @@ import { AugmentDecoratorStatementNode, DecoratorExpressionNode } from "@typespe import { unsafe_Realm } from "@typespec/compiler/experimental"; import { DuplicateTracker } from "@typespec/compiler/utils"; import { getClientNameOverride } from "../decorators.js"; -import { SdkClient, TCGCContext } from "../interfaces.js"; +import { SdkClient, SdkContext, TCGCContext } from "../interfaces.js"; import { AllScopes, clientKey, @@ -28,7 +28,7 @@ import { } from "../internal-utils.js"; import { reportDiagnostic } from "../lib.js"; -export function validateTypes(context: TCGCContext) { +export function validateTypes(context: SdkContext) { validateClientNames(context); } @@ -60,25 +60,27 @@ function getMultiServiceNamespaces(context: TCGCContext): Namespace[] { * * This function checks for duplicate client names for types considering the impact of `@clientName` for all possible scopes. * It also handles the movement of operations to new clients based on the `@clientLocation` decorators. - * For multi-service clients, it validates names across ALL service namespaces together. + * For multi-service clients, it validates names across ALL service namespaces together since combining + * multiple services into one client means types from all services will be in the same client. * - * @param tcgcContext The context for the TypeSpec Client Generator. + * @param sdkContext The SDK context (includes emitter options like namespaceFlag). */ -function validateClientNames(tcgcContext: TCGCContext) { - const languageScopes = getDefinedLanguageScopes(tcgcContext.program); +function validateClientNames(sdkContext: SdkContext) { + const languageScopes = getDefinedLanguageScopes(sdkContext.program); // If no `@client` or `@operationGroup` decorators are defined, we consider `@clientLocation` - const needToConsiderClientLocation = !hasExplicitClientOrOperationGroup(tcgcContext); + const needToConsiderClientLocation = !hasExplicitClientOrOperationGroup(sdkContext); - // Detect multi-service scenario - const multiServiceNamespaces = getMultiServiceNamespaces(tcgcContext); + // Detect multi-service scenario. + // Multi-service (@client with multiple services) inherently combines types from all services + // into a single client, so cross-namespace validation is needed. + const multiServiceNamespaces = getMultiServiceNamespaces(sdkContext); const isMultiService = multiServiceNamespaces.length > 0; // Check if Azure.ResourceManager namespace exists (ARM library is being used) - const hasArmNamespace = - getAzureResourceManagerNamespace(tcgcContext.program) !== undefined; + const hasArmNamespace = getAzureResourceManagerNamespace(sdkContext.program) !== undefined; // Ensure we always run validation at least once (with AllScopes) for: - // - Multi-service scenarios (types across services may collide) + // - Multi-service scenarios (types across services may collide in the combined client) // - ARM scenarios (user types may conflict with ARM library types) if (languageScopes.size === 0 && (isMultiService || hasArmNamespace)) { languageScopes.add(AllScopes); @@ -86,7 +88,7 @@ function validateClientNames(tcgcContext: TCGCContext) { // Build a map of namespace names to their types for resolving string targets const namedNamespaces = new Map(); - for (const ns of listAllUserDefinedNamespaces(tcgcContext)) { + for (const ns of listAllUserDefinedNamespaces(sdkContext)) { if (ns.name) { namedNamespaces.set(ns.name, ns); } @@ -101,7 +103,7 @@ function validateClientNames(tcgcContext: TCGCContext) { if (needToConsiderClientLocation) { // Cache all `@clientName` overrides for the current scope for (const [type, target] of listScopedDecoratorData( - tcgcContext, + sdkContext, clientLocationKey, scope, ).entries()) { @@ -143,29 +145,29 @@ function validateClientNames(tcgcContext: TCGCContext) { if (isMultiService) { // For multi-service: validate types across ALL service namespaces together - // because they will be generated into the same namespace during multi-service generation. + // because they will be in the same client. // Same-named types across different services will collide. validateClientNamesAcrossNamespaces( - tcgcContext, + sdkContext, scope, moved, movedTo, multiServiceNamespaces, ); } else { - // For single-service: existing per-namespace validation + // For single-service: per-namespace validation validateClientNamesPerNamespace( - tcgcContext, + sdkContext, scope, moved, movedTo, - tcgcContext.program.getGlobalNamespaceType(), + sdkContext.program.getGlobalNamespaceType(), ); } // Validate client names for new client's operations [...newClients.values()].map((operations) => { - validateClientNamesCore(tcgcContext, scope, operations); + validateClientNamesCore(sdkContext, scope, operations); }); } } 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 0d1a6f1ce9..cdbfebb4e1 100644 --- a/packages/typespec-client-generator-core/test/clients/structure.test.ts +++ b/packages/typespec-client-generator-core/test/clients/structure.test.ts @@ -1498,8 +1498,7 @@ it("one client from multiple services with different useDependency versions", as }); it("error: duplicate model names across services in multi-service client", async () => { - // Models with the same name across different services will collide when generated - // into the same namespace during multi-service generation + // Models with the same name across different services will collide when combined into one client const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( ` @service diff --git a/packages/typespec-client-generator-core/test/validations/types.test.ts b/packages/typespec-client-generator-core/test/validations/types.test.ts index 78fc24ebb4..35ce53f80c 100644 --- a/packages/typespec-client-generator-core/test/validations/types.test.ts +++ b/packages/typespec-client-generator-core/test/validations/types.test.ts @@ -13,10 +13,11 @@ beforeEach(async () => { describe("multi-service duplicate name validation", () => { // In multi-service scenarios, models/enums/unions with the same name in different services - // ARE duplicates because they will be generated into the same namespace during multi-service generation. + // ARE duplicates because combining multiple services into one client means all types + // will be in the same client. it("error for same model name across services in multi-service client", async () => { - // Same-named models in different services will collide when generated + // Same-named models in different services will collide when combined into one client const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( ` @service @@ -54,7 +55,7 @@ describe("multi-service duplicate name validation", () => { }); it("error for same enum name across services in multi-service client", async () => { - // Same-named enums in different services will collide when generated + // Same-named enums in different services will collide when combined into one client const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( ` @service @@ -92,7 +93,7 @@ describe("multi-service duplicate name validation", () => { }); it("error for same union name across services in multi-service client", async () => { - // Same-named unions in different services will collide when generated + // Same-named unions in different services will collide when combined into one client const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( ` @service @@ -156,7 +157,7 @@ describe("multi-service duplicate name validation", () => { }); it("error for @clientName same name across services in multi-service client", async () => { - // @clientName causing same name across services will collide when generated + // @clientName causing same name across services will collide when combined into one client const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( ` @service @@ -194,7 +195,7 @@ describe("multi-service duplicate name validation", () => { }); it("error for nested namespace type with same name in multi-service client", async () => { - // Nested namespaces in different services will also collide when generated + // Nested namespaces in different services will also collide when combined into one client const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( ` @service From 4c0e2b75962ebeabd30c6f34a172704d9ac90bb7 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 16:38:57 -0500 Subject: [PATCH 12/19] check for conflicts with azure namespace --- .../src/validations/types.ts | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 4688f9359c..cf279e2643 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -76,13 +76,14 @@ function validateClientNames(sdkContext: SdkContext) { const multiServiceNamespaces = getMultiServiceNamespaces(sdkContext); const isMultiService = multiServiceNamespaces.length > 0; - // Check if Azure.ResourceManager namespace exists (ARM library is being used) - const hasArmNamespace = getAzureResourceManagerNamespace(sdkContext.program) !== undefined; + // Check if any Azure library namespace exists (Azure.Core or Azure.ResourceManager) + const azureLibraryNamespaces = getAzureLibraryNamespaces(sdkContext.program); + const hasAzureLibrary = azureLibraryNamespaces.length > 0; // Ensure we always run validation at least once (with AllScopes) for: // - Multi-service scenarios (types across services may collide in the combined client) - // - ARM scenarios (user types may conflict with ARM library types) - if (languageScopes.size === 0 && (isMultiService || hasArmNamespace)) { + // - Azure library scenarios (user types may conflict with Azure.Core or Azure.ResourceManager types) + if (languageScopes.size === 0 && (isMultiService || hasAzureLibrary)) { languageScopes.add(AllScopes); } @@ -227,11 +228,11 @@ function validateClientNamesPerNamespace( // Check for duplicate client names for scalars validateClientNamesCore(tcgcContext, scope, namespace.scalars.values()); - // Check for ARM type conflicts separately - // Only check if user types with @clientName conflict with ARM library types - const armNamespace = getAzureResourceManagerNamespace(tcgcContext.program); - if (armNamespace) { - validateArmTypeConflicts(tcgcContext, scope, typesToValidate, armNamespace); + // Check for Azure library type conflicts separately + // Only check if user types with @clientName conflict with Azure library types + const azureNamespaces = getAzureLibraryNamespaces(tcgcContext.program); + if (azureNamespaces.length > 0) { + validateAzureLibraryTypeConflicts(tcgcContext, scope, typesToValidate, azureNamespaces); } // Check for duplicate client names for operations @@ -302,22 +303,34 @@ function collectTypesFromNamespace( } /** - * Get the Azure.ResourceManager namespace if it exists in the program. - * This is used to detect naming conflicts between user-defined types - * and ARM library types. + * Get Azure library namespaces (Azure.Core and Azure.ResourceManager) if they exist. + * These are used to detect naming conflicts between user-defined types + * and built-in Azure library types. */ -function getAzureResourceManagerNamespace(program: Program): Namespace | undefined { +function getAzureLibraryNamespaces(program: Program): Namespace[] { + const namespaces: Namespace[] = []; const globalNs = program.getGlobalNamespaceType(); const azureNs = globalNs.namespaces.get("Azure"); - if (!azureNs) return undefined; - return azureNs.namespaces.get("ResourceManager"); + if (!azureNs) return namespaces; + + const coreNs = azureNs.namespaces.get("Core"); + if (coreNs) { + namespaces.push(coreNs); + } + + const armNs = azureNs.namespaces.get("ResourceManager"); + if (armNs) { + namespaces.push(armNs); + } + + return namespaces; } /** - * Collect all ARM type names from the Azure.ResourceManager namespace. + * Collect all type names from a namespace recursively. * This includes models, enums, and unions from all nested namespaces. */ -function collectArmTypeNames(namespace: Namespace): Set { +function collectTypeNamesFromNamespace(namespace: Namespace): Set { const names = new Set(); for (const model of namespace.models.values()) { @@ -338,7 +351,7 @@ function collectArmTypeNames(namespace: Namespace): Set { // Recursively collect from nested namespaces for (const nestedNs of namespace.namespaces.values()) { - const nestedNames = collectArmTypeNames(nestedNs); + const nestedNames = collectTypeNamesFromNamespace(nestedNs); for (const name of nestedNames) { names.add(name); } @@ -348,26 +361,33 @@ function collectArmTypeNames(namespace: Namespace): Set { } /** - * Validate that user-defined types with @clientName don't conflict with ARM library types. + * Validate that user-defined types with @clientName don't conflict with Azure library types. * This is a targeted check that only reports conflicts when: * 1. A user type has an explicit @clientName decorator - * 2. The client name matches an ARM library type name + * 2. The client name matches an Azure library type name (from Azure.Core or Azure.ResourceManager) * - * This avoids false positives from ARM's own internal type duplicates. + * This avoids false positives from Azure's own internal type duplicates. */ -function validateArmTypeConflicts( +function validateAzureLibraryTypeConflicts( tcgcContext: TCGCContext, scope: string | typeof AllScopes, userTypes: (Model | Enum | Union)[], - armNamespace: Namespace, + azureNamespaces: Namespace[], ) { - const armTypeNames = collectArmTypeNames(armNamespace); + // Collect all type names from all Azure library namespaces + const azureTypeNames = new Set(); + for (const ns of azureNamespaces) { + const names = collectTypeNamesFromNamespace(ns); + for (const name of names) { + azureTypeNames.add(name); + } + } for (const userType of userTypes) { // Only check types that have an explicit @clientName decorator const clientName = getClientNameOverride(tcgcContext, userType, scope); - if (clientName !== undefined && armTypeNames.has(clientName)) { - // User type with @clientName conflicts with an ARM type + if (clientName !== undefined && azureTypeNames.has(clientName)) { + // User type with @clientName conflicts with an Azure library type const clientNameDecorator = userType.decorators.find( (x) => x.definition?.name === "@clientName", ); From 6e7ec745d5f2bf6e6b935c4f354630b0e401dde5 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 16:41:24 -0500 Subject: [PATCH 13/19] only validate if type is used --- .../src/validations/types.ts | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index cf279e2643..fdfbff3a53 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -170,6 +170,11 @@ function validateClientNames(sdkContext: SdkContext) { [...newClients.values()].map((operations) => { validateClientNamesCore(sdkContext, scope, operations); }); + + // Check for Azure library type conflicts (only for types actually used by the client) + if (azureLibraryNamespaces.length > 0) { + validateAzureLibraryTypeConflicts(sdkContext, scope, azureLibraryNamespaces); + } } } @@ -228,12 +233,6 @@ function validateClientNamesPerNamespace( // Check for duplicate client names for scalars validateClientNamesCore(tcgcContext, scope, namespace.scalars.values()); - // Check for Azure library type conflicts separately - // Only check if user types with @clientName conflict with Azure library types - const azureNamespaces = getAzureLibraryNamespaces(tcgcContext.program); - if (azureNamespaces.length > 0) { - validateAzureLibraryTypeConflicts(tcgcContext, scope, typesToValidate, azureNamespaces); - } // Check for duplicate client names for operations validateClientNamesCore( @@ -365,13 +364,13 @@ function collectTypeNamesFromNamespace(namespace: Namespace): Set { * This is a targeted check that only reports conflicts when: * 1. A user type has an explicit @clientName decorator * 2. The client name matches an Azure library type name (from Azure.Core or Azure.ResourceManager) + * 3. The type is actually used by the client (present in sdkPackage) * - * This avoids false positives from Azure's own internal type duplicates. + * This avoids false positives from Azure's own internal type duplicates and unused types. */ function validateAzureLibraryTypeConflicts( - tcgcContext: TCGCContext, + sdkContext: SdkContext, scope: string | typeof AllScopes, - userTypes: (Model | Enum | Union)[], azureNamespaces: Namespace[], ) { // Collect all type names from all Azure library namespaces @@ -383,9 +382,31 @@ function validateAzureLibraryTypeConflicts( } } - for (const userType of userTypes) { + // Build a set of used TypeSpec types from the sdkPackage + const usedTypes = new Set(); + for (const model of sdkContext.sdkPackage.models) { + if (model.__raw) { + usedTypes.add(model.__raw); + } + } + for (const enumType of sdkContext.sdkPackage.enums) { + if (enumType.__raw) { + usedTypes.add(enumType.__raw); + } + } + for (const unionType of sdkContext.sdkPackage.unions) { + if (unionType.__raw) { + usedTypes.add(unionType.__raw); + } + } + + // Check only used types for conflicts + for (const userType of usedTypes) { + if (userType.kind !== "Model" && userType.kind !== "Enum" && userType.kind !== "Union") { + continue; + } // Only check types that have an explicit @clientName decorator - const clientName = getClientNameOverride(tcgcContext, userType, scope); + const clientName = getClientNameOverride(sdkContext, userType, scope); if (clientName !== undefined && azureTypeNames.has(clientName)) { // User type with @clientName conflicts with an Azure library type const clientNameDecorator = userType.decorators.find( @@ -393,7 +414,7 @@ function validateAzureLibraryTypeConflicts( ); if (clientNameDecorator?.node !== undefined) { const scopeStr = scope === AllScopes ? "AllScopes" : scope; - reportDiagnostic(tcgcContext.program, { + reportDiagnostic(sdkContext.program, { code: "duplicate-client-name", format: { name: clientName, scope: scopeStr }, target: clientNameDecorator.node, From ee7ee6547cba47d083bca1c24bc04d37083cb64b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 16:46:12 -0500 Subject: [PATCH 14/19] some perf improvements --- .../src/validations/types.ts | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index fdfbff3a53..8fd99651b1 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -87,6 +87,41 @@ function validateClientNames(sdkContext: SdkContext) { languageScopes.add(AllScopes); } + // Pre-compute Azure library type names once (O(A) where A = types in Azure libraries) + // This avoids recomputing for each scope + let azureTypeNames: Set | undefined; + if (hasAzureLibrary) { + azureTypeNames = new Set(); + for (const ns of azureLibraryNamespaces) { + const names = collectTypeNamesFromNamespace(ns); + for (const name of names) { + azureTypeNames.add(name); + } + } + } + + // Pre-compute used types from sdkPackage once (O(U) where U = used types) + // These are the only types we need to check for Azure library conflicts + let usedTypes: Set | undefined; + if (hasAzureLibrary) { + usedTypes = new Set(); + for (const model of sdkContext.sdkPackage.models) { + if (model.__raw) { + usedTypes.add(model.__raw); + } + } + for (const enumType of sdkContext.sdkPackage.enums) { + if (enumType.__raw) { + usedTypes.add(enumType.__raw); + } + } + for (const unionType of sdkContext.sdkPackage.unions) { + if (unionType.__raw) { + usedTypes.add(unionType.__raw); + } + } + } + // Build a map of namespace names to their types for resolving string targets const namedNamespaces = new Map(); for (const ns of listAllUserDefinedNamespaces(sdkContext)) { @@ -172,8 +207,8 @@ function validateClientNames(sdkContext: SdkContext) { }); // Check for Azure library type conflicts (only for types actually used by the client) - if (azureLibraryNamespaces.length > 0) { - validateAzureLibraryTypeConflicts(sdkContext, scope, azureLibraryNamespaces); + if (azureTypeNames !== undefined && usedTypes !== undefined) { + validateAzureLibraryTypeConflicts(sdkContext, scope, azureTypeNames, usedTypes); } } } @@ -367,39 +402,16 @@ function collectTypeNamesFromNamespace(namespace: Namespace): Set { * 3. The type is actually used by the client (present in sdkPackage) * * This avoids false positives from Azure's own internal type duplicates and unused types. + * + * Time complexity: O(U) where U = number of used types in sdkPackage + * Space complexity: O(1) - only uses pre-computed sets passed in, no new allocations */ function validateAzureLibraryTypeConflicts( sdkContext: SdkContext, scope: string | typeof AllScopes, - azureNamespaces: Namespace[], + azureTypeNames: Set, + usedTypes: Set, ) { - // Collect all type names from all Azure library namespaces - const azureTypeNames = new Set(); - for (const ns of azureNamespaces) { - const names = collectTypeNamesFromNamespace(ns); - for (const name of names) { - azureTypeNames.add(name); - } - } - - // Build a set of used TypeSpec types from the sdkPackage - const usedTypes = new Set(); - for (const model of sdkContext.sdkPackage.models) { - if (model.__raw) { - usedTypes.add(model.__raw); - } - } - for (const enumType of sdkContext.sdkPackage.enums) { - if (enumType.__raw) { - usedTypes.add(enumType.__raw); - } - } - for (const unionType of sdkContext.sdkPackage.unions) { - if (unionType.__raw) { - usedTypes.add(unionType.__raw); - } - } - // Check only used types for conflicts for (const userType of usedTypes) { if (userType.kind !== "Model" && userType.kind !== "Enum" && userType.kind !== "Union") { From 94fcfc4fb39f149f4ac888aa526d047f5f161233 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 17:00:55 -0500 Subject: [PATCH 15/19] add tests for api version enum --- .../src/context.ts | 2 +- .../src/validations/types.ts | 21 +++++++++-- .../test/validations/types.test.ts | 35 +++++++++++++++++-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index b856ede004..3b9669aa67 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -21,7 +21,6 @@ import { stringify } from "yaml"; import { prepareClientAndOperationCache } from "./cache.js"; import { defaultDecoratorsAllowList } from "./configs.js"; import { handleClientExamples } from "./example.js"; -import { validateTypes } from "./validations/types.js"; import { getKnownScalars, SdkArrayType, @@ -48,6 +47,7 @@ import { TspLiteralType, } from "./internal-utils.js"; import { createSdkPackage } from "./package.js"; +import { validateTypes } from "./validations/types.js"; interface CreateTCGCContextOptions { mutateNamespace?: boolean; // whether to mutate global namespace for versioning diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 8fd99651b1..f288f71b88 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -16,7 +16,7 @@ import { AugmentDecoratorStatementNode, DecoratorExpressionNode } from "@typespe import { unsafe_Realm } from "@typespec/compiler/experimental"; import { DuplicateTracker } from "@typespec/compiler/utils"; import { getClientNameOverride } from "../decorators.js"; -import { SdkClient, SdkContext, TCGCContext } from "../interfaces.js"; +import { SdkClient, SdkContext, TCGCContext, UsageFlags } from "../interfaces.js"; import { AllScopes, clientKey, @@ -122,6 +122,16 @@ function validateClientNames(sdkContext: SdkContext) { } } + // Pre-compute API version enum names to exclude from duplicate name validation + // API version enums (e.g., "Versions") commonly have the same name across services and that's OK + // We store names instead of Type references because versioning can create different type instances + const apiVersionEnumNames = new Set(); + for (const enumType of sdkContext.sdkPackage.enums) { + if ((enumType.usage & UsageFlags.ApiVersionEnum) !== 0) { + apiVersionEnumNames.add(enumType.name); + } + } + // Build a map of namespace names to their types for resolving string targets const namedNamespaces = new Map(); for (const ns of listAllUserDefinedNamespaces(sdkContext)) { @@ -189,6 +199,7 @@ function validateClientNames(sdkContext: SdkContext) { moved, movedTo, multiServiceNamespaces, + apiVersionEnumNames, ); } else { // For single-service: per-namespace validation @@ -268,7 +279,6 @@ function validateClientNamesPerNamespace( // Check for duplicate client names for scalars validateClientNamesCore(tcgcContext, scope, namespace.scalars.values()); - // Check for duplicate client names for operations validateClientNamesCore( tcgcContext, @@ -447,6 +457,7 @@ function validateClientNamesAcrossNamespaces( moved: Set, movedTo: Map, serviceNamespaces: Namespace[], + apiVersionEnumNames: Set, ) { // Collect all types from all service namespaces const allModels: Model[] = []; @@ -457,8 +468,12 @@ function validateClientNamesAcrossNamespaces( collectTypesFromNamespace(serviceNs, allModels, allEnums, allUnions); } + // Filter out API version enums - they commonly have the same name (e.g., "Versions") + // across services and that's expected/allowed + const filteredEnums = allEnums.filter((e) => !e.name || !apiVersionEnumNames.has(e.name)); + // Validate models, enums, and unions together across all services - validateClientNamesCore(tcgcContext, scope, [...allModels, ...allEnums, ...allUnions]); + validateClientNamesCore(tcgcContext, scope, [...allModels, ...filteredEnums, ...allUnions]); // Also validate within each service namespace for operations, interfaces, properties, etc. // These are scoped to their containers and don't need cross-service validation diff --git a/packages/typespec-client-generator-core/test/validations/types.test.ts b/packages/typespec-client-generator-core/test/validations/types.test.ts index 35ce53f80c..22762f5179 100644 --- a/packages/typespec-client-generator-core/test/validations/types.test.ts +++ b/packages/typespec-client-generator-core/test/validations/types.test.ts @@ -255,6 +255,38 @@ describe("multi-service duplicate name validation", () => { expectDiagnosticEmpty(diagnostics); }); + it("no error for same API version enum name across services in multi-service client", async () => { + // API version enums (e.g., "Versions") commonly have the same name across services + // and that's expected and allowed - they're identified by UsageFlags.ApiVersionEnum + const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( + ` + @service + @versioned(Versions) + namespace ServiceA { + enum Versions { v1 } + model FooA { a: string; } + @route("/a") op getA(): FooA; + } + @service + @versioned(Versions) + namespace ServiceB { + enum Versions { v1 } + model FooB { b: string; } + @route("/b") op getB(): FooB; + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.Versions.v1, ServiceB.Versions.v1) + namespace CombineClient; + `, + ); + + // Should not report errors for "Versions" enums being duplicated + // because they are API version enums + expectDiagnosticEmpty(diagnostics); + }); + it("error for duplicate model name within same service namespace", async () => { // Within the same service namespace, duplicate names ARE an error const diagnostics = await runner.diagnose( @@ -330,8 +362,7 @@ describe("multi-service duplicate name validation", () => { expectDiagnostics(diagnostics, [ { code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", - message: - 'Client name: "ExtensionResource" is duplicated in language scope: "AllScopes"', + message: 'Client name: "ExtensionResource" is duplicated in language scope: "AllScopes"', }, ]); }); From cf0847cd31eb4721288cfee2c1b412d827f8a97b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 17:05:22 -0500 Subject: [PATCH 16/19] remove scalar checks --- .../typespec-client-generator-core/src/internal-utils.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index fb6c8bea7f..616d760bee 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -1209,13 +1209,4 @@ function collectTypesFromNamespace( allTypeNames.set(iface.name, existing); } } - - // Collect scalars - for (const scalar of namespace.scalars.values()) { - if (scalar.name && $(program).type.isUserDefined(scalar)) { - const existing = allTypeNames.get(scalar.name) ?? []; - existing.push(scalar); - allTypeNames.set(scalar.name, existing); - } - } } From c852c589bb82a4de1549c1747f4e3a90ca97206e Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Tue, 20 Jan 2026 17:16:58 -0500 Subject: [PATCH 17/19] still validate names within namespace in onValidate --- .../src/context.ts | 7 +- .../src/validate.ts | 7 +- .../src/validations/types.ts | 152 +++++++++++++----- 3 files changed, 125 insertions(+), 41 deletions(-) diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index 3b9669aa67..40491b8bd7 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -47,7 +47,7 @@ import { TspLiteralType, } from "./internal-utils.js"; import { createSdkPackage } from "./package.js"; -import { validateTypes } from "./validations/types.js"; +import { validateNamespaceCollisions } from "./validations/types.js"; interface CreateTCGCContextOptions { mutateNamespace?: boolean; // whether to mutate global namespace for versioning @@ -218,8 +218,9 @@ export async function createSdkContext< } sdkContext.diagnostics = sdkContext.diagnostics.concat(diagnostics.diagnostics); - // Validate type names for duplicates (done here to have access to emitter options/flags) - validateTypes(sdkContext); + // Validate cross-namespace collisions (multi-service and Azure library conflicts) + // Done here to have access to sdkPackage and emitter options + validateNamespaceCollisions(sdkContext); if (options?.exportTCGCoutput) { await exportTCGCOutput(sdkContext); diff --git a/packages/typespec-client-generator-core/src/validate.ts b/packages/typespec-client-generator-core/src/validate.ts index d62b07dd27..738148e738 100644 --- a/packages/typespec-client-generator-core/src/validate.ts +++ b/packages/typespec-client-generator-core/src/validate.ts @@ -4,6 +4,7 @@ import { validateClients } from "./validations/clients.js"; import { validateHttp } from "./validations/http.js"; import { validateMethods } from "./validations/methods.js"; import { validatePackage } from "./validations/package.js"; +import { validateTypes } from "./validations/types.js"; export function $onValidate(program: Program) { const tcgcContext = createTCGCContext(program, "@azure-tools/typespec-client-generator-core", { @@ -14,6 +15,8 @@ export function $onValidate(program: Program) { validateClients(tcgcContext); validateMethods(tcgcContext); validateHttp(tcgcContext); - // Note: Type name validation (validateTypes) is now done in createSdkContext - // where we have access to emitter options/flags + // Basic client name validation within namespaces + validateTypes(tcgcContext); + // Note: Cross-namespace collision validation (multi-service and Azure library conflicts) + // is done in createSdkContext where we have access to sdkPackage and emitter options } diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index f288f71b88..6e92a02bd6 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -28,8 +28,16 @@ import { } from "../internal-utils.js"; import { reportDiagnostic } from "../lib.js"; -export function validateTypes(context: SdkContext) { - validateClientNames(context); +export function validateTypes(context: TCGCContext) { + validateClientNamesWithinNamespaces(context); +} + +/** + * Validate cross-namespace collisions for multi-service and Azure library conflicts. + * This runs in createSdkContext where we have access to sdkPackage and emitter options. + */ +export function validateNamespaceCollisions(context: SdkContext) { + validateCrossNamespaceClientNames(context); } /** @@ -56,16 +64,107 @@ function getMultiServiceNamespaces(context: TCGCContext): Namespace[] { } /** - * Validate naming with `@clientName` and `@clientLocation` decorators. - * - * This function checks for duplicate client names for types considering the impact of `@clientName` for all possible scopes. + * Validate basic client name duplicates within namespaces. + * This checks for duplicate client names for types considering the impact of `@clientName` for all possible scopes. * It also handles the movement of operations to new clients based on the `@clientLocation` decorators. - * For multi-service clients, it validates names across ALL service namespaces together since combining - * multiple services into one client means types from all services will be in the same client. * - * @param sdkContext The SDK context (includes emitter options like namespaceFlag). + * This runs in $onValidate and doesn't require SdkContext. + * + * @param tcgcContext The TCGC context. + */ +function validateClientNamesWithinNamespaces(tcgcContext: TCGCContext) { + const languageScopes = getDefinedLanguageScopes(tcgcContext.program); + // If no `@client` or `@operationGroup` decorators are defined, we consider `@clientLocation` + const needToConsiderClientLocation = !hasExplicitClientOrOperationGroup(tcgcContext); + + // Ensure we always run validation at least once (with AllScopes) + if (languageScopes.size === 0) { + languageScopes.add(AllScopes); + } + + // Build a map of namespace names to their types for resolving string targets + const namedNamespaces = new Map(); + for (const ns of listAllUserDefinedNamespaces(tcgcContext)) { + if (ns.name) { + namedNamespaces.set(ns.name, ns); + } + } + + // Check all possible language scopes + for (const scope of languageScopes) { + // Gather all moved operations and their targets + const moved = new Set(); + const movedTo = new Map(); + const newClients = new Map(); + if (needToConsiderClientLocation) { + // Cache all `@clientName` overrides for the current scope + for (const [type, target] of listScopedDecoratorData( + tcgcContext, + clientLocationKey, + scope, + ).entries()) { + if (unsafe_Realm.realmForType.has(type)) { + // Skip `@clientName` on versioning types + continue; + } + if (type.kind === "Operation") { + moved.add(type); + if (typeof target === "string") { + // Check if the string target matches an existing namespace + const existingNamespace = namedNamespaces.get(target); + if (existingNamespace) { + // Move to existing namespace referenced by name + if (!movedTo.has(existingNamespace)) { + movedTo.set(existingNamespace, [type]); + } else { + movedTo.get(existingNamespace)!.push(type); + } + } else { + // Move to new clients (string doesn't match any existing namespace) + if (!newClients.has(target)) { + newClients.set(target, [type]); + } else { + newClients.get(target)!.push(type); + } + } + } else { + // Move to existing clients (target is already a Namespace or Interface) + if (!movedTo.has(target)) { + movedTo.set(target, [type]); + } else { + movedTo.get(target)!.push(type); + } + } + } + } + } + + // Per-namespace validation for client name duplicates + validateClientNamesPerNamespace( + tcgcContext, + scope, + moved, + movedTo, + tcgcContext.program.getGlobalNamespaceType(), + ); + + // Validate client names for new client's operations + [...newClients.values()].map((operations) => { + validateClientNamesCore(tcgcContext, scope, operations); + }); + } +} + +/** + * Validate cross-namespace collisions for multi-service clients and Azure library type conflicts. + * This requires SdkContext because: + * 1. Multi-service validation needs to check types across namespaces + * 2. Azure library conflict detection needs sdkPackage to check only used types + * 3. API version enum exclusion needs UsageFlags from sdkPackage + * + * @param sdkContext The SDK context (includes sdkPackage and emitter options). */ -function validateClientNames(sdkContext: SdkContext) { +function validateCrossNamespaceClientNames(sdkContext: SdkContext) { const languageScopes = getDefinedLanguageScopes(sdkContext.program); // If no `@client` or `@operationGroup` decorators are defined, we consider `@clientLocation` const needToConsiderClientLocation = !hasExplicitClientOrOperationGroup(sdkContext); @@ -80,10 +179,13 @@ function validateClientNames(sdkContext: SdkContext) { const azureLibraryNamespaces = getAzureLibraryNamespaces(sdkContext.program); const hasAzureLibrary = azureLibraryNamespaces.length > 0; - // Ensure we always run validation at least once (with AllScopes) for: - // - Multi-service scenarios (types across services may collide in the combined client) - // - Azure library scenarios (user types may conflict with Azure.Core or Azure.ResourceManager types) - if (languageScopes.size === 0 && (isMultiService || hasAzureLibrary)) { + // Only run if there's cross-namespace validation to do + if (!isMultiService && !hasAzureLibrary) { + return; + } + + // Ensure we always run validation at least once (with AllScopes) + if (languageScopes.size === 0) { languageScopes.add(AllScopes); } @@ -142,10 +244,9 @@ function validateClientNames(sdkContext: SdkContext) { // Check all possible language scopes for (const scope of languageScopes) { - // Gather all moved operations and their targets + // Gather all moved operations and their targets (needed for multi-service validation) const moved = new Set(); const movedTo = new Map(); - const newClients = new Map(); if (needToConsiderClientLocation) { // Cache all `@clientName` overrides for the current scope for (const [type, target] of listScopedDecoratorData( @@ -169,13 +270,6 @@ function validateClientNames(sdkContext: SdkContext) { } else { movedTo.get(existingNamespace)!.push(type); } - } else { - // Move to new clients (string doesn't match any existing namespace) - if (!newClients.has(target)) { - newClients.set(target, [type]); - } else { - newClients.get(target)!.push(type); - } } } else { // Move to existing clients (target is already a Namespace or Interface) @@ -201,22 +295,8 @@ function validateClientNames(sdkContext: SdkContext) { multiServiceNamespaces, apiVersionEnumNames, ); - } else { - // For single-service: per-namespace validation - validateClientNamesPerNamespace( - sdkContext, - scope, - moved, - movedTo, - sdkContext.program.getGlobalNamespaceType(), - ); } - // Validate client names for new client's operations - [...newClients.values()].map((operations) => { - validateClientNamesCore(sdkContext, scope, operations); - }); - // Check for Azure library type conflicts (only for types actually used by the client) if (azureTypeNames !== undefined && usedTypes !== undefined) { validateAzureLibraryTypeConflicts(sdkContext, scope, azureTypeNames, usedTypes); From 2df1e8f15fa4fc39c83ea55f0878a6ff3736cd0f Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Jan 2026 13:03:29 -0500 Subject: [PATCH 18/19] only throw if namespace flag flattens --- .../src/validations/types.ts | 250 +++--------------- .../test/validations/types.test.ts | 201 +++++++++++--- 2 files changed, 203 insertions(+), 248 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index 6e92a02bd6..a7a37f56eb 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -16,10 +16,9 @@ import { AugmentDecoratorStatementNode, DecoratorExpressionNode } from "@typespe import { unsafe_Realm } from "@typespec/compiler/experimental"; import { DuplicateTracker } from "@typespec/compiler/utils"; import { getClientNameOverride } from "../decorators.js"; -import { SdkClient, SdkContext, TCGCContext, UsageFlags } from "../interfaces.js"; +import { SdkContext, TCGCContext, UsageFlags } from "../interfaces.js"; import { AllScopes, - clientKey, clientLocationKey, clientNameKey, hasExplicitClientOrOperationGroup, @@ -33,36 +32,13 @@ export function validateTypes(context: TCGCContext) { } /** - * Validate cross-namespace collisions for multi-service and Azure library conflicts. + * Validate cross-namespace collisions when the --namespace flag is set. * This runs in createSdkContext where we have access to sdkPackage and emitter options. */ export function validateNamespaceCollisions(context: SdkContext) { validateCrossNamespaceClientNames(context); } -/** - * Get all service namespaces from multi-service clients. - * Uses listScopedDecoratorData directly instead of listClients to avoid - * the cache cleanup that removes empty multi-service clients. - * Returns empty array if no multi-service clients exist. - */ -function getMultiServiceNamespaces(context: TCGCContext): Namespace[] { - // Use a map keyed by namespace name to deduplicate, as versioning can create - // different namespace instances with the same name - const namespaceMap = new Map(); - // Directly query @client decorator data to find multi-service clients - listScopedDecoratorData(context, clientKey).forEach((clientData: SdkClient) => { - if (clientData.services && clientData.services.length > 1) { - for (const ns of clientData.services) { - if (ns.name && !namespaceMap.has(ns.name)) { - namespaceMap.set(ns.name, ns); - } - } - } - }); - return [...namespaceMap.values()]; -} - /** * Validate basic client name duplicates within namespaces. * This checks for duplicate client names for types considering the impact of `@clientName` for all possible scopes. @@ -156,34 +132,30 @@ function validateClientNamesWithinNamespaces(tcgcContext: TCGCContext) { } /** - * Validate cross-namespace collisions for multi-service clients and Azure library type conflicts. + * Validate cross-namespace collisions when the --namespace flag is set. + * When namespaces are flattened, types from different namespaces may collide. + * Also checks for Azure library type conflicts. + * * This requires SdkContext because: - * 1. Multi-service validation needs to check types across namespaces + * 1. Needs access to namespaceFlag emitter option * 2. Azure library conflict detection needs sdkPackage to check only used types - * 3. API version enum exclusion needs UsageFlags from sdkPackage * * @param sdkContext The SDK context (includes sdkPackage and emitter options). */ function validateCrossNamespaceClientNames(sdkContext: SdkContext) { - const languageScopes = getDefinedLanguageScopes(sdkContext.program); - // If no `@client` or `@operationGroup` decorators are defined, we consider `@clientLocation` - const needToConsiderClientLocation = !hasExplicitClientOrOperationGroup(sdkContext); - - // Detect multi-service scenario. - // Multi-service (@client with multiple services) inherently combines types from all services - // into a single client, so cross-namespace validation is needed. - const multiServiceNamespaces = getMultiServiceNamespaces(sdkContext); - const isMultiService = multiServiceNamespaces.length > 0; - // Check if any Azure library namespace exists (Azure.Core or Azure.ResourceManager) const azureLibraryNamespaces = getAzureLibraryNamespaces(sdkContext.program); const hasAzureLibrary = azureLibraryNamespaces.length > 0; - // Only run if there's cross-namespace validation to do - if (!isMultiService && !hasAzureLibrary) { + // Only run if there's cross-namespace validation to do: + // - namespaceFlag is set (types will be flattened) + // - or Azure library is present (need to check for conflicts) + if (!sdkContext.namespaceFlag && !hasAzureLibrary) { return; } + const languageScopes = getDefinedLanguageScopes(sdkContext.program); + // Ensure we always run validation at least once (with AllScopes) if (languageScopes.size === 0) { languageScopes.add(AllScopes); @@ -224,77 +196,12 @@ function validateCrossNamespaceClientNames(sdkContext: SdkContext) { } } - // Pre-compute API version enum names to exclude from duplicate name validation - // API version enums (e.g., "Versions") commonly have the same name across services and that's OK - // We store names instead of Type references because versioning can create different type instances - const apiVersionEnumNames = new Set(); - for (const enumType of sdkContext.sdkPackage.enums) { - if ((enumType.usage & UsageFlags.ApiVersionEnum) !== 0) { - apiVersionEnumNames.add(enumType.name); - } - } - - // Build a map of namespace names to their types for resolving string targets - const namedNamespaces = new Map(); - for (const ns of listAllUserDefinedNamespaces(sdkContext)) { - if (ns.name) { - namedNamespaces.set(ns.name, ns); - } - } - // Check all possible language scopes for (const scope of languageScopes) { - // Gather all moved operations and their targets (needed for multi-service validation) - const moved = new Set(); - const movedTo = new Map(); - if (needToConsiderClientLocation) { - // Cache all `@clientName` overrides for the current scope - for (const [type, target] of listScopedDecoratorData( - sdkContext, - clientLocationKey, - scope, - ).entries()) { - if (unsafe_Realm.realmForType.has(type)) { - // Skip `@clientName` on versioning types - continue; - } - if (type.kind === "Operation") { - moved.add(type); - if (typeof target === "string") { - // Check if the string target matches an existing namespace - const existingNamespace = namedNamespaces.get(target); - if (existingNamespace) { - // Move to existing namespace referenced by name - if (!movedTo.has(existingNamespace)) { - movedTo.set(existingNamespace, [type]); - } else { - movedTo.get(existingNamespace)!.push(type); - } - } - } else { - // Move to existing clients (target is already a Namespace or Interface) - if (!movedTo.has(target)) { - movedTo.set(target, [type]); - } else { - movedTo.get(target)!.push(type); - } - } - } - } - } - - if (isMultiService) { - // For multi-service: validate types across ALL service namespaces together - // because they will be in the same client. - // Same-named types across different services will collide. - validateClientNamesAcrossNamespaces( - sdkContext, - scope, - moved, - movedTo, - multiServiceNamespaces, - apiVersionEnumNames, - ); + // When namespaceFlag is set, validate types across ALL namespaces together + // because they will be flattened into a single namespace + if (sdkContext.namespaceFlag) { + validateClientNamesAcrossAllNamespaces(sdkContext, scope); } // Check for Azure library type conflicts (only for types actually used by the client) @@ -402,30 +309,6 @@ function validateClientNamesPerNamespace( } } -/** - * Collect all types from a namespace and its nested namespaces recursively. - * - * TODO: This currently collects ALL types in the namespace, including types that may not - * be used by the client. Ideally we should only validate types that are actually referenced - * by the client's operations. This would require running after SDK type resolution or - * implementing usage tracking at the TypeSpec level. - */ -function collectTypesFromNamespace( - namespace: Namespace, - models: Model[], - enums: Enum[], - unions: Union[], -) { - models.push(...namespace.models.values()); - enums.push(...namespace.enums.values()); - unions.push(...namespace.unions.values()); - - // Recursively collect from nested namespaces - for (const nestedNs of namespace.namespaces.values()) { - collectTypesFromNamespace(nestedNs, models, enums, unions); - } -} - /** * Get Azure library namespaces (Azure.Core and Azure.ResourceManager) if they exist. * These are used to detect naming conflicts between user-defined types @@ -527,93 +410,40 @@ function validateAzureLibraryTypeConflicts( } /** - * Validate client names across multiple service namespaces for multi-service clients. - * Types with the same name across different services will collide when generated - * into the same namespace. + * Validate client names across all user-defined namespaces when the --namespace flag is set. + * When namespaces are flattened, types from different namespaces may collide. */ -function validateClientNamesAcrossNamespaces( - tcgcContext: TCGCContext, - scope: string | typeof AllScopes, - moved: Set, - movedTo: Map, - serviceNamespaces: Namespace[], - apiVersionEnumNames: Set, -) { - // Collect all types from all service namespaces +function validateClientNamesAcrossAllNamespaces(sdkContext: SdkContext, scope: string | typeof AllScopes) { + // Collect all types from all user-defined namespaces + // Note: listAllUserDefinedNamespaces already includes nested namespaces, + // so we collect types directly from each namespace without recursion const allModels: Model[] = []; const allEnums: Enum[] = []; const allUnions: Union[] = []; - for (const serviceNs of serviceNamespaces) { - collectTypesFromNamespace(serviceNs, allModels, allEnums, allUnions); + for (const ns of listAllUserDefinedNamespaces(sdkContext)) { + // Collect types directly from this namespace (not recursively) + allModels.push(...ns.models.values()); + allEnums.push(...ns.enums.values()); + allUnions.push(...ns.unions.values()); + } + + // Pre-compute API version enum names to exclude from duplicate name validation + // API version enums (e.g., "Versions") commonly have the same name across services and that's OK + // We store names instead of Type references because versioning can create different type instances + const apiVersionEnumNames = new Set(); + for (const enumType of sdkContext.sdkPackage.enums) { + if ((enumType.usage & UsageFlags.ApiVersionEnum) !== 0) { + apiVersionEnumNames.add(enumType.name); + } } // Filter out API version enums - they commonly have the same name (e.g., "Versions") // across services and that's expected/allowed const filteredEnums = allEnums.filter((e) => !e.name || !apiVersionEnumNames.has(e.name)); - // Validate models, enums, and unions together across all services - validateClientNamesCore(tcgcContext, scope, [...allModels, ...filteredEnums, ...allUnions]); - - // Also validate within each service namespace for operations, interfaces, properties, etc. - // These are scoped to their containers and don't need cross-service validation - for (const serviceNs of serviceNamespaces) { - validateClientNamesPerNamespaceOperationsOnly(tcgcContext, scope, moved, movedTo, serviceNs); - } -} - -/** - * Validate only operations and their containers within a namespace. - * Used for multi-service validation where types are validated separately across all services. - */ -function validateClientNamesPerNamespaceOperationsOnly( - tcgcContext: TCGCContext, - scope: string | typeof AllScopes, - moved: Set, - movedTo: Map, - namespace: Namespace, -) { - // Check for duplicate client names for operations - validateClientNamesCore( - tcgcContext, - scope, - adjustOperations(namespace.operations.values(), moved, movedTo, namespace), - ); - - // Check for duplicate client names for operations in interfaces - for (const item of namespace.interfaces.values()) { - validateClientNamesCore( - tcgcContext, - scope, - adjustOperations(item.operations.values(), moved, movedTo, item), - ); - } - - // Check for duplicate client names for interfaces - validateClientNamesCore(tcgcContext, scope, namespace.interfaces.values()); - - // Check for duplicate client names for namespaces - validateClientNamesCore(tcgcContext, scope, namespace.namespaces.values()); - - // Check for duplicate client names for model properties (within each model) - for (const model of namespace.models.values()) { - validateClientNamesCore(tcgcContext, scope, model.properties.values()); - } - - // Check for duplicate client names for enum members (within each enum) - for (const item of namespace.enums.values()) { - validateClientNamesCore(tcgcContext, scope, item.members.values()); - } - - // Check for duplicate client names for union variants (within each union) - for (const item of namespace.unions.values()) { - validateClientNamesCore(tcgcContext, scope, item.variants.values()); - } - - // Recurse into nested namespaces - for (const item of namespace.namespaces.values()) { - validateClientNamesPerNamespaceOperationsOnly(tcgcContext, scope, moved, movedTo, item); - } + // Validate models, enums, and unions together across all namespaces + validateClientNamesCore(sdkContext, scope, [...allModels, ...filteredEnums, ...allUnions]); } function validateClientNamesCore( diff --git a/packages/typespec-client-generator-core/test/validations/types.test.ts b/packages/typespec-client-generator-core/test/validations/types.test.ts index 14e8b1849b..364638e824 100644 --- a/packages/typespec-client-generator-core/test/validations/types.test.ts +++ b/packages/typespec-client-generator-core/test/validations/types.test.ts @@ -8,14 +8,14 @@ import { SimpleTester, } from "../tester.js"; -describe("multi-service duplicate name validation", () => { - // In multi-service scenarios, models/enums/unions with the same name in different services - // ARE duplicates because combining multiple services into one client means all types - // will be in the same client. - - it("error for same model name across services in multi-service client", async () => { - // Same-named models in different services will collide when combined into one client - const [{ program }, diagnostics] = await SimpleBaseTester.compileAndDiagnose( +describe("cross-namespace duplicate name validation", () => { + // Cross-namespace validation runs when the --namespace flag is set. + // When namespaces are flattened, types with the same name across different namespaces + // will collide in the generated client. + + it("error for same model name across namespaces with namespace flag", async () => { + // Same-named models in different namespaces will collide when namespace flag is set + const { program } = await SimpleBaseTester.compile( createClientCustomizationInput( ` @service @@ -39,8 +39,12 @@ describe("multi-service duplicate name validation", () => { ), ); - const context = await createSdkContextForTester(program); - expectDiagnostics([...diagnostics, ...context.diagnostics], [ + const context = await createSdkContextForTester(program, { namespace: "CombineClient" }); + // Filter to get only the cross-namespace duplicate diagnostics from program + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnostics(duplicateDiagnostics, [ { code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", message: @@ -54,9 +58,9 @@ describe("multi-service duplicate name validation", () => { ]); }); - it("error for same enum name across services in multi-service client", async () => { - // Same-named enums in different services will collide when combined into one client - const [{ program }, diagnostics] = await SimpleBaseTester.compileAndDiagnose( + it("error for same enum name across namespaces with namespace flag", async () => { + // Same-named enums in different namespaces will collide when namespace flag is set + const { program } = await SimpleBaseTester.compile( createClientCustomizationInput( ` @service @@ -80,8 +84,11 @@ describe("multi-service duplicate name validation", () => { ), ); - const context = await createSdkContextForTester(program); - expectDiagnostics([...diagnostics, ...context.diagnostics], [ + const context = await createSdkContextForTester(program, { namespace: "CombineClient" }); + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnostics(duplicateDiagnostics, [ { code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", message: @@ -95,9 +102,9 @@ describe("multi-service duplicate name validation", () => { ]); }); - it("error for same union name across services in multi-service client", async () => { - // Same-named unions in different services will collide when combined into one client - const [{ program }, diagnostics] = await SimpleBaseTester.compileAndDiagnose( + it("error for same union name across namespaces with namespace flag", async () => { + // Same-named unions in different namespaces will collide when namespace flag is set + const { program } = await SimpleBaseTester.compile( createClientCustomizationInput( ` @service @@ -121,12 +128,11 @@ describe("multi-service duplicate name validation", () => { ), ); - const context = await createSdkContextForTester(program); - // Filter to only check TCGC duplicate-client-name diagnostics (ignore Azure Core union warnings) - const allDiagnostics = [...diagnostics, ...context.diagnostics].filter( + const context = await createSdkContextForTester(program, { namespace: "CombineClient" }); + const duplicateDiagnostics = context.program.diagnostics.filter( (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", ); - expectDiagnostics(allDiagnostics, [ + expectDiagnostics(duplicateDiagnostics, [ { code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", message: @@ -140,8 +146,8 @@ describe("multi-service duplicate name validation", () => { ]); }); - it("no error for different names across services", async () => { - const [_, diagnostics] = await SimpleBaseTester.compileAndDiagnose( + it("no error for different names across namespaces with namespace flag", async () => { + const { program } = await SimpleBaseTester.compile( createClientCustomizationInput( ` @service @@ -165,12 +171,16 @@ describe("multi-service duplicate name validation", () => { ), ); - expectDiagnosticEmpty(diagnostics); + const context = await createSdkContextForTester(program, { namespace: "CombineClient" }); + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnosticEmpty(duplicateDiagnostics); }); - it("error for @clientName same name across services in multi-service client", async () => { - // @clientName causing same name across services will collide when combined into one client - const [{ program }, diagnostics] = await SimpleBaseTester.compileAndDiagnose( + it("error for @clientName same name across namespaces with namespace flag", async () => { + // @clientName causing same name across namespaces will collide when namespace flag is set + const { program } = await SimpleBaseTester.compile( createClientCustomizationInput( ` @service @@ -196,8 +206,11 @@ describe("multi-service duplicate name validation", () => { ), ); - const context = await createSdkContextForTester(program); - expectDiagnostics([...diagnostics, ...context.diagnostics], [ + const context = await createSdkContextForTester(program, { namespace: "CombineClient" }); + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnostics(duplicateDiagnostics, [ { code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", message: 'Client name: "SharedName" is duplicated in language scope: "AllScopes"', @@ -209,9 +222,9 @@ describe("multi-service duplicate name validation", () => { ]); }); - it("error for nested namespace type with same name in multi-service client", async () => { - // Nested namespaces in different services will also collide when combined into one client - const [{ program }, diagnostics] = await SimpleBaseTester.compileAndDiagnose( + it("error for nested namespace type with same name with namespace flag", async () => { + // Nested namespaces will also collide when namespace flag is set + const { program } = await SimpleBaseTester.compile( createClientCustomizationInput( ` @service @@ -239,8 +252,11 @@ describe("multi-service duplicate name validation", () => { ), ); - const context = await createSdkContextForTester(program); - expectDiagnostics([...diagnostics, ...context.diagnostics], [ + const context = await createSdkContextForTester(program, { namespace: "CombineClient" }); + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnostics(duplicateDiagnostics, [ { code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", message: @@ -273,10 +289,10 @@ describe("multi-service duplicate name validation", () => { expectDiagnosticEmpty(diagnostics); }); - it("no error for same API version enum name across services in multi-service client", async () => { + it("no error for same API version enum name across namespaces with namespace flag", async () => { // API version enums (e.g., "Versions") commonly have the same name across services // and that's expected and allowed - they're identified by UsageFlags.ApiVersionEnum - const [_, diagnostics] = await SimpleBaseTester.compileAndDiagnose( + const { program } = await SimpleBaseTester.compile( createClientCustomizationInput( ` @service @@ -302,9 +318,118 @@ describe("multi-service duplicate name validation", () => { ), ); + const context = await createSdkContextForTester(program, { namespace: "CombineClient" }); // Should not report errors for "Versions" enums being duplicated // because they are API version enums - expectDiagnosticEmpty(diagnostics); + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnosticEmpty(duplicateDiagnostics); + }); + + it("no error for same model name across services in multi-service client without namespace flag", async () => { + // Multi-service clients without namespace flag should NOT raise duplicate name errors + // because the namespaces are not being flattened + const { program } = await SimpleBaseTester.compile( + createClientCustomizationInput( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { v1 } + model Foo { a: string; } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { v1 } + model Foo { b: string; } + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.VersionsA.v1, ServiceB.VersionsB.v1) + namespace CombineClient; + `, + ), + ); + + // No namespace flag - should not report cross-namespace duplicates + const context = await createSdkContextForTester(program); + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnosticEmpty(duplicateDiagnostics); + }); + + it("no error for same enum name across services in multi-service client without namespace flag", async () => { + // Multi-service clients without namespace flag should NOT raise duplicate name errors + const { program } = await SimpleBaseTester.compile( + createClientCustomizationInput( + ` + @service + @versioned(VersionsA) + namespace ServiceA { + enum VersionsA { v1 } + enum Status { Active, Inactive } + } + @service + @versioned(VersionsB) + namespace ServiceB { + enum VersionsB { v1 } + enum Status { Pending, Complete } + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.VersionsA.v1, ServiceB.VersionsB.v1) + namespace CombineClient; + `, + ), + ); + + // No namespace flag - should not report cross-namespace duplicates + const context = await createSdkContextForTester(program); + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnosticEmpty(duplicateDiagnostics); + }); + + it("no error for same API version enum name across services in multi-service client without namespace flag", async () => { + // API version enums with the same name in multi-service clients without namespace flag should not error + const { program } = await SimpleBaseTester.compile( + createClientCustomizationInput( + ` + @service + @versioned(Versions) + namespace ServiceA { + enum Versions { v1 } + model FooA { a: string; } + @route("/a") op getA(): FooA; + } + @service + @versioned(Versions) + namespace ServiceB { + enum Versions { v1 } + model FooB { b: string; } + @route("/b") op getB(): FooB; + } + `, + ` + @client({ name: "CombineClient", service: [ServiceA, ServiceB] }) + @useDependency(ServiceA.Versions.v1, ServiceB.Versions.v1) + namespace CombineClient; + `, + ), + ); + + // No namespace flag - should not report any duplicates + const context = await createSdkContextForTester(program); + const duplicateDiagnostics = context.program.diagnostics.filter( + (d) => d.code === "@azure-tools/typespec-client-generator-core/duplicate-client-name", + ); + expectDiagnosticEmpty(duplicateDiagnostics); }); it("error for duplicate model name within same service namespace", async () => { From 7fd7f92d81274b025b1aa15176a00497890b085b Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Wed, 28 Jan 2026 13:04:16 -0500 Subject: [PATCH 19/19] format --- .../src/validations/types.ts | 5 ++++- .../test/validations/types.test.ts | 15 +++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/typespec-client-generator-core/src/validations/types.ts b/packages/typespec-client-generator-core/src/validations/types.ts index a7a37f56eb..e44d0899c8 100644 --- a/packages/typespec-client-generator-core/src/validations/types.ts +++ b/packages/typespec-client-generator-core/src/validations/types.ts @@ -413,7 +413,10 @@ function validateAzureLibraryTypeConflicts( * Validate client names across all user-defined namespaces when the --namespace flag is set. * When namespaces are flattened, types from different namespaces may collide. */ -function validateClientNamesAcrossAllNamespaces(sdkContext: SdkContext, scope: string | typeof AllScopes) { +function validateClientNamesAcrossAllNamespaces( + sdkContext: SdkContext, + scope: string | typeof AllScopes, +) { // Collect all types from all user-defined namespaces // Note: listAllUserDefinedNamespaces already includes nested namespaces, // so we collect types directly from each namespace without recursion diff --git a/packages/typespec-client-generator-core/test/validations/types.test.ts b/packages/typespec-client-generator-core/test/validations/types.test.ts index 364638e824..cc206dc426 100644 --- a/packages/typespec-client-generator-core/test/validations/types.test.ts +++ b/packages/typespec-client-generator-core/test/validations/types.test.ts @@ -502,12 +502,15 @@ describe("cross-namespace duplicate name validation", () => { // Should report a single diagnostic because user's @clientName("ExtensionResource") // conflicts with ARM's ExtensionResource type - expectDiagnostics([...diagnostics, ...context.diagnostics], [ - { - code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", - message: 'Client name: "ExtensionResource" is duplicated in language scope: "AllScopes"', - }, - ]); + expectDiagnostics( + [...diagnostics, ...context.diagnostics], + [ + { + code: "@azure-tools/typespec-client-generator-core/duplicate-client-name", + message: 'Client name: "ExtensionResource" is duplicated in language scope: "AllScopes"', + }, + ], + ); }); });