Skip to content

Commit b21cf58

Browse files
fix: split tests
1 parent 20dd2b8 commit b21cf58

File tree

11 files changed

+1898
-951
lines changed

11 files changed

+1898
-951
lines changed

src/commands/jq/evaluator.ts

Lines changed: 311 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -231,53 +231,34 @@ export function evaluate(
231231
}
232232

233233
case "UpdateOp": {
234-
const pathResults = evaluate(value, ast.path, ctx);
235-
const valueResults = evaluate(value, ast.value, ctx);
236-
237-
return pathResults.flatMap((current) =>
238-
valueResults.map((newVal) => {
239-
switch (ast.op) {
240-
case "=":
241-
return newVal;
242-
case "+=":
243-
if (typeof current === "number" && typeof newVal === "number")
244-
return current + newVal;
245-
if (typeof current === "string" && typeof newVal === "string")
246-
return current + newVal;
247-
if (Array.isArray(current) && Array.isArray(newVal))
248-
return [...current, ...newVal];
249-
if (
250-
current &&
251-
newVal &&
252-
typeof current === "object" &&
253-
typeof newVal === "object"
254-
) {
255-
return { ...current, ...newVal };
256-
}
257-
return newVal;
258-
case "-=":
259-
if (typeof current === "number" && typeof newVal === "number")
260-
return current - newVal;
261-
return current;
262-
case "*=":
263-
if (typeof current === "number" && typeof newVal === "number")
264-
return current * newVal;
265-
return current;
266-
case "/=":
267-
if (typeof current === "number" && typeof newVal === "number")
268-
return current / newVal;
269-
return current;
270-
case "%=":
271-
if (typeof current === "number" && typeof newVal === "number")
272-
return current % newVal;
273-
return current;
274-
case "//=":
275-
return current === null || current === false ? newVal : current;
276-
default:
277-
return newVal;
278-
}
279-
}),
280-
);
234+
return [applyUpdate(value, ast.path, ast.op, ast.value, ctx)];
235+
}
236+
237+
case "Reduce": {
238+
const items = evaluate(value, ast.expr, ctx);
239+
let accumulator = evaluate(value, ast.init, ctx)[0];
240+
for (const item of items) {
241+
const newCtx = withVar(ctx, ast.varName, item);
242+
accumulator = evaluate(accumulator, ast.update, newCtx)[0];
243+
}
244+
return [accumulator];
245+
}
246+
247+
case "Foreach": {
248+
const items = evaluate(value, ast.expr, ctx);
249+
let state = evaluate(value, ast.init, ctx)[0];
250+
const results: JqValue[] = [];
251+
for (const item of items) {
252+
const newCtx = withVar(ctx, ast.varName, item);
253+
state = evaluate(state, ast.update, newCtx)[0];
254+
if (ast.extract) {
255+
const extracted = evaluate(state, ast.extract, newCtx);
256+
results.push(...extracted);
257+
} else {
258+
results.push(state);
259+
}
260+
}
261+
return results;
281262
}
282263

