diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts index 22e459bc3e..6e69541fb9 100644 --- a/packages/core/tools/src/py-types.ts +++ b/packages/core/tools/src/py-types.ts @@ -259,7 +259,17 @@ function renderType(schema: unknown, className: string, state: RenderState): str continue } if (frame.kind === 'oneOf') { - finish(frame.childTypes.join(' | ')) + // Concatenate with `+` (not `Array.join`): V8 builds a lazy + // ConsString, so a deep oneOf chain materializes once at the root + // instead of re-materializing the accumulated string at every level + // (which `join` would, making it Θ(depth²)). This matches the array + // arm's template-literal laziness and ts-types' composable-document + // approach — the whole walk stays linear in schema depth. + let union = '' + for (const [index, childType] of frame.childTypes.entries()) { + union = index === 0 ? childType : `${union} | ${childType}` + } + finish(union) continue } /* jscpd:ignore-end */ diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts index 89db13d852..aa0bc30cff 100644 --- a/packages/core/tools/tests/py-types.spec.ts +++ b/packages/core/tools/tests/py-types.spec.ts @@ -469,6 +469,20 @@ describe('renderToolsSdkPy', () => { expect(type.length).toBe('list['.length * 20000 + 'str'.length + ']'.repeat(20000).length) }) + it('renders a deeply nested oneOf chain in linear time (no per-level re-materialization)', () => { + // Each level is a two-branch oneOf whose first branch recurses; joining the + // accumulated union string at every level would be Theta(depth^2). The `+` + // (ConsString) concatenation keeps it linear, like the array arm. + const depth = 20000 + let deep: Record = { type: 'string' } + for (let i = 0; i < depth; i++) deep = { oneOf: [deep, { type: 'null' }] } + const type = jsonSchemaToPy(deep) + // depth levels of ` | None` appended to the innermost `str`. + expect(type.startsWith('str | None')).toBe(true) + expect(type.endsWith(' | None')).toBe(true) + expect(type.length).toBe('str'.length + ' | None'.length * depth) + }) + it('emits pass for a subscript-only tool set (comments are not statements)', () => { const t: ToolSdkSchema = { name: 'my-exotic.tool',