diff --git a/.changeset/warm-flowers-burn.md b/.changeset/warm-flowers-burn.md new file mode 100644 index 000000000..91cde3efb --- /dev/null +++ b/.changeset/warm-flowers-burn.md @@ -0,0 +1,5 @@ +--- +'@hey-api/codegen-core': patch +--- + +**fix**: simplify symbol merging logic diff --git a/dev/openapi-ts.config.ts b/dev/openapi-ts.config.ts index c366f5414..504e47447 100644 --- a/dev/openapi-ts.config.ts +++ b/dev/openapi-ts.config.ts @@ -342,7 +342,7 @@ export default defineConfig(() => { // signature: 'object', // transformer: '@hey-api/transformers', // transformer: true, - validator: 'valibot', + // validator: 'valibot', // validator: { // request: 'zod', // response: 'zod', @@ -441,7 +441,7 @@ export default defineConfig(() => { // definitions: 'z{{name}}', exportFromIndex: true, // metadata: true, - name: 'valibot', + // name: 'valibot', // requests: { // case: 'PascalCase', // name: '{{name}}Data', diff --git a/packages/codegen-core/src/planner/planner.ts b/packages/codegen-core/src/planner/planner.ts index a5ee20320..e5645b5bb 100644 --- a/packages/codegen-core/src/planner/planner.ts +++ b/packages/codegen-core/src/planner/planner.ts @@ -358,6 +358,7 @@ export class Planner { private assignTopLevelName( args: Partial & { ctx: AnalysisContext; + debug?: boolean; node?: INode; symbol: Symbol; }, @@ -388,6 +389,7 @@ export class Planner { args: Pick, 'scope'> & Pick & { ctx: AnalysisContext; + debug?: boolean; /** The file the symbol belongs to. */ file: File; node?: INode; @@ -410,6 +412,7 @@ export class Planner { private assignSymbolName( args: AssignOptions & { ctx: AnalysisContext; + debug?: boolean; /** The file the symbol belongs to. */ file: File; node?: INode; diff --git a/packages/codegen-core/src/project/namespace.ts b/packages/codegen-core/src/project/namespace.ts index d6b1aaa1d..8a4ee8fd1 100644 --- a/packages/codegen-core/src/project/namespace.ts +++ b/packages/codegen-core/src/project/namespace.ts @@ -1,77 +1,37 @@ import type { SymbolKind } from '../symbols/types'; +const kindRank: Record = { + class: 3, + enum: 4, + function: 5, + interface: 1, + namespace: 0, + type: 2, + var: 6, +}; + /** * 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 { - // same-kind always valid for interfaces (merging) - if (a === 'interface' && b === 'interface') return true; - - // type vs interface merges - if ( - (a === 'interface' && b === 'type') || - (a === 'type' && b === 'interface') - ) { - return false; // TypeScript does NOT merge type-alias with interface. - } - - // type vs type = conflict - if (a === 'type' && b === 'type') return false; - - // interface vs class = allowed (declare-merge) - if ( - (a === 'interface' && b === 'class') || - (a === 'class' && b === 'interface') - ) { - return true; + // 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]) { + [a, b] = [b, a]; } - // enum vs namespace = allowed (merges into value+type) - if ( - (a === 'enum' && b === 'namespace') || - (a === 'namespace' && b === 'enum') - ) { - return true; + switch (a) { + case 'interface': + return b === 'class' || b === 'interface'; + case 'namespace': + return ( + b === 'class' || b === 'enum' || b === 'function' || b === 'namespace' + ); + case 'type': + // type can only merge with value-only declarations + return b === 'function' || b === 'var'; + default: + return false; } - - // class vs namespace = allowed - if ( - (a === 'class' && b === 'namespace') || - (a === 'namespace' && b === 'class') - ) { - return true; - } - - // namespace vs namespace = allowed (merging) - if (a === 'namespace' && b === 'namespace') return true; - - // enum vs enum = conflict IF values conflict (TypeScript flags duplicates) - if (a === 'enum' && b === 'enum') return false; - - // function and namespace merge (namespace can augment function) - if ( - (a === 'function' && b === 'namespace') || - (a === 'namespace' && b === 'function') - ) { - return true; - } - - // these collide with each other in the value namespace - const valueKinds = new Set(['class', 'enum', 'function', 'var']); - - const aInValue = valueKinds.has(a); - const bInValue = valueKinds.has(b); - - if (aInValue && bInValue) return false; - - // type-only declarations do not collide with value-only declarations - const typeKinds = new Set(['interface', 'type']); - const aInType = typeKinds.has(a); - const bInType = typeKinds.has(b); - - // if one is type-only and the other is value-only, they do NOT collide - if (aInType !== bInType) return true; - - return true; }