Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/warm-flowers-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/codegen-core': patch
---

**fix**: simplify symbol merging logic
4 changes: 2 additions & 2 deletions dev/openapi-ts.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ export default defineConfig(() => {
// signature: 'object',
// transformer: '@hey-api/transformers',
// transformer: true,
validator: 'valibot',
// validator: 'valibot',
// validator: {
// request: 'zod',
// response: 'zod',
Expand Down Expand Up @@ -441,7 +441,7 @@ export default defineConfig(() => {
// definitions: 'z{{name}}',
exportFromIndex: true,
// metadata: true,
name: 'valibot',
// name: 'valibot',
// requests: {
// case: 'PascalCase',
// name: '{{name}}Data',
Expand Down
3 changes: 3 additions & 0 deletions packages/codegen-core/src/planner/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ export class Planner {
private assignTopLevelName(
args: Partial<AssignOptions> & {
ctx: AnalysisContext;
debug?: boolean;
node?: INode;
symbol: Symbol;
},
Expand Down Expand Up @@ -388,6 +389,7 @@ export class Planner {
args: Pick<Partial<AssignOptions>, 'scope'> &
Pick<AssignOptions, 'scopesToUpdate'> & {
ctx: AnalysisContext;
debug?: boolean;
/** The file the symbol belongs to. */
file: File;
node?: INode;
Expand All @@ -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;
Expand Down
92 changes: 26 additions & 66 deletions packages/codegen-core/src/project/namespace.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,37 @@
import type { SymbolKind } from '../symbols/types';

const kindRank: Record<SymbolKind, number> = {
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<SymbolKind>(['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<SymbolKind>(['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;
}
Loading