From 8cad3d9841d758409bb638bce7c7041b106afef3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:26:43 +0000 Subject: [PATCH 1/5] Initial plan From c27fb4e60342119acfa924c8e480909a1ab2f3a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:50:38 +0000 Subject: [PATCH 2/5] Fix missing transformer function references - Add hasSymbolValue method to PluginInstance to check if a symbol has a value set - Use a Set to track schemas currently being processed to handle recursive schemas - Only generate calls to transformer functions that exist (have values) or are currently being processed - Fixes issue where schemas with no transformations were registered but never had values set, causing TypeScript errors when referenced Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../src/plugins/@hey-api/transformers/plugin.ts | 14 +++++++++++++- .../src/plugins/shared/utils/instance.ts | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts index 1299da64e..c2a6116e2 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -10,6 +10,9 @@ import type { HeyApiTransformersPlugin } from './types'; const dataVariableName = 'data'; +// Track schemas currently being processed to handle recursion +const processingSchemas = new Set(); + const ensureStatements = ( nodes: Array, ): Array => @@ -59,11 +62,15 @@ const processSchemaType = ({ }): Array => { if (schema.$ref) { const selector = plugin.api.selector('response-ref', schema.$ref); + const selectorKey = JSON.stringify(selector); if (!plugin.getSymbol(selector)) { // TODO: remove // create each schema response transformer only once + // Mark as currently processing to handle recursion + processingSchemas.add(selectorKey); + // Register symbol early to prevent infinite recursion with self-referential schemas const symbol = plugin.registerSymbol({ name: buildName({ @@ -83,6 +90,10 @@ const processSchemaType = ({ plugin, schema: refSchema, }); + + // Done processing + processingSchemas.delete(selectorKey); + if (nodes.length) { const node = tsc.constVariable({ expression: tsc.arrowFunction({ @@ -103,7 +114,8 @@ const processSchemaType = ({ } } - if (plugin.isSymbolRegistered(selector)) { + // Reference the symbol if it has a value OR is currently being processed (recursive case) + if (plugin.hasSymbolValue(selector) || processingSchemas.has(selectorKey)) { const ref = plugin.referenceSymbol(selector); const callExpression = tsc.callExpression({ functionName: ref.placeholder, diff --git a/packages/openapi-ts/src/plugins/shared/utils/instance.ts b/packages/openapi-ts/src/plugins/shared/utils/instance.ts index f3fffd15a..08cbd8cd7 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/instance.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/instance.ts @@ -297,6 +297,11 @@ export class PluginInstance { return this.gen.symbols.isRegistered(symbolIdOrSelector); } + hasSymbolValue(symbolIdOrSelector: number | Selector): boolean { + const symbol = this.getSymbol(symbolIdOrSelector); + return symbol ? this.gen.symbols.hasValue(symbol.id) : false; + } + referenceSymbol(symbolIdOrSelector: number | Selector): Symbol { return this.gen.symbols.reference(symbolIdOrSelector); } From 912187221f382357c7ce8a19d4b0549ed92df21d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:09:01 +0000 Subject: [PATCH 3/5] Address code review feedback: make processingSchemas instance-scoped - Move processingSchemas Set from global scope to handler function scope - Pass processingSchemas as parameter through the function call chain - This prevents potential race conditions if multiple plugin instances process schemas concurrently - Maintains the fix for missing transformer function references while improving thread-safety Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../src/plugins/@hey-api/transformers/plugin.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts index c2a6116e2..0e2500a4e 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -10,9 +10,6 @@ import type { HeyApiTransformersPlugin } from './types'; const dataVariableName = 'data'; -// Track schemas currently being processed to handle recursion -const processingSchemas = new Set(); - const ensureStatements = ( nodes: Array, ): Array => @@ -30,15 +27,18 @@ const isNodeReturnStatement = ({ const schemaResponseTransformerNodes = ({ plugin, + processingSchemas, schema, }: { plugin: HeyApiTransformersPlugin['Instance']; + processingSchemas: Set; schema: IR.SchemaObject; }): Array => { const identifierData = tsc.identifier({ text: dataVariableName }); const nodes = processSchemaType({ dataExpression: identifierData, plugin, + processingSchemas, schema, }); // append return statement if one does not already exist @@ -54,10 +54,12 @@ const schemaResponseTransformerNodes = ({ const processSchemaType = ({ dataExpression, plugin, + processingSchemas, schema, }: { dataExpression?: ts.Expression | string; plugin: HeyApiTransformersPlugin['Instance']; + processingSchemas: Set; schema: IR.SchemaObject; }): Array => { if (schema.$ref) { @@ -88,6 +90,7 @@ const processSchemaType = ({ ); const nodes = schemaResponseTransformerNodes({ plugin, + processingSchemas, schema: refSchema, }); @@ -157,6 +160,7 @@ const processSchemaType = ({ : processSchemaType({ dataExpression: 'item', plugin, + processingSchemas, schema: schema.items?.[0] ? schema.items[0] : { @@ -221,6 +225,7 @@ const processSchemaType = ({ const propertyNodes = processSchemaType({ dataExpression: propertyAccessExpression, plugin, + processingSchemas, schema: property, }); if (!propertyNodes.length) { @@ -257,6 +262,7 @@ const processSchemaType = ({ return processSchemaType({ dataExpression: 'item', plugin, + processingSchemas, schema: schema.items[0]!, }); } @@ -274,6 +280,7 @@ const processSchemaType = ({ const nodes = processSchemaType({ dataExpression: dataExpression || 'item', plugin, + processingSchemas, schema: item, }); if (nodes.length) { @@ -331,6 +338,9 @@ const processSchemaType = ({ // handles only response transformers for now export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { + // Track schemas currently being processed to handle recursion + const processingSchemas = new Set(); + plugin.forEach( 'operation', ({ operation }) => { @@ -355,6 +365,7 @@ export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { // TODO: parser - consider handling simple string response which is also a date const nodes = schemaResponseTransformerNodes({ plugin, + processingSchemas, schema: response, }); if (!nodes.length) return; From 4efe95c8e439a42ecb68a27bf86d5f336187ba46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:00:56 +0000 Subject: [PATCH 4/5] Fix: Always generate schema transformer functions The previous fix was incorrect - it prevented calling non-existent transformers, but the actual issue is that schema transformer functions should ALWAYS be generated, even when they have no transformation logic (they should just return data unchanged). This ensures that when operation transformers reference schema transformers, those functions always exist, preventing TypeScript errors like "Cannot find name 'chatResponseDtoSchemaResponseTransformer'". Fixes #2864 Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../plugins/@hey-api/transformers/plugin.ts | 65 +++++++------------ .../src/plugins/shared/utils/instance.ts | 5 -- 2 files changed, 23 insertions(+), 47 deletions(-) diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts index 0e2500a4e..5a3bc6331 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -27,18 +27,15 @@ const isNodeReturnStatement = ({ const schemaResponseTransformerNodes = ({ plugin, - processingSchemas, schema, }: { plugin: HeyApiTransformersPlugin['Instance']; - processingSchemas: Set; schema: IR.SchemaObject; }): Array => { const identifierData = tsc.identifier({ text: dataVariableName }); const nodes = processSchemaType({ dataExpression: identifierData, plugin, - processingSchemas, schema, }); // append return statement if one does not already exist @@ -54,25 +51,19 @@ const schemaResponseTransformerNodes = ({ const processSchemaType = ({ dataExpression, plugin, - processingSchemas, schema, }: { dataExpression?: ts.Expression | string; plugin: HeyApiTransformersPlugin['Instance']; - processingSchemas: Set; schema: IR.SchemaObject; }): Array => { if (schema.$ref) { const selector = plugin.api.selector('response-ref', schema.$ref); - const selectorKey = JSON.stringify(selector); if (!plugin.getSymbol(selector)) { // TODO: remove // create each schema response transformer only once - // Mark as currently processing to handle recursion - processingSchemas.add(selectorKey); - // Register symbol early to prevent infinite recursion with self-referential schemas const symbol = plugin.registerSymbol({ name: buildName({ @@ -90,35 +81,33 @@ const processSchemaType = ({ ); const nodes = schemaResponseTransformerNodes({ plugin, - processingSchemas, schema: refSchema, }); - - // Done processing - processingSchemas.delete(selectorKey); - - if (nodes.length) { - const node = tsc.constVariable({ - expression: tsc.arrowFunction({ - async: false, - multiLine: true, - parameters: [ - { - name: dataVariableName, - // TODO: parser - add types, generate types without transforms - type: tsc.keywordTypeNode({ keyword: 'any' }), - }, - ], - statements: ensureStatements(nodes), - }), - name: symbol.placeholder, - }); - plugin.setSymbolValue(symbol, node); - } + // Always create the transformer function, even if there are no transformations + // This ensures consistency and prevents missing function errors + const identifierData = tsc.identifier({ text: dataVariableName }); + const statements = nodes.length + ? ensureStatements(nodes) + : [tsc.returnStatement({ expression: identifierData })]; + const node = tsc.constVariable({ + expression: tsc.arrowFunction({ + async: false, + multiLine: true, + parameters: [ + { + name: dataVariableName, + // TODO: parser - add types, generate types without transforms + type: tsc.keywordTypeNode({ keyword: 'any' }), + }, + ], + statements, + }), + name: symbol.placeholder, + }); + plugin.setSymbolValue(symbol, node); } - // Reference the symbol if it has a value OR is currently being processed (recursive case) - if (plugin.hasSymbolValue(selector) || processingSchemas.has(selectorKey)) { + if (plugin.isSymbolRegistered(selector)) { const ref = plugin.referenceSymbol(selector); const callExpression = tsc.callExpression({ functionName: ref.placeholder, @@ -160,7 +149,6 @@ const processSchemaType = ({ : processSchemaType({ dataExpression: 'item', plugin, - processingSchemas, schema: schema.items?.[0] ? schema.items[0] : { @@ -225,7 +213,6 @@ const processSchemaType = ({ const propertyNodes = processSchemaType({ dataExpression: propertyAccessExpression, plugin, - processingSchemas, schema: property, }); if (!propertyNodes.length) { @@ -262,7 +249,6 @@ const processSchemaType = ({ return processSchemaType({ dataExpression: 'item', plugin, - processingSchemas, schema: schema.items[0]!, }); } @@ -280,7 +266,6 @@ const processSchemaType = ({ const nodes = processSchemaType({ dataExpression: dataExpression || 'item', plugin, - processingSchemas, schema: item, }); if (nodes.length) { @@ -338,9 +323,6 @@ const processSchemaType = ({ // handles only response transformers for now export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { - // Track schemas currently being processed to handle recursion - const processingSchemas = new Set(); - plugin.forEach( 'operation', ({ operation }) => { @@ -365,7 +347,6 @@ export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { // TODO: parser - consider handling simple string response which is also a date const nodes = schemaResponseTransformerNodes({ plugin, - processingSchemas, schema: response, }); if (!nodes.length) return; diff --git a/packages/openapi-ts/src/plugins/shared/utils/instance.ts b/packages/openapi-ts/src/plugins/shared/utils/instance.ts index 08cbd8cd7..f3fffd15a 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/instance.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/instance.ts @@ -297,11 +297,6 @@ export class PluginInstance { return this.gen.symbols.isRegistered(symbolIdOrSelector); } - hasSymbolValue(symbolIdOrSelector: number | Selector): boolean { - const symbol = this.getSymbol(symbolIdOrSelector); - return symbol ? this.gen.symbols.hasValue(symbol.id) : false; - } - referenceSymbol(symbolIdOrSelector: number | Selector): Symbol { return this.gen.symbols.reference(symbolIdOrSelector); } From 80035e79b2eb7cce587317cc4dfc9f9834fc5117 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:30:03 +0000 Subject: [PATCH 5/5] Fix: Only reference schema transformers that have values or are being processed Reverted the "always generate" approach and implemented the correct fix: - Added hasSymbolValue() method to check if a symbol has an emitted value - Track schemas being processed in a Set to handle recursive schemas - Only generate calls to transformer functions that exist (have values) or are currently being processed (recursive case) - Schemas without transformations won't have transformer functions generated, and won't be referenced This ensures: 1. No unnecessary pass-through functions are generated 2. Recursive/self-referential schemas still work correctly 3. Missing function errors are prevented Fixes #2864 Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../plugins/@hey-api/transformers/plugin.ts | 66 ++++++++++++------- .../src/plugins/shared/utils/instance.ts | 5 ++ 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts index 5a3bc6331..1844458ec 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -27,15 +27,18 @@ const isNodeReturnStatement = ({ const schemaResponseTransformerNodes = ({ plugin, + processingSchemas, schema, }: { plugin: HeyApiTransformersPlugin['Instance']; + processingSchemas: Set; schema: IR.SchemaObject; }): Array => { const identifierData = tsc.identifier({ text: dataVariableName }); const nodes = processSchemaType({ dataExpression: identifierData, plugin, + processingSchemas, schema, }); // append return statement if one does not already exist @@ -51,19 +54,25 @@ const schemaResponseTransformerNodes = ({ const processSchemaType = ({ dataExpression, plugin, + processingSchemas, schema, }: { dataExpression?: ts.Expression | string; plugin: HeyApiTransformersPlugin['Instance']; + processingSchemas: Set; schema: IR.SchemaObject; }): Array => { if (schema.$ref) { const selector = plugin.api.selector('response-ref', schema.$ref); + const selectorKey = JSON.stringify(selector); if (!plugin.getSymbol(selector)) { // TODO: remove // create each schema response transformer only once + // Mark as processing to handle recursion + processingSchemas.add(selectorKey); + // Register symbol early to prevent infinite recursion with self-referential schemas const symbol = plugin.registerSymbol({ name: buildName({ @@ -81,33 +90,36 @@ const processSchemaType = ({ ); const nodes = schemaResponseTransformerNodes({ plugin, + processingSchemas, schema: refSchema, }); - // Always create the transformer function, even if there are no transformations - // This ensures consistency and prevents missing function errors - const identifierData = tsc.identifier({ text: dataVariableName }); - const statements = nodes.length - ? ensureStatements(nodes) - : [tsc.returnStatement({ expression: identifierData })]; - const node = tsc.constVariable({ - expression: tsc.arrowFunction({ - async: false, - multiLine: true, - parameters: [ - { - name: dataVariableName, - // TODO: parser - add types, generate types without transforms - type: tsc.keywordTypeNode({ keyword: 'any' }), - }, - ], - statements, - }), - name: symbol.placeholder, - }); - plugin.setSymbolValue(symbol, node); + + // Done processing + processingSchemas.delete(selectorKey); + + if (nodes.length) { + const node = tsc.constVariable({ + expression: tsc.arrowFunction({ + async: false, + multiLine: true, + parameters: [ + { + name: dataVariableName, + // TODO: parser - add types, generate types without transforms + type: tsc.keywordTypeNode({ keyword: 'any' }), + }, + ], + statements: ensureStatements(nodes), + }), + name: symbol.placeholder, + }); + plugin.setSymbolValue(symbol, node); + } } - if (plugin.isSymbolRegistered(selector)) { + // Only reference the symbol if it has a value (transformer function was generated) + // OR is currently being processed (recursive case) + if (plugin.hasSymbolValue(selector) || processingSchemas.has(selectorKey)) { const ref = plugin.referenceSymbol(selector); const callExpression = tsc.callExpression({ functionName: ref.placeholder, @@ -149,6 +161,7 @@ const processSchemaType = ({ : processSchemaType({ dataExpression: 'item', plugin, + processingSchemas, schema: schema.items?.[0] ? schema.items[0] : { @@ -213,6 +226,7 @@ const processSchemaType = ({ const propertyNodes = processSchemaType({ dataExpression: propertyAccessExpression, plugin, + processingSchemas, schema: property, }); if (!propertyNodes.length) { @@ -249,6 +263,7 @@ const processSchemaType = ({ return processSchemaType({ dataExpression: 'item', plugin, + processingSchemas, schema: schema.items[0]!, }); } @@ -266,6 +281,7 @@ const processSchemaType = ({ const nodes = processSchemaType({ dataExpression: dataExpression || 'item', plugin, + processingSchemas, schema: item, }); if (nodes.length) { @@ -323,6 +339,9 @@ const processSchemaType = ({ // handles only response transformers for now export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { + // Track schemas currently being processed to handle recursion + const processingSchemas = new Set(); + plugin.forEach( 'operation', ({ operation }) => { @@ -347,6 +366,7 @@ export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { // TODO: parser - consider handling simple string response which is also a date const nodes = schemaResponseTransformerNodes({ plugin, + processingSchemas, schema: response, }); if (!nodes.length) return; diff --git a/packages/openapi-ts/src/plugins/shared/utils/instance.ts b/packages/openapi-ts/src/plugins/shared/utils/instance.ts index f3fffd15a..08cbd8cd7 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/instance.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/instance.ts @@ -297,6 +297,11 @@ export class PluginInstance { return this.gen.symbols.isRegistered(symbolIdOrSelector); } + hasSymbolValue(symbolIdOrSelector: number | Selector): boolean { + const symbol = this.getSymbol(symbolIdOrSelector); + return symbol ? this.gen.symbols.hasValue(symbol.id) : false; + } + referenceSymbol(symbolIdOrSelector: number | Selector): Symbol { return this.gen.symbols.reference(symbolIdOrSelector); }