Skip to content

Commit 0d5f9d6

Browse files
hi-ogawaclaude
andauthored
fix(pretty-format): limit output for large object (#9949)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7b60543 commit 0d5f9d6

File tree

5 files changed

+231
-17
lines changed

5 files changed

+231
-17
lines changed

packages/pretty-format/src/index.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -380,29 +380,44 @@ function printer(
380380
refs: Refs,
381381
hasCalledToJSON?: boolean,
382382
): string {
383+
let result: string
384+
383385
const plugin = findPlugin(config.plugins, val)
384386
if (plugin !== null) {
385-
return printPlugin(plugin, val, config, indentation, depth, refs)
387+
result = printPlugin(plugin, val, config, indentation, depth, refs)
388+
}
389+
else {
390+
const basicResult = printBasicValue(
391+
val,
392+
config.printFunctionName,
393+
config.escapeRegex,
394+
config.escapeString,
395+
)
396+
if (basicResult !== null) {
397+
result = basicResult
398+
}
399+
else {
400+
result = printComplexValue(
401+
val,
402+
config,
403+
indentation,
404+
depth,
405+
refs,
406+
hasCalledToJSON,
407+
)
408+
}
386409
}
387410

388-
const basicResult = printBasicValue(
389-
val,
390-
config.printFunctionName,
391-
config.escapeRegex,
392-
config.escapeString,
393-
)
394-
if (basicResult !== null) {
395-
return basicResult
411+
// Check string length budget:
412+
// accumulate output length and if exceeded,
413+
// force no further recursion by patching maxDepth.
414+
// Inspired by Node's util.inspect bail out approach.
415+
config.outputLength += result.length
416+
if (config.outputLength > config.maxOutputLength) {
417+
config.maxDepth = 0
396418
}
397419

398-
return printComplexValue(
399-
val,
400-
config,
401-
indentation,
402-
depth,
403-
refs,
404-
hasCalledToJSON,
405-
)
420+
return result
406421
}
407422

408423
const DEFAULT_THEME: Theme = {
@@ -425,6 +440,9 @@ export const DEFAULT_OPTIONS: Options = {
425440
highlight: false,
426441
indent: 2,
427442
maxDepth: Number.POSITIVE_INFINITY,
443+
// Practical default hard-limit to avoid too long string being generated
444+
// (Node's limit is buffer.constants.MAX_STRING_LENGTH ~ 512MB)
445+
maxOutputLength: 1_000_000,
428446
maxWidth: Number.POSITIVE_INFINITY,
429447
min: false,
430448
plugins: [],
@@ -509,6 +527,8 @@ function getConfig(options?: OptionsReceived): Config {
509527
printShadowRoot: options?.printShadowRoot ?? true,
510528
spacingInner: options?.min ? ' ' : '\n',
511529
spacingOuter: options?.min ? '' : '\n',
530+
maxOutputLength: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength,
531+
outputLength: 0,
512532
}
513533
}
514534

packages/pretty-format/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface PrettyFormatOptions {
4545
indent?: number
4646
maxDepth?: number
4747
maxWidth?: number
48+
maxOutputLength?: number
4849
min?: boolean
4950
printBasicPrototype?: boolean
5051
printFunctionName?: boolean
@@ -71,6 +72,8 @@ export interface Config {
7172
printShadowRoot: boolean
7273
spacingInner: string
7374
spacingOuter: string
75+
maxOutputLength: number
76+
outputLength: number
7477
}
7578

7679
export type Printer = (

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@test/vite-external": "link:./projects/vite-external",
2323
"@vitest/expect": "workspace:*",
2424
"@vitest/mocker": "workspace:*",
25+
"@vitest/pretty-format": "workspace:*",
2526
"@vitest/runner": "workspace:*",
2627
"@vitest/test-dep-cjs": "file:./deps/dep-cjs",
2728
"@vitest/test-dep-nested-cjs": "file:./deps/dep-nested-cjs",
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { format } from '@vitest/pretty-format'
2+
import { describe, expect, test } from 'vitest'
3+
4+
describe('maxOutputLength', () => {
5+
function createObjectGraph(n: number) {
6+
// owner
7+
// |-> cats
8+
// |-> cat0 -> owner
9+
// |-> cat1 -> owner
10+
// |-> cat2
11+
// |-> ...
12+
// |-> dogs
13+
// |-> dog0
14+
// |-> dog1
15+
// |-> dog2
16+
// |-> ...
17+
interface Owner {
18+
dogs: Pet[]
19+
cats: Pet[]
20+
}
21+
interface Pet {
22+
name: string
23+
owner: Owner
24+
}
25+
const owner: Owner = { dogs: [], cats: [] }
26+
for (let i = 0; i < n; i++) {
27+
owner.dogs.push({ name: `dog${i}`, owner })
28+
}
29+
for (let i = 0; i < n; i++) {
30+
owner.cats.push({ name: `cat${i}`, owner })
31+
}
32+
return owner
33+
}
34+
35+
test('quadratic growth example depending on format root', () => {
36+
const owner = createObjectGraph(3)
37+
38+
// when starting from owner, each pet is expanded once, so no amplification, just linear growth.
39+
expect(format(owner)).toMatchInlineSnapshot(`
40+
"Object {
41+
"cats": Array [
42+
Object {
43+
"name": "cat0",
44+
"owner": [Circular],
45+
},
46+
Object {
47+
"name": "cat1",
48+
"owner": [Circular],
49+
},
50+
Object {
51+
"name": "cat2",
52+
"owner": [Circular],
53+
},
54+
],
55+
"dogs": Array [
56+
Object {
57+
"name": "dog0",
58+
"owner": [Circular],
59+
},
60+
Object {
61+
"name": "dog1",
62+
"owner": [Circular],
63+
},
64+
Object {
65+
"name": "dog2",
66+
"owner": [Circular],
67+
},
68+
],
69+
}"
70+
`)
71+
72+
// when starting from owner.cats, each cat re-expands the full dogs list via owner.
73+
// this exhibits quadratic growth, which is what the budget is designed to prevent.
74+
expect(format(owner.cats)).toMatchInlineSnapshot(`
75+
"Array [
76+
Object {
77+
"name": "cat0",
78+
"owner": Object {
79+
"cats": [Circular],
80+
"dogs": Array [
81+
Object {
82+
"name": "dog0",
83+
"owner": [Circular],
84+
},
85+
Object {
86+
"name": "dog1",
87+
"owner": [Circular],
88+
},
89+
Object {
90+
"name": "dog2",
91+
"owner": [Circular],
92+
},
93+
],
94+
},
95+
},
96+
Object {
97+
"name": "cat1",
98+
"owner": Object {
99+
"cats": [Circular],
100+
"dogs": Array [
101+
Object {
102+
"name": "dog0",
103+
"owner": [Circular],
104+
},
105+
Object {
106+
"name": "dog1",
107+
"owner": [Circular],
108+
},
109+
Object {
110+
"name": "dog2",
111+
"owner": [Circular],
112+
},
113+
],
114+
},
115+
},
116+
Object {
117+
"name": "cat2",
118+
"owner": Object {
119+
"cats": [Circular],
120+
"dogs": Array [
121+
Object {
122+
"name": "dog0",
123+
"owner": [Circular],
124+
},
125+
Object {
126+
"name": "dog1",
127+
"owner": [Circular],
128+
},
129+
Object {
130+
"name": "dog2",
131+
"owner": [Circular],
132+
},
133+
],
134+
},
135+
},
136+
]"
137+
`)
138+
})
139+
140+
test('budget prevents blowup on large graphs', () => {
141+
// quickly hit the kill switch due to quadratic growth
142+
expect([10, 20, 30, 1000, 2000, 3000].map(n => format(createObjectGraph(n).cats).length))
143+
.toMatchInlineSnapshot(`
144+
[
145+
9729,
146+
36659,
147+
80789,
148+
273009,
149+
374009,
150+
299009,
151+
]
152+
`)
153+
154+
// depending on object/array shape, output can exceed the limit 1mb
155+
// but the output size is proportional to the amount of objects and the size of array.
156+
expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`936779`)
157+
expect(format(createObjectGraph(20000).cats).length).toMatchInlineSnapshot(`1236779`)
158+
})
159+
160+
test('early elements expanded, later elements folded after budget trips', () => {
161+
// First few objects are fully expanded, but once budget is exceeded,
162+
// maxDepth = 0 means no more expansion.
163+
const arr = Array.from({ length: 10 }, (_, i) => ({ i }))
164+
expect(format(arr, { maxOutputLength: 100 })).toMatchInlineSnapshot(`
165+
"Array [
166+
Object {
167+
"i": 0,
168+
},
169+
Object {
170+
"i": 1,
171+
},
172+
Object {
173+
"i": 2,
174+
},
175+
Object {
176+
"i": 3,
177+
},
178+
[Object],
179+
[Object],
180+
[Object],
181+
[Object],
182+
[Object],
183+
[Object],
184+
]"
185+
`)
186+
})
187+
})

0 commit comments

Comments
 (0)