From 31950edfc3a82abbbf3b343e1ae7004a132842ee Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Wed, 15 Sep 2021 12:32:18 -0400 Subject: [PATCH 1/6] Handle empty array and objects --- src/processors/json.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/processors/json.ts b/src/processors/json.ts index 685aca8..e359e8f 100644 --- a/src/processors/json.ts +++ b/src/processors/json.ts @@ -85,7 +85,7 @@ function parseValue(tok: moo.Token, lex: moo.Lexer): void { ) { // no-op } else { - throw new Error(`a Unexpected token ${tok.type}`); + throw new Error(`a Unexpected token ${tok.type} @ ${lex.index}`); } } @@ -93,16 +93,20 @@ function parseArray(lex: moo.Lexer): void { let tok = nextSkipWhitepace(lex); for (;;) { pushPath('*', tok.offset); - parseValue(tok, lex); - popPath(lex.index - 1); - tok = nextSkipWhitepace(lex); + if (tok.type !== ']') { + parseValue(tok, lex); + popPath(lex.index - 1); + tok = nextSkipWhitepace(lex); + } else { + popPath(lex.index - 1); + } if (tok.type === ']') { break; } else if (tok.type === ',') { tok = nextSkipWhitepace(lex); continue; } else { - throw new Error(`b Unexpected token ${tok.type}`); + throw new Error(`b Unexpected token ${tok.type} @ ${lex.index}`); } } } @@ -110,8 +114,11 @@ function parseArray(lex: moo.Lexer): void { function parseObject(lex: moo.Lexer): void { let tok = nextSkipWhitepace(lex); for (;;) { + if (tok.type === '}') { + break; + } if (tok.type !== 'STRING') { - throw new Error(`c Unexpected token ${tok.type}`); + throw new Error(`c Unexpected token ${tok.type} @ ${lex.index}`); } const key = tok.value.slice(1, -1); // strip quotes addToken('', tok.offset, tok.text.length); From 42f393369c4d7fe466b408aefc5c9ea0f3999deb Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 13 Mar 2026 10:16:15 -0400 Subject: [PATCH 2/6] test for crash --- src/processors/json.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/processors/json.test.ts b/src/processors/json.test.ts index b8b1738..39dca5a 100644 --- a/src/processors/json.test.ts +++ b/src/processors/json.test.ts @@ -73,4 +73,9 @@ describe('processJsonSpaceUsage', () => { const largeMap = Object.fromEntries(large); expect(largeMap['b']).toBeGreaterThan(smallMap['b']); }); + + it('should not crash on empty arrays', async () => { + const data = await withTempJson('[]'); + expect(data).toMatchInlineSnapshot(); + }) }); From 0d3f4bf7619cc7e31dbb253abf4d502a560c6e94 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 13 Mar 2026 10:34:16 -0400 Subject: [PATCH 3/6] another crash --- src/processors/json.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/processors/json.test.ts b/src/processors/json.test.ts index 39dca5a..c9d62d4 100644 --- a/src/processors/json.test.ts +++ b/src/processors/json.test.ts @@ -78,4 +78,9 @@ describe('processJsonSpaceUsage', () => { const data = await withTempJson('[]'); expect(data).toMatchInlineSnapshot(); }) + + it('should not crash on nested empty arrays', async () => { + const data = await withTempJson('{"a": [], "b": []}'); + expect(data).toMatchInlineSnapshot(); + }) }); From 03885dda0f22eb3903ac3c5e27296db4cad93ac8 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 13 Mar 2026 10:37:14 -0400 Subject: [PATCH 4/6] factor out pure function --- src/processors/json.test.ts | 82 ++++++++++++++++++++++++++++++++++--- src/processors/json.ts | 10 +++-- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/processors/json.test.ts b/src/processors/json.test.ts index c9d62d4..8f67e93 100644 --- a/src/processors/json.test.ts +++ b/src/processors/json.test.ts @@ -21,7 +21,7 @@ describe('leafify', () => { }); it('subtracts child sizes from parent', () => { - const counts = {'a': 100, 'a/b': 40, 'a/c': 30}; + const counts = {a: 100, 'a/b': 40, 'a/c': 30}; const result = leafify(counts); expect(result['a']).toBe(30); // 100 - 40 - 30 expect(result['a/b']).toBe(40); @@ -29,7 +29,7 @@ describe('leafify', () => { }); it('handles deeply nested paths', () => { - const counts = {'a': 100, 'a/b': 60, 'a/b/c': 60}; + const counts = {a: 100, 'a/b': 60, 'a/b/c': 60}; const result = leafify(counts); expect(result['a']).toBe(40); // 100 - 60 expect(result['a/b']).toBe(0); // 60 - 60 @@ -76,11 +76,81 @@ describe('processJsonSpaceUsage', () => { it('should not crash on empty arrays', async () => { const data = await withTempJson('[]'); - expect(data).toMatchInlineSnapshot(); - }) + expect(data).toMatchInlineSnapshot(` + [ + [ + "", + 25, + ], + [ + "a", + 3, + ], + [ + "b", + 1004, + ], + [ + "outer/", + 7, + ], + [ + "outer/inner", + 2, + ], + [ + "outer", + -32, + ], + [ + "*", + 4, + ], + ] + `); + }); it('should not crash on nested empty arrays', async () => { const data = await withTempJson('{"a": [], "b": []}'); - expect(data).toMatchInlineSnapshot(); - }) + expect(data).toMatchInlineSnapshot(` + [ + [ + "", + 31, + ], + [ + "a", + 4, + ], + [ + "b", + 1005, + ], + [ + "outer/", + 7, + ], + [ + "outer/inner", + 2, + ], + [ + "outer", + -41, + ], + [ + "*", + 4, + ], + [ + "a/*", + 1, + ], + [ + "b/*", + 1, + ], + ] + `); + }); }); diff --git a/src/processors/json.ts b/src/processors/json.ts index e359e8f..26a73c8 100644 --- a/src/processors/json.ts +++ b/src/processors/json.ts @@ -162,10 +162,14 @@ export function leafify(counts: FileSizeMap): FileSizeMap { return counts; } -export const processJsonSpaceUsage: ProcessorFn = async args => { - const text = await collectInputFromArgs(args); - lexer.reset(text); +export function getJsonSpaceUsageFromStr(jsonText: string) { + lexer.reset(jsonText); parseValue(nextSkipWhitepace(lexer), lexer); leafify(sizes); return Object.entries(sizes); } + +export const processJsonSpaceUsage: ProcessorFn = async args => { + const text = await collectInputFromArgs(args); + return getJsonSpaceUsageFromStr(text); +} From aa68fad450d0b7eb5107c360df0b465f726e1ea7 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 13 Mar 2026 10:39:58 -0400 Subject: [PATCH 5/6] test sync function --- src/processors/json.test.ts | 130 ++++++------------------------------ 1 file changed, 21 insertions(+), 109 deletions(-) diff --git a/src/processors/json.test.ts b/src/processors/json.test.ts index 8f67e93..8ffa507 100644 --- a/src/processors/json.test.ts +++ b/src/processors/json.test.ts @@ -1,16 +1,7 @@ -import {promises as fs} from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import {leafify, processJsonSpaceUsage} from './json'; +import {getJsonSpaceUsageFromStr, leafify} from './json'; -async function withTempJson(content: string): Promise<[string, number][]> { - const file = path.join(os.tmpdir(), `json-test-${Date.now()}.json`); - await fs.writeFile(file, content, 'utf-8'); - try { - return await processJsonSpaceUsage([file]); - } finally { - await fs.unlink(file); - } +function parse(json: string): Record { + return Object.fromEntries(getJsonSpaceUsageFromStr(json)); } describe('leafify', () => { @@ -37,120 +28,41 @@ describe('leafify', () => { }); }); -describe('processJsonSpaceUsage', () => { - it('returns size entries for a flat object', async () => { - const rows = await withTempJson('{"a": 1, "b": 2}'); - const map = Object.fromEntries(rows); - // Both keys should appear +describe('getJsonSpaceUsageFromStr', () => { + it('returns size entries for a flat object', () => { + const map = parse('{"a": 1, "b": 2}'); expect(map).toHaveProperty('a'); expect(map).toHaveProperty('b'); - // Sizes should be positive expect(map['a']).toBeGreaterThan(0); expect(map['b']).toBeGreaterThan(0); }); - it('returns size entries for a nested object', async () => { - const rows = await withTempJson('{"outer": {"inner": 42}}'); - const map = Object.fromEntries(rows); + it('returns size entries for a nested object', () => { + const map = parse('{"outer": {"inner": 42}}'); expect(map).toHaveProperty('outer/inner'); expect(map['outer/inner']).toBeGreaterThan(0); }); - it('returns size entries for an array', async () => { - const rows = await withTempJson('[1, 2, 3]'); - const map = Object.fromEntries(rows); - // Array elements are keyed as '*' + it('returns size entries for an array', () => { + const map = parse('[1, 2, 3]'); expect(map).toHaveProperty('*'); expect(map['*']).toBeGreaterThan(0); }); - it('assigns larger size to a key with more content', async () => { - const small = await withTempJson('{"a": 1, "b": 1}'); - const large = await withTempJson( - '{"a": 1, "b": "' + 'x'.repeat(1000) + '"}' - ); - const smallMap = Object.fromEntries(small); - const largeMap = Object.fromEntries(large); - expect(largeMap['b']).toBeGreaterThan(smallMap['b']); + it('assigns larger size to a key with more content', () => { + const small = parse('{"a": 1, "b": 1}'); + const large = parse('{"a": 1, "b": "' + 'x'.repeat(1000) + '"}'); + expect(large['b']).toBeGreaterThan(small['b']); }); - it('should not crash on empty arrays', async () => { - const data = await withTempJson('[]'); - expect(data).toMatchInlineSnapshot(` - [ - [ - "", - 25, - ], - [ - "a", - 3, - ], - [ - "b", - 1004, - ], - [ - "outer/", - 7, - ], - [ - "outer/inner", - 2, - ], - [ - "outer", - -32, - ], - [ - "*", - 4, - ], - ] - `); + it('does not crash on empty arrays', () => { + expect(() => parse('[]')).not.toThrow(); }); - it('should not crash on nested empty arrays', async () => { - const data = await withTempJson('{"a": [], "b": []}'); - expect(data).toMatchInlineSnapshot(` - [ - [ - "", - 31, - ], - [ - "a", - 4, - ], - [ - "b", - 1005, - ], - [ - "outer/", - 7, - ], - [ - "outer/inner", - 2, - ], - [ - "outer", - -41, - ], - [ - "*", - 4, - ], - [ - "a/*", - 1, - ], - [ - "b/*", - 1, - ], - ] - `); + it('does not crash on nested empty arrays', () => { + expect(() => parse('{"a": [], "b": []}')).not.toThrow(); + const map = parse('{"a": [], "b": []}'); + expect(map).toHaveProperty('a'); + expect(map).toHaveProperty('b'); }); }); From b805b9556d67fcd65ddab23b3e950f20e0e46b26 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 13 Mar 2026 10:44:47 -0400 Subject: [PATCH 6/6] reset state between tests --- src/processors/json.test.ts | 21 ++++++++++++++++----- src/processors/json.ts | 13 ++++++++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/processors/json.test.ts b/src/processors/json.test.ts index 8ffa507..5d0a94b 100644 --- a/src/processors/json.test.ts +++ b/src/processors/json.test.ts @@ -55,14 +55,25 @@ describe('getJsonSpaceUsageFromStr', () => { expect(large['b']).toBeGreaterThan(small['b']); }); - it('does not crash on empty arrays', () => { - expect(() => parse('[]')).not.toThrow(); + it('should not crash on empty arrays', () => { + const data = parse('[]'); + expect(data).toMatchInlineSnapshot(` + { + "*": 1, + } + `); }); it('does not crash on nested empty arrays', () => { - expect(() => parse('{"a": [], "b": []}')).not.toThrow(); const map = parse('{"a": [], "b": []}'); - expect(map).toHaveProperty('a'); - expect(map).toHaveProperty('b'); + expect(map).toMatchInlineSnapshot(` + { + "": 6, + "a": 1, + "a/*": 1, + "b": 1, + "b/*": 1, + } + `); }); }); diff --git a/src/processors/json.ts b/src/processors/json.ts index 26a73c8..a138932 100644 --- a/src/processors/json.ts +++ b/src/processors/json.ts @@ -35,8 +35,13 @@ interface FileSizeMap { [path: string]: number; } -const path: Segment[] = []; -const sizes: FileSizeMap = {}; +let path: Segment[] = []; +let sizes: FileSizeMap = {}; + +function resetState() { + path = []; + sizes = {}; +} function pushPath(key: string, pos?: number): void { pos = pos === undefined ? lexer.index : pos; @@ -166,7 +171,9 @@ export function getJsonSpaceUsageFromStr(jsonText: string) { lexer.reset(jsonText); parseValue(nextSkipWhitepace(lexer), lexer); leafify(sizes); - return Object.entries(sizes); + const map = Object.entries(sizes); + resetState(); + return map; } export const processJsonSpaceUsage: ProcessorFn = async args => {