283264
default: {
@@ -294,6 +275,241 @@ function normalizeIndex(idx: number, len: number): number {
294275
return Math.min(idx, len);
295276
}
296277

278+
function applyUpdate(
279+
root: JqValue,
280+
pathExpr: AstNode,
281+
op: string,
282+
valueExpr: AstNode,
283+
ctx: EvalContext,
284+
): JqValue {
285+
function computeNewValue(current: JqValue, newVal: JqValue): JqValue {
286+
switch (op) {
287+
case "=":
288+
return newVal;
289+
case "|=": {
290+
const results = evaluate(current, valueExpr, ctx);
291+
return results[0] ?? null;
292+
}
293+
case "+=":
294+
if (typeof current === "number" && typeof newVal === "number")
295+
return current + newVal;
296+
if (typeof current === "string" && typeof newVal === "string")
297+
return current + newVal;
298+
if (Array.isArray(current) && Array.isArray(newVal))
299+
return [...current, ...newVal];
300+
if (
301+
current &&
302+
newVal &&
303+
typeof current === "object" &&
304+
typeof newVal === "object"
305+
) {
306+
return { ...current, ...newVal };
307+
}
308+
return newVal;
309+
case "-=":
310+
if (typeof current === "number" && typeof newVal === "number")
311+
return current - newVal;
312+
return current;
313+
case "*=":
314+
if (typeof current === "number" && typeof newVal === "number")
315+
return current * newVal;
316+
return current;
317+
case "/=":
318+
if (typeof current === "number" && typeof newVal === "number")
319+
return current / newVal;
320+
return current;
321+
case "%=":
322+
if (typeof current === "number" && typeof newVal === "number")
323+
return current % newVal;
324+
return current;
325+
case "//=":
326+
return current === null || current === false ? newVal : current;
327+
default:
328+
return newVal;
329+
}
330+
}
331+
332+
function updateRecursive(
333+
val: JqValue,
334+
path: AstNode,
335+
transform: (current: JqValue) => JqValue,
336+
): JqValue {
337+
switch (path.type) {
338+
case "Identity":
339+
return transform(val);
340+
341+
case "Field": {
342+
if (path.base) {
343+
return updateRecursive(val, path.base, (baseVal) => {
344+
if (
345+
baseVal &&
346+
typeof baseVal === "object" &&
347+
!Array.isArray(baseVal)
348+
) {
349+
const obj = { ...baseVal } as Record<string, unknown>;
350+
obj[path.name] = transform(obj[path.name]);
351+
return obj;
352+
}
353+
return baseVal;
354+
});
355+
}
356+
if (val && typeof val === "object" && !Array.isArray(val)) {
357+
const obj = { ...val } as Record<string, unknown>;
358+
obj[path.name] = transform(obj[path.name]);
359+
return obj;
360+
}
361+
return val;
362+
}
363+
364+
case "Index": {
365+
const indices = evaluate(root, path.index, ctx);
366+
const idx = indices[0];
367+
368+
if (path.base) {
369+
return updateRecursive(val, path.base, (baseVal) => {
370+
if (typeof idx === "number" && Array.isArray(baseVal)) {
371+
const arr = [...baseVal];
372+
const i = idx < 0 ? arr.length + idx : idx;
373+
if (i >= 0 && i < arr.length) {
374+
arr[i] = transform(arr[i]);
375+
}
376+
return arr;
377+
}
378+
if (
379+
typeof idx === "string" &&
380+
baseVal &&
381+
typeof baseVal === "object" &&
382+
!Array.isArray(baseVal)
383+
) {
384+
const obj = { ...baseVal } as Record<string, unknown>;
385+
obj[idx] = transform(obj[idx]);
386+
return obj;
387+
}
388+
return baseVal;
389+
});
390+
}
391+
392+
if (typeof idx === "number" && Array.isArray(val)) {
393+
const arr = [...val];
394+
const i = idx < 0 ? arr.length + idx : idx;
395+
if (i >= 0 && i < arr.length) {
396+
arr[i] = transform(arr[i]);
397+
}
398+
return arr;
399+
}
400+
if (
401+
typeof idx === "string" &&
402+
val &&
403+
typeof val === "object" &&
404+
!Array.isArray(val)
405+
) {
406+
const obj = { ...val } as Record<string, unknown>;
407+
obj[idx] = transform(obj[idx]);
408+
return obj;
409+
}
410+
return val;
411+
}
412+
413+
case "Iterate": {
414+
const applyToContainer = (container: JqValue): JqValue => {
415+
if (Array.isArray(container)) {
416+
return container.map((item) => transform(item));
417+
}
418+
if (container && typeof container === "object") {
419+
const obj: Record<string, unknown> = {};
420+
for (const [k, v] of Object.entries(container)) {
421+
obj[k] = transform(v);
422+
}
423+
return obj;
424+
}
425+
return container;
426+
};
427+
428+
if (path.base) {
429+
return updateRecursive(val, path.base, applyToContainer);
430+
}
431+
return applyToContainer(val);
432+
}
433+
434+
case "Pipe": {
435+
const leftResult = updateRecursive(val, path.left, (x) => x);
436+
return updateRecursive(leftResult, path.right, transform);
437+
}
438+
439+
default:
440+
return transform(val);
441+
}
442+
}
443+
444+
const transformer = (current: JqValue): JqValue => {
445+
if (op === "|=") {
446+
return computeNewValue(current, current);
447+
}
448+
const newVals = evaluate(root, valueExpr, ctx);
449+
return computeNewValue(current, newVals[0] ?? null);
450+
};
451+
452+
return updateRecursive(root, pathExpr, transformer);
453+
}
454+
455+
function applyDel(root: JqValue, pathExpr: AstNode, ctx: EvalContext): JqValue {
456+
function deleteAt(val: JqValue, path: AstNode): JqValue {
457+
switch (path.type) {
458+
case "Identity":
459+
return null;
460+
461+
case "Field": {
462+
if (val && typeof val === "object" && !Array.isArray(val)) {
463+
const obj = { ...val } as Record<string, unknown>;
464+
delete obj[path.name];
465+
return obj;
466+
}
467+
return val;
468+
}
469+
470+
case "Index": {
471+
const indices = evaluate(root, path.index, ctx);
472+
const idx = indices[0];
473+
474+
if (typeof idx === "number" && Array.isArray(val)) {
475+
const arr = [...val];
476+
const i = idx < 0 ? arr.length + idx : idx;
477+
if (i >= 0 && i < arr.length) {
478+
arr.splice(i, 1);
479+
}
480+
return arr;
481+
}
482+
if (
483+
typeof idx === "string" &&
484+
val &&
485+
typeof val === "object" &&
486+
!Array.isArray(val)
487+
) {
488+
const obj = { ...val } as Record<string, unknown>;
489+
delete obj[idx];
490+
return obj;
491+
}
492+
return val;
493+
}
494+
495+
case "Iterate": {
496+
if (Array.isArray(val)) {
497+
return [];
498+
}
499+
if (val && typeof val === "object") {
500+
return {};
501+
}
502+
return val;
503+
}
504+
505+
default:
506+
return val;
507+
}
508+
}
509+
510+
return deleteAt(root, pathExpr);
511+
}
512+
297513
function isTruthy(v: JqValue): boolean {
298514
return v !== null && v !== false;
299515
}
@@ -326,7 +542,9 @@ function evalBinaryOp(
326542

327543
if (op === "//") {
328544
const leftVals = evaluate(value, left, ctx);
329-
const nonNull = leftVals.filter((v) => v !== null && v !== false);
545+
const nonNull = leftVals.filter(
546+
(v) => v !== null && v !== undefined && v !== false,
547+
);
330548
if (nonNull.length > 0) return nonNull;
331549
return evaluate(value, right, ctx);
332550
}
@@ -655,7 +873,9 @@ function evalBuiltin(
655873
case "flatten": {
656874
if (!Array.isArray(value)) return [null];
657875
const depth =
658-
args.length > 0 ? (evaluate(value, args[0], ctx)[0] as number) : 1;
876+
args.length > 0
877+
? (evaluate(value, args[0], ctx)[0] as number)
878+
: Number.POSITIVE_INFINITY;
659879
return [value.flat(depth)];
660880
}
661881

@@ -816,6 +1036,48 @@ function evalBuiltin(
8161036
return paths;
8171037
}
8181038

1039+
case "del": {
1040+
if (args.length === 0) return [value];
1041+
return [applyDel(value, args[0], ctx)];
1042+
}
1043+
1044+
case "paths": {
1045+
const paths: (string | number)[][] = [];
1046+
const walk = (v: JqValue, path: (string | number)[]) => {
1047+
if (v && typeof v === "object") {
1048+
if (Array.isArray(v)) {
1049+
for (let i = 0; i < v.length; i++) {
1050+
paths.push([...path, i]);
1051+
walk(v[i], [...path, i]);
1052+
}
1053+
} else {
1054+
for (const key of Object.keys(v)) {
1055+
paths.push([...path, key]);
1056+
walk((v as Record<string, unknown>)[key], [...path, key]);
1057+
}
1058+
}
1059+
}
1060+
};
1061+
walk(value, []);
1062+
if (args.length > 0) {
1063+
return paths.filter((p) => {
1064+
let v: JqValue = value;
1065+
for (const k of p) {
1066+
if (Array.isArray(v) && typeof k === "number") {
1067+
v = v[k];
1068+
} else if (v && typeof v === "object" && typeof k === "string") {
1069+
v = (v as Record<string, unknown>)[k];
1070+
} else {
1071+
return false;
1072+
}
1073+
}
1074+
const results = evaluate(v, args[0], ctx);
1075+
return results.some(isTruthy);
1076+
});
1077+
}
1078+
return paths;
1079+
}
1080+
8191081
case "leaf_paths": {
8201082
const paths: (string | number)[][] = [];
8211083
const walk = (v: JqValue, path: (string | number)[]) => {

0 commit comments

Comments
 (0)