Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 38 additions & 35 deletions src/processors/json.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, number> {
return Object.fromEntries(getJsonSpaceUsageFromStr(json));
}

describe('leafify', () => {
Expand All @@ -21,56 +12,68 @@ 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);
expect(result['a/c']).toBe(30);
});

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
expect(result['a/b/c']).toBe(60);
});
});

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', () => {
const data = parse('[]');
expect(data).toMatchInlineSnapshot(`
{
"*": 1,
}
`);
});

it('does not crash on nested empty arrays', () => {
const map = parse('{"a": [], "b": []}');
expect(map).toMatchInlineSnapshot(`
{
"<keys>": 6,
"a": 1,
"a/*": 1,
"b": 1,
"b/*": 1,
}
`);
});
});
42 changes: 30 additions & 12 deletions src/processors/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -85,33 +90,40 @@ 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}`);
}
}

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}`);
}
}
}

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('<keys>', tok.offset, tok.text.length);
Expand Down Expand Up @@ -155,10 +167,16 @@ 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);
const map = Object.entries(sizes);
resetState();
return map;
}

export const processJsonSpaceUsage: ProcessorFn = async args => {
const text = await collectInputFromArgs(args);
return getJsonSpaceUsageFromStr(text);
}
Loading