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/few-seas-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/codegen-core": patch
---

**symbol**: add `children` and `override` property
5 changes: 5 additions & 0 deletions .changeset/long-dolls-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": patch
---

**dsl**: track extended class symbols
5 changes: 5 additions & 0 deletions .changeset/twenty-forks-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/codegen-core": patch
---

**symbols**: export `pythonNameConflictResolver`, `SymbolChild`, `SymbolKind`, and `SymbolRegistry`
4 changes: 4 additions & 0 deletions packages/codegen-core/src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const constExports = [
'Logger',
'mergeConfigs',
'nodeBrand',
'pythonNameConflictResolver',
'Project',
'ref',
'refs',
Expand Down Expand Up @@ -65,9 +66,12 @@ export type _TypeExports = [
index.StructureShell,
index.StructureShellResult,
index.Symbol,
index.SymbolChild,
index.SymbolIdentifier,
index.SymbolIn,
index.SymbolKind,
index.SymbolMeta,
index.SymbolRegistry,
];

describe('index exports', () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/codegen-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export type {
NodeScope,
} from './nodes/node';
export type { IOutput as Output } from './output';
export { simpleNameConflictResolver, underscoreNameConflictResolver } from './planner/resolvers';
export {
pythonNameConflictResolver,
simpleNameConflictResolver,
underscoreNameConflictResolver,
} from './planner/resolvers';
export type { IAnalysisContext as AnalysisContext, NameConflictResolver } from './planner/types';
export { Project } from './project/project';
export type { IProject } from './project/types';
Expand All @@ -48,6 +52,9 @@ export type {
export { Symbol } from './symbols/symbol';
export type {
BindingKind,
ISymbolChild as SymbolChild,
ISymbolIdentifier as SymbolIdentifier,
ISymbolIn as SymbolIn,
SymbolKind,
ISymbolRegistry as SymbolRegistry,
} from './symbols/types';
4 changes: 2 additions & 2 deletions packages/codegen-core/src/languages/resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { underscoreNameConflictResolver } from '../planner/resolvers';
import { pythonNameConflictResolver, underscoreNameConflictResolver } from '../planner/resolvers';
import type { NameConflictResolvers } from './types';

export const defaultNameConflictResolvers: NameConflictResolvers = {
php: underscoreNameConflictResolver,
python: underscoreNameConflictResolver,
python: pythonNameConflictResolver,
ruby: underscoreNameConflictResolver,
};
14 changes: 13 additions & 1 deletion packages/codegen-core/src/planner/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fromRef, isRef, ref } from '../refs/refs';
import type { Ref } from '../refs/types';
import type { Symbol } from '../symbols/symbol';
import type { NameScopes, Scope } from './scope';
import { createScope } from './scope';
import { createScope, registerChildName } from './scope';
import type { IAnalysisContext, Input } from './types';

export class AnalysisContext implements IAnalysisContext {
Expand Down Expand Up @@ -74,6 +74,18 @@ export class AnalysisContext implements IAnalysisContext {
}
}

injectChildren(input: Input): void {
const value = isRef(input) ? input : ref(input);
const symbol = isSymbolRef(value) ? fromRef(value) : undefined;
if (!symbol) return;

for (const child of symbol.children) {
if (!child.overridable) {
registerChildName(this.scope, child.name, child.kind);
}
}
}

localNames(scope: Scope): NameScopes {
const names: NameScopes = new Map();
for (const [name, kinds] of scope.localNames) {
Expand Down
46 changes: 24 additions & 22 deletions packages/codegen-core/src/planner/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { SymbolKind } from '../symbols/types';
import type { AnalysisContext } from './analyzer';
import { Analyzer } from './analyzer';
import type { AssignOptions, Scope } from './scope';
import { createScope } from './scope';
import { createScope, registerName } from './scope';

const isTypeOnlyKind = (kind: SymbolKind) => kind === 'type' || kind === 'interface';

Expand Down Expand Up @@ -418,7 +418,13 @@ export class Planner {
while (true) {
const language = node?.language || symbol.node?.language || file.language;

const ok = this.nameIsAvailable({ kind: symbol.kind, language, name: finalName, scope });
const ok = this.nameIsAvailable({
kind: symbol.kind,
language,
name: finalName,
override: symbol.override,
scope,
});
if (ok) break;

const resolver =
Expand All @@ -440,7 +446,7 @@ export class Planner {
this.cacheResolvedNames.add(symbol.id);
const updateScopes = [scope, ...scopesToUpdate];
for (const scope of updateScopes) {
this.updateScope(symbol, scope);
registerName(scope, symbol.finalName, symbol.kind);
}
}

Expand All @@ -455,37 +461,33 @@ export class Planner {
kind,
language,
name,
override,
scope,
}: {
kind: SymbolKind;
language: string | undefined;
name: string;
override?: boolean;
scope: Scope;
}): boolean {
function conflicts(kinds: Set<SymbolKind> | undefined): boolean {
if (!kinds) return false;
for (const existingKind of kinds) {
if (!canDeclarationsShareIdentifier(language, kind, existingKind)) {
return true;
}
}
return false;
}

let current: Scope | undefined = scope;
while (current) {
const kinds = current.localNames.get(name);
if (kinds) {
for (const existingKind of kinds) {
if (!canDeclarationsShareIdentifier(language, kind, existingKind)) {
return false;
}
}
if (!override && conflicts(current.childNames.get(name))) {
return false;
}
if (conflicts(current.localNames.get(name))) return false;
current = current.parent;
}
return true;
}

/**
* Updates the provided name scope with the symbol's final name and kind.
*
* Ensures the name scope tracks all kinds associated with a given name.
*/
private updateScope(symbol: Symbol, scope: Scope): void {
const name = symbol.finalName;
const cache = scope.localNames.get(name) ?? new Set();
cache.add(symbol.kind);
scope.localNames.set(name, cache);
}
}
7 changes: 5 additions & 2 deletions packages/codegen-core/src/planner/resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { NameConflictResolver } from './types';

export const pythonNameConflictResolver: NameConflictResolver = ({ attempt, baseName }) =>
attempt === 1 ? `${baseName}_` : `${baseName}_${attempt}`;

export const simpleNameConflictResolver: NameConflictResolver = ({ attempt, baseName }) =>
attempt === 0 ? baseName : `${baseName}${attempt + 1}`;
`${baseName}${attempt + 1}`;

export const underscoreNameConflictResolver: NameConflictResolver = ({ attempt, baseName }) =>
attempt === 0 ? baseName : `${baseName}_${attempt + 1}`;
`${baseName}_${attempt + 1}`;
38 changes: 26 additions & 12 deletions packages/codegen-core/src/planner/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import type { SymbolKind } from '../symbols/types';
export type NameScopes = Map<string, Set<SymbolKind>>;

export type Scope = {
/** Soft conflicts, inherited names from children symbols. */
childNames: NameScopes;
/** Child scopes. */
children: Array<Scope>;
/** Resolved names in this scope. */
/** Hard conflicts, declared names in this scope. */
localNames: NameScopes;
/** Parent scope, if any. */
parent?: Scope;
Expand All @@ -22,14 +24,26 @@ export type AssignOptions = {
scopesToUpdate: ReadonlyArray<Scope>;
};

export const createScope = (
args: {
localNames?: NameScopes;
parent?: Scope;
} = {},
): Scope => ({
children: [],
localNames: args.localNames || new Map(),
parent: args.parent,
symbols: [],
});
export function createScope(
args: Pick<Partial<Scope>, 'childNames' | 'localNames' | 'parent'> = {},
): Scope {
return {
childNames: args.childNames || new Map(),
children: [],
localNames: args.localNames || new Map(),
parent: args.parent,
symbols: [],
};
}

export function registerName(scope: Scope, name: string, kind: SymbolKind): void {
const kinds = scope.localNames.get(name) ?? new Set();
kinds.add(kind);
scope.localNames.set(name, kinds);
}

export function registerChildName(scope: Scope, name: string, kind: SymbolKind): void {
const kinds = scope.childNames.get(name) ?? new Set();
kinds.add(kind);
scope.childNames.set(name, kinds);
}
9 changes: 8 additions & 1 deletion packages/codegen-core/src/planner/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import type { NameScopes, Scope } from './scope';

export type Input = Ref<object> | object | string | number | undefined;

export type NameConflictResolver = (args: { attempt: number; baseName: string }) => string | null;
export type NameConflictResolver = (args: {
/** The current attempt number for generating a unique name. Starts at 1. */
attempt: number;
/** The base name to resolve conflicts for. */
baseName: string;
}) => string | null;

export interface IAnalysisContext {
/** Register a dependency on another symbol. */
addDependency(symbol: Ref<Symbol>): void;
/** Register a dependency on another symbol or analyze further. */
analyze(input: Input): void;
/** Inject children symbols into the current scope. */
injectChildren(input: Input): void;
/** Get local names in the current scope. */
localNames(scope: Scope): NameScopes;
/** Pop the current local scope. */
Expand Down
50 changes: 49 additions & 1 deletion packages/codegen-core/src/symbols/symbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ISymbolMeta } from '../extensions';
import type { File } from '../files/file';
import { log } from '../log';
import type { INode } from '../nodes/node';
import type { BindingKind, ISymbolIn, SymbolKind } from './types';
import type { BindingKind, ISymbolChild, ISymbolIn, SymbolKind } from './types';

export class Symbol<Node extends INode = INode> {
/**
Expand All @@ -14,6 +14,12 @@ export class Symbol<Node extends INode = INode> {
* should defer to the canonical symbol.
*/
private _canonical?: Symbol;
/**
* Child symbol bindings scoped under this symbol.
*
* @default []
*/
private _children: Array<ISymbolChild>;
/**
* True if this symbol is exported from its defining file.
*
Expand Down Expand Up @@ -77,13 +83,20 @@ export class Symbol<Node extends INode = INode> {
* Node that defines this symbol.
*/
private _node?: Node;
/**
* Indicates whether this symbol overrides another declaration.
*
* @default false
*/
private _override: boolean;

/** Brand used for identifying symbols. */
readonly '~brand' = symbolBrand;
/** Globally unique, stable symbol ID. */
readonly id: number;

constructor(input: ISymbolIn, id: number) {
this._children = input.children ?? [];
this._exported = input.exported ?? false;
this._external = input.external;
this._getExportFromFilePath = input.getExportFromFilePath;
Expand All @@ -93,6 +106,7 @@ export class Symbol<Node extends INode = INode> {
this._kind = input.kind ?? 'var';
this._meta = input.meta;
this._name = input.name;
this._override = input.override ?? false;
}

/**
Expand All @@ -106,6 +120,13 @@ export class Symbol<Node extends INode = INode> {
return this._canonical ?? this;
}

/**
* Read-only accessor for child symbol bindings.
*/
get children(): ReadonlyArray<ISymbolChild> {
return this.canonical._children;
}

/**
* Indicates whether this symbol is exported from its defining file.
*/
Expand Down Expand Up @@ -197,6 +218,13 @@ export class Symbol<Node extends INode = INode> {
return this.canonical._node as Node | undefined;
}

/**
* Indicates whether this symbol is marked as an override.
*/
get override(): boolean {
return this.canonical._override;
}

/**
* Marks this symbol as a stub and assigns its canonical symbol.
*
Expand All @@ -209,6 +237,16 @@ export class Symbol<Node extends INode = INode> {
this._canonical = symbol;
}

/**
* Assigns the child symbol bindings associated with this symbol.
*
* @param children — Child bindings to associate with the symbol.
*/
setChildren(children: Array<ISymbolChild>): void {
this.assertCanonical();
this._children = children;
}

/**
* Marks the symbol as exported from its file.
*
Expand Down Expand Up @@ -297,6 +335,16 @@ export class Symbol<Node extends INode = INode> {
node.symbol = this;
}

/**
* Marks whether this symbol should be treated as an override.
*
* @param override — Override marker value.
*/
setOverride(override: boolean): void {
this.assertCanonical();
this._override = override;
}

/**
* Returns a debug‑friendly string representation identifying the symbol.
*/
Expand Down
Loading
Loading