fix(tools): detect render-phase cycles and fix class-name JSDoc placement
Address ds-review-bot v5/v6 review round 6: - renderType tracks the active ancestor schemas by object identity (the frame stack is the DFS path). A stateful getter can mutate the graph after validation so a child returns an ancestor at render time; without this the walk pushed frames forever instead of degrading. A repeated ancestor now degrades to Any, honoring the never-throw contract; distinct nodes in a legitimately deep chain are different objects, so it stays O(1) per push and O(depth) memory. - The multiline allocateClassName JSDoc was still attached to the MAX_CLASS_NAME_BASE constant (a self-referential @link, and the function had no doc). Move the doc onto the function and give the constant its own one-liner. - Tests cover the post-validation cycle and a non-object render-time child; py-types.ts stays at 100% per-file coverage.
This commit is contained in:
@@ -144,6 +144,43 @@ describe('jsonSchemaToPy', () => {
|
||||
expect(text).toContain('class FooArgsPhase3(TypedDict):')
|
||||
})
|
||||
|
||||
it('degrades to Any instead of looping when a stateful getter introduces a cycle after validation', () => {
|
||||
// `items` validates as a scalar, then returns the root schema at render
|
||||
// time — a cycle a post-validation mutation introduced. The walk must
|
||||
// degrade to Any rather than push frames forever.
|
||||
let itemReads = 0
|
||||
const root: Record<string, unknown> = { type: 'array' }
|
||||
Object.defineProperty(root, 'items', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
itemReads += 1
|
||||
return itemReads <= 1 ? { type: 'string' } : root
|
||||
},
|
||||
})
|
||||
let out: string | undefined
|
||||
expect(() => { out = jsonSchemaToPy(root) }).not.toThrow()
|
||||
// list[...] of a self-cycle: the inner cycle degrades to Any.
|
||||
expect(out).toBe('list[Any]')
|
||||
})
|
||||
|
||||
it('degrades to Any when a stateful getter returns a non-object child at render time', () => {
|
||||
// `items` validates as a scalar node, then returns a bare string (a
|
||||
// non-object) at render. The walk must handle a non-object child without
|
||||
// tracking identity and degrade it, not throw.
|
||||
let itemReads = 0
|
||||
const root: Record<string, unknown> = { type: 'array' }
|
||||
Object.defineProperty(root, 'items', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
itemReads += 1
|
||||
return itemReads <= 1 ? { type: 'string' } : 'not-a-schema-object'
|
||||
},
|
||||
})
|
||||
let out: string | undefined
|
||||
expect(() => { out = jsonSchemaToPy(root) }).not.toThrow()
|
||||
expect(out).toBe('list[Any]')
|
||||
})
|
||||
|
||||
it('emits exact digits for a beyond-safe-range integer literal', () => {
|
||||
// Python integers are arbitrary-precision, so the emitted digits ARE the
|
||||
// value the model programs against. `String(2 ** 60)` prints the rounded
|
||||
|
||||
Reference in New Issue
Block a user