Skip to content

Commit c19b445

Browse files
committed
fix: resolve name collisions
Ticket: DX-2854 This commit resolves name collisions by appending a collision number to the component name
1 parent 5b0dafb commit c19b445

8 files changed

Lines changed: 119 additions & 9 deletions

File tree

packages/openapi-generator/src/cli.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,35 @@ const app = command({
157157
...Object.values(route.response),
158158
];
159159
});
160+
const componentLocations: Record<string, string> = {};
161+
const componentCollisionCounters: Record<string, number> = {};
162+
163+
// Helper to generate unique component names (name, name1, name2, ...)
164+
const getUniqueComponentName = (name: string): string => {
165+
if (components[name] === undefined) {
166+
return name;
167+
}
168+
const counter = (componentCollisionCounters[name] ?? 0) + 1;
169+
componentCollisionCounters[name] = counter;
170+
return `${name}${counter}`;
171+
};
172+
160173
let schema: Schema | undefined;
161174
while (((schema = queue.pop()), schema !== undefined)) {
162175
const refs = getRefs(schema, project.getTypes());
163176
for (const ref of refs) {
164-
if (components[ref.name] !== undefined) {
177+
if (
178+
components[ref.name] !== undefined &&
179+
componentLocations[ref.name] === ref.location
180+
) {
165181
continue;
166182
}
183+
184+
const componentName =
185+
components[ref.name] !== undefined
186+
? getUniqueComponentName(ref.name)
187+
: ref.name;
188+
167189
const sourceFile = project.get(ref.location);
168190
if (sourceFile === undefined) {
169191
logError(`Could not find '${ref.name}' from '${ref.location}'`);
@@ -217,7 +239,8 @@ const app = command({
217239
codecE.right.comment = comment;
218240
}
219241

220-
components[ref.name] = codecE.right;
242+
components[componentName] = codecE.right;
243+
componentLocations[componentName] = ref.location;
221244
queue.push(codecE.right);
222245
}
223246
}

packages/openapi-generator/src/project.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,39 @@ export class Project {
1616
private processedFiles: Record<string, SourceFile>;
1717
private pendingFiles: Set<string>;
1818
private types: Record<string, string>;
19+
private typeCollisionCounters: Record<string, number>;
1920
private visitedPackages: Set<string>;
2021

2122
constructor(files: Record<string, SourceFile> = {}, knownImports = KNOWN_IMPORTS) {
2223
this.processedFiles = files;
2324
this.pendingFiles = new Set();
2425
this.knownImports = knownImports;
2526
this.types = {};
27+
this.typeCollisionCounters = {};
2628
this.visitedPackages = new Set();
2729
}
2830

2931
add(path: string, sourceFile: SourceFile): void {
3032
this.processedFiles[path] = sourceFile;
3133
this.pendingFiles.delete(path);
3234

33-
// Update types mapping
35+
// Update types mapping with collision handling
3436
for (const exp of sourceFile.symbols.exports) {
35-
this.types[exp.exportedName] = path;
37+
const name = this.getUniqueTypeName(exp.exportedName);
38+
this.types[name] = path;
3639
}
3740
}
3841

42+
private getUniqueTypeName(name: string): string {
43+
if (this.types[name] === undefined) {
44+
return name;
45+
}
46+
47+
const counter = (this.typeCollisionCounters[name] ?? 0) + 1;
48+
this.typeCollisionCounters[name] = counter;
49+
return `${name}${counter}`;
50+
}
51+
3952
get(path: string): SourceFile | undefined {
4053
return this.processedFiles[path];
4154
}
@@ -62,11 +75,6 @@ export class Project {
6275
continue;
6376
}
6477

65-
// map types to their file path
66-
for (const exp of sourceFile.symbols.exports) {
67-
this.types[exp.exportedName] = path;
68-
}
69-
7078
this.add(path, sourceFile);
7179

7280
// Process imports

packages/openapi-generator/test/externalModule.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,43 @@ testCase(
199199
{},
200200
{},
201201
);
202+
203+
test('handles name collisions with numbered suffixes (name, name1, name2, ...)', async () => {
204+
const project = new Project({}, KNOWN_IMPORTS);
205+
const entryPointPath = p.resolve('test/sample-types/nameCollision.ts');
206+
await project.parseEntryPoint(entryPointPath);
207+
208+
const types = project.getTypes();
209+
210+
const pkgAPath = p.resolve('test/sample-types/node_modules/@test/pkg-a/src/index.ts');
211+
const pkgBPath = p.resolve('test/sample-types/node_modules/@test/pkg-b/src/index.ts');
212+
213+
const sharedTypePath = types['SharedType'];
214+
assert.ok(
215+
sharedTypePath === pkgAPath || sharedTypePath === pkgBPath,
216+
'SharedType should map to whichever package was processed first',
217+
);
218+
219+
const sharedType1Path = types['SharedType1'];
220+
assert.ok(
221+
sharedType1Path === pkgAPath || sharedType1Path === pkgBPath,
222+
'SharedType1 should map to the other package',
223+
);
224+
225+
// They should be different
226+
assert.notEqual(
227+
sharedTypePath,
228+
sharedType1Path,
229+
'SharedType and SharedType1 should map to different packages',
230+
);
231+
232+
// SharedTypeCodec should be mapped normally
233+
assert.equal(types['SharedTypeCodec'], pkgBPath);
234+
235+
// Verify both files were parsed correctly
236+
const pkgAFile = project.get(pkgAPath);
237+
const pkgBFile = project.get(pkgBPath);
238+
239+
assert.notEqual(pkgAFile, undefined, 'pkg-a file should be parsed');
240+
assert.notEqual(pkgBFile, undefined, 'pkg-b file should be parsed');
241+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as t from 'io-ts';
2+
import { SharedType } from '@test/pkg-a';
3+
import { SharedTypeCodec } from '@test/pkg-b';
4+
5+
// This file uses SharedType from pkg-a
6+
// and SharedTypeCodec from pkg-b (which internally has its own SharedType)
7+
export const MyCodec = t.type({
8+
state: SharedType,
9+
otherState: SharedTypeCodec,
10+
});

packages/openapi-generator/test/sample-types/node_modules/@test/pkg-a/package.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/openapi-generator/test/sample-types/node_modules/@test/pkg-a/src/index.ts

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/openapi-generator/test/sample-types/node_modules/@test/pkg-b/package.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/openapi-generator/test/sample-types/node_modules/@test/pkg-b/src/index.ts

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)