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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-client-generator-core"
---

Correctly detect name collisions with `--namespace` flag and or multi service usage
5 changes: 5 additions & 0 deletions packages/typespec-client-generator-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
TspLiteralType,
} from "./internal-utils.js";
import { createSdkPackage } from "./package.js";
import { validateNamespaceCollisions } from "./validations/types.js";

interface CreateTCGCContextOptions {
mutateNamespace?: boolean; // whether to mutate global namespace for versioning
Expand Down Expand Up @@ -217,6 +218,10 @@ export async function createSdkContext<
}
sdkContext.diagnostics = sdkContext.diagnostics.concat(diagnostics.diagnostics);

// 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);
}
Expand Down
88 changes: 88 additions & 0 deletions packages/typespec-client-generator-core/src/internal-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,3 +1122,91 @@ 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<typeof createDiagnosticCollector>,
): 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<string, Type[]>();

// 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<string, Type[]>,
): 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);
}
}
}
6 changes: 6 additions & 0 deletions packages/typespec-client-generator-core/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
filterApiVersionsWithDecorators,
getActualClientType,
getTypeDecorators,
validateCrossNamespaceNamesWithFlag,
} from "./internal-utils.js";
import { getLicenseInfo } from "./license.js";
import { getCrossLanguagePackageId, getNamespaceFromType } from "./public-utils.js";
Expand All @@ -32,6 +33,11 @@ export function createSdkPackage<TServiceOperation extends SdkServiceOperation>(
context: TCGCContext,
): [SdkPackage<TServiceOperation>, 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));
Expand Down
3 changes: 3 additions & 0 deletions packages/typespec-client-generator-core/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ export function $onValidate(program: Program) {
validateClients(tcgcContext);
validateMethods(tcgcContext);
validateHttp(tcgcContext);
// 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
}
Loading
Loading