From aa9acde54caed027b9e0a3ca50fcdfe589150201 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 24 Mar 2026 18:55:58 +0900 Subject: [PATCH 1/4] fix(pretty-format): fix output limit over counting --- test/core/test/pretty-format.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index 70c188862f65..a95b9c1dd9c6 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -157,6 +157,20 @@ describe('maxOutputLength', () => { expect(format(createObjectGraph(20000).cats).length).toMatchInlineSnapshot(`1236779`) }) + test('budget should not truncate output shorter than maxOutputLength', () => { + const data = Array.from({ length: 50 }, (_, i) => ({ a: { b: { c: i } } })) + const full = format(data, { maxOutputLength: Infinity }) + const limited = format(data, { maxOutputLength: full.length }) + // this invariant should hold for any input + // expect(limited.length).toBe(full.length) + expect({ limited: limited.length, full: full.length }).toMatchInlineSnapshot(` + { + "full": 4349, + "limited": 2399, + } + `) + }) + test('early elements expanded, later elements folded after budget trips', () => { // First few objects are fully expanded, but once budget is exceeded, // maxDepth = 0 means no more expansion. From 42e140d76e843155d5d68f055644595cadc9f400 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 24 Mar 2026 19:26:40 +0900 Subject: [PATCH 2/4] fix: actually fix --- packages/pretty-format/src/index.ts | 14 +++++++------- packages/pretty-format/src/types.ts | 3 ++- test/core/test/pretty-format.test.ts | 18 ++++++++++-------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 7dcee15ebdb1..0b86f1100dc2 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -408,12 +408,12 @@ function printer( } } - // Check string length budget: - // accumulate output length and if exceeded, - // force no further recursion by patching maxDepth. - // Inspired by Node's util.inspect bail out approach. - config.outputLength += result.length - if (config.outputLength > config.maxOutputLength) { + // Per-depth output budget (inspired by Node's util.inspect). + // Each depth level tracks output independently, so nested results + // don't inflate a single counter. See node/lib/internal/util/inspect.js#L1486. + config.outputLengthPerDepth[depth] ??= 0 + config.outputLengthPerDepth[depth] += result.length + if (config.outputLengthPerDepth[depth] > config.maxOutputLength) { config.maxDepth = 0 } @@ -528,7 +528,7 @@ function getConfig(options?: OptionsReceived): Config { spacingInner: options?.min ? ' ' : '\n', spacingOuter: options?.min ? '' : '\n', maxOutputLength: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength, - outputLength: 0, + outputLengthPerDepth: [], } } diff --git a/packages/pretty-format/src/types.ts b/packages/pretty-format/src/types.ts index 2e37ebb4f595..644650639010 100644 --- a/packages/pretty-format/src/types.ts +++ b/packages/pretty-format/src/types.ts @@ -73,7 +73,8 @@ export interface Config { spacingInner: string spacingOuter: string maxOutputLength: number - outputLength: number + // track total printed string length per depth as we print + outputLengthPerDepth: number[] } export type Printer = ( diff --git a/test/core/test/pretty-format.test.ts b/test/core/test/pretty-format.test.ts index a95b9c1dd9c6..12a54ddcf52c 100644 --- a/test/core/test/pretty-format.test.ts +++ b/test/core/test/pretty-format.test.ts @@ -145,16 +145,16 @@ describe('maxOutputLength', () => { 9729, 36659, 80789, - 273009, - 374009, - 299009, + 1056011, + 1074009, + 1088009, ] `) // depending on object/array shape, output can exceed the limit 1mb // but the output size is proportional to the amount of objects and the size of array. - expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`936779`) - expect(format(createObjectGraph(20000).cats).length).toMatchInlineSnapshot(`1236779`) + expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`1377439`) + expect(format(createObjectGraph(20000).cats).length).toMatchInlineSnapshot(`1497738`) }) test('budget should not truncate output shorter than maxOutputLength', () => { @@ -162,11 +162,11 @@ describe('maxOutputLength', () => { const full = format(data, { maxOutputLength: Infinity }) const limited = format(data, { maxOutputLength: full.length }) // this invariant should hold for any input - // expect(limited.length).toBe(full.length) + expect(limited.length).toBe(full.length) expect({ limited: limited.length, full: full.length }).toMatchInlineSnapshot(` { "full": 4349, - "limited": 2399, + "limited": 4349, } `) }) @@ -189,7 +189,9 @@ describe('maxOutputLength', () => { Object { "i": 3, }, - [Object], + Object { + "i": 4, + }, [Object], [Object], [Object], From bb95ef95687a11147791d10345e222d86b3d901b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 24 Mar 2026 19:46:00 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20comment=20"maxDepth=20=C3=97=20max?= =?UTF-8?q?OutputLength"=20bound=20is=20actual=20guarantee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/pretty-format/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 0b86f1100dc2..8293d476c8c9 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -410,7 +410,10 @@ function printer( // Per-depth output budget (inspired by Node's util.inspect). // Each depth level tracks output independently, so nested results - // don't inflate a single counter. See node/lib/internal/util/inspect.js#L1486. + // don't inflate a single counter (which would undercount by ~Nx for + // N levels of nesting). Nodes at the same depth produce disjoint spans + // in the output string, so each bucket accurately reflects output at + // that level. Total output is bounded by maxDepth × maxOutputLength. config.outputLengthPerDepth[depth] ??= 0 config.outputLengthPerDepth[depth] += result.length if (config.outputLengthPerDepth[depth] > config.maxOutputLength) { From 696730ddb04e946d734ab4a0c4ff76812e1b0b0f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 25 Mar 2026 09:38:19 +0900 Subject: [PATCH 4/4] chore: rename --- packages/pretty-format/src/index.ts | 8 ++++---- packages/pretty-format/src/types.ts | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 8293d476c8c9..542cf0630cd9 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -414,9 +414,9 @@ function printer( // N levels of nesting). Nodes at the same depth produce disjoint spans // in the output string, so each bucket accurately reflects output at // that level. Total output is bounded by maxDepth × maxOutputLength. - config.outputLengthPerDepth[depth] ??= 0 - config.outputLengthPerDepth[depth] += result.length - if (config.outputLengthPerDepth[depth] > config.maxOutputLength) { + config._outputLengthPerDepth[depth] ??= 0 + config._outputLengthPerDepth[depth] += result.length + if (config._outputLengthPerDepth[depth] > config.maxOutputLength) { config.maxDepth = 0 } @@ -531,7 +531,7 @@ function getConfig(options?: OptionsReceived): Config { spacingInner: options?.min ? ' ' : '\n', spacingOuter: options?.min ? '' : '\n', maxOutputLength: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength, - outputLengthPerDepth: [], + _outputLengthPerDepth: [], } } diff --git a/packages/pretty-format/src/types.ts b/packages/pretty-format/src/types.ts index 644650639010..2c7544caeaee 100644 --- a/packages/pretty-format/src/types.ts +++ b/packages/pretty-format/src/types.ts @@ -45,6 +45,13 @@ export interface PrettyFormatOptions { indent?: number maxDepth?: number maxWidth?: number + /** + * Approximate per-depth-level budget for output length. + * When the accumulated output at any single depth level exceeds this value, + * further nesting is collapsed. This is a heuristic safety valve, not a hard + * limit — total output can reach up to roughly `maxDepth × maxOutputLength`. + * @default 1_000_000 + */ maxOutputLength?: number min?: boolean printBasicPrototype?: boolean @@ -73,8 +80,11 @@ export interface Config { spacingInner: string spacingOuter: string maxOutputLength: number - // track total printed string length per depth as we print - outputLengthPerDepth: number[] + /** + * Per-depth budget accumulator for {@link maxOutputLength}. + * @internal + */ + _outputLengthPerDepth: number[] } export type Printer = (