···11111212const dataVariableName = 'data';
13131414+// Track symbols that are currently being built so recursive references
1515+// can emit calls to transformers that will be implemented later.
1616+const buildingSymbols = new Set<number>();
1717+1418const ensureStatements = (
1519 nodes: Array<ts.Expression | ts.Statement>,
1620): Array<ts.Statement> =>
···6569 resourceId: schema.$ref,
6670 };
67716868- if (!plugin.getSymbol(query)) {
6969- // TODO: remove
7070- // create each schema response transformer only once
7272+ let symbol = plugin.getSymbol(query);
71737272- // Register symbol early to prevent infinite recursion with self-referential schemas
7373- const symbol = plugin.registerSymbol({
7474+ if (!symbol) {
7575+ // Register a placeholder symbol immediately and set its value to null
7676+ // as a stop token to prevent infinite recursion for self-referential
7777+ // schemas. We also mark this symbol as "building" so that nested
7878+ // references to it can emit calls that will be implemented later.
7979+ symbol = plugin.registerSymbol({
7480 meta: query,
7581 name: buildName({
7682 config: {
···8086 name: refToName(schema.$ref),
8187 }),
8288 });
8989+ plugin.setSymbolValue(symbol, null);
9090+ }
83918484- const refSchema = plugin.context.resolveIrRef<IR.SchemaObject>(
8585- schema.$ref,
8686- );
8787- const nodes = schemaResponseTransformerNodes({
8888- plugin,
8989- schema: refSchema,
9090- });
9191- if (nodes.length) {
9292- const node = tsc.constVariable({
9393- expression: tsc.arrowFunction({
9494- async: false,
9595- multiLine: true,
9696- parameters: [
9797- {
9898- name: dataVariableName,
9999- // TODO: parser - add types, generate types without transforms
100100- type: tsc.keywordTypeNode({ keyword: 'any' }),
101101- },
102102- ],
103103- statements: ensureStatements(nodes),
104104- }),
105105- name: symbol.placeholder,
9292+ // Only compute the implementation if the symbol isn't already being built.
9393+ // This prevents infinite recursion on self-referential schemas. We still
9494+ // allow emitting a call when the symbol is currently being built so
9595+ // parent nodes can reference the transformer that will be emitted later.
9696+ const existingValue = plugin.gen.symbols.getValue(symbol.id);
9797+ if (!existingValue && !buildingSymbols.has(symbol.id)) {
9898+ buildingSymbols.add(symbol.id);
9999+ try {
100100+ const refSchema = plugin.context.resolveIrRef<IR.SchemaObject>(
101101+ schema.$ref,
102102+ );
103103+ const nodes = schemaResponseTransformerNodes({
104104+ plugin,
105105+ schema: refSchema,
106106 });
107107- plugin.setSymbolValue(symbol, node);
107107+108108+ if (nodes.length) {
109109+ const node = tsc.constVariable({
110110+ expression: tsc.arrowFunction({
111111+ async: false,
112112+ multiLine: true,
113113+ parameters: [
114114+ {
115115+ name: dataVariableName,
116116+ // TODO: parser - add types, generate types without transforms
117117+ type: tsc.keywordTypeNode({ keyword: 'any' }),
118118+ },
119119+ ],
120120+ statements: ensureStatements(nodes),
121121+ }),
122122+ name: symbol.placeholder,
123123+ });
124124+ plugin.setSymbolValue(symbol, node);
125125+ }
126126+ } finally {
127127+ buildingSymbols.delete(symbol.id);
108128 }
109129 }
110130111111- if (plugin.isSymbolRegistered(query)) {
131131+ // Only emit a call if the symbol has a value (implementation) OR the
132132+ // symbol is currently being built (recursive reference) — in the
133133+ // latter case we allow emitting a call that will be implemented later.
134134+ const currentValue = plugin.gen.symbols.getValue(symbol.id);
135135+ if (currentValue || buildingSymbols.has(symbol.id)) {
112136 const ref = plugin.referenceSymbol(query);
113137 const callExpression = tsc.callExpression({
114138 functionName: ref.placeholder,