diff --git a/.changeset/stale-spiders-sleep.md b/.changeset/stale-spiders-sleep.md new file mode 100644 index 000000000..95894ba2e --- /dev/null +++ b/.changeset/stale-spiders-sleep.md @@ -0,0 +1,5 @@ +--- +"@hey-api/codegen-core": patch +--- + +**planner**: language-aware declaration sharing check diff --git a/packages/codegen-core/src/__tests__/project.test.ts b/packages/codegen-core/src/__tests__/project.test.ts index 0d9d3c6be..b7dc6c11f 100644 --- a/packages/codegen-core/src/__tests__/project.test.ts +++ b/packages/codegen-core/src/__tests__/project.test.ts @@ -1,4 +1,6 @@ +import { canDeclarationsShareIdentifier } from '../project/namespace'; import { Project } from '../project/project'; +import type { SymbolKind } from '../symbols/types'; // Mock Planner so we control what files appear in project.files vi.mock('../planner/planner', () => { @@ -54,3 +56,44 @@ describe('Project', () => { }); }); }); + +describe('canDeclarationsShareIdentifier', () => { + const kinds: ReadonlyArray = [ + 'class', + 'enum', + 'function', + 'interface', + 'namespace', + 'type', + 'var', + ]; + + it('matches TypeScript declaration merging matrix', () => { + const allowed = new Set([ + 'interface|interface', + 'class|interface', + 'class|namespace', + 'enum|namespace', + 'function|namespace', + 'namespace|namespace', + 'function|type', + 'type|var', + ]); + + for (const a of kinds) { + for (const b of kinds) { + expect(canDeclarationsShareIdentifier('typescript', a, b)).toBe( + allowed.has([a, b].sort().join('|')), + ); + } + } + }); + + it('returns false for Python declaration pairs', () => { + for (const a of kinds) { + for (const b of kinds) { + expect(canDeclarationsShareIdentifier('python', a, b)).toBe(false); + } + } + }); +}); diff --git a/packages/codegen-core/src/planner/planner.ts b/packages/codegen-core/src/planner/planner.ts index 452ad9508..b99badc7c 100644 --- a/packages/codegen-core/src/planner/planner.ts +++ b/packages/codegen-core/src/planner/planner.ts @@ -4,7 +4,7 @@ import type { ExportModule, ImportModule } from '../bindings'; import type { IProjectRenderMeta } from '../extensions'; import type { File } from '../files/file'; import type { INode } from '../nodes/node'; -import { canShareName } from '../project/namespace'; +import { canDeclarationsShareIdentifier } from '../project/namespace'; import type { IProject } from '../project/types'; import { fromRef } from '../refs/refs'; import type { RenderContext } from '../renderer'; @@ -417,13 +417,13 @@ export class Planner { const localNames = ctx.localNames(scope); while (true) { + const language = node?.language || symbol.node?.language || file.language; + const kinds = [...(localNames.get(finalName) ?? [])]; - // TODO: adjust canShareName for language - const ok = kinds.every((kind) => canShareName(symbol.kind, kind)); + const ok = kinds.every((kind) => canDeclarationsShareIdentifier(language, symbol.kind, kind)); if (ok) break; - const language = node?.language || symbol.node?.language || file.language; const resolver = (language ? this.project.nameConflictResolvers[language] : undefined) ?? this.project.defaultNameConflictResolver; diff --git a/packages/codegen-core/src/project/namespace.ts b/packages/codegen-core/src/project/namespace.ts index 209b8979f..a38c8cee3 100644 --- a/packages/codegen-core/src/project/namespace.ts +++ b/packages/codegen-core/src/project/namespace.ts @@ -1,6 +1,7 @@ +import type { Language } from '../languages/types'; import type { SymbolKind } from '../symbols/types'; -const kindRank: Record = { +const typescriptMergeKindRank: Record = { class: 3, enum: 4, function: 5, @@ -14,10 +15,10 @@ const kindRank: Record = { * Returns true if two declarations of given kinds * are allowed to share the same identifier in TypeScript. */ -export function canShareName(a: SymbolKind, b: SymbolKind): boolean { +function canTypeScriptDeclarationsShareIdentifier(a: SymbolKind, b: SymbolKind): boolean { // sort based on TypeScript merge precedence so `a` is always the weaker merge candidate // ensures that asymmetric merges like `type + var` are correctly handled - if (kindRank[a] > kindRank[b]) { + if (typescriptMergeKindRank[a] > typescriptMergeKindRank[b]) { [a, b] = [b, a]; } @@ -33,3 +34,15 @@ export function canShareName(a: SymbolKind, b: SymbolKind): boolean { return false; } } + +export function canDeclarationsShareIdentifier( + language: Language | undefined, + a: SymbolKind, + b: SymbolKind, +): boolean { + if (language === 'typescript') { + return canTypeScriptDeclarationsShareIdentifier(a, b); + } + + return false; +}