@samitouri / QOS-React-2 / commits / d4a325df4d

[compiler] Add snap subcommand to minimize a test input (#35663)

Snap now supports subcommands 'test' (default) and 'minimize`. The minimize subcommand attempts to minimize a single failing input fixture by incrementally simplifying the ast so long as the same error occurs. I spot-checked it and it seemed to work pretty well. This is intended for use in a new subagent designed for investigating bugs — fixture simplification is an important part of the process and we can automate this rather than light tokens on fire. Example Input: ```js function Component(props) { const x = []; let result; for (let i = 0; i < 10; i++) { if (cond) { try { result = {key: bar([props.cond && props.foo])}; } catch (e) { console.log(e); } } } x.push(result); return <Stringify x={x} />; } ``` Command output: ``` $ yarn snap minimize --path .../input.js Minimizing: .../input.js Minimizing................ --- Minimized Code --- function Component(props) { try { props && props; } catch (e) {} } Reduced from 16 lines to 5 lines ``` This demonstrates things like: * Removing one statement at at time * Replacing if/else with the test, consequent, or alternate. Similar for other control-flow statements including try/catch * Removing individual array/object expression properties * Replacing single-value array/object with the value * Replacing control-flow expression (logical, consequent) w the test or left/right values * Removing call arguments * Replacing calls with a single argument with the argument * Replacing calls with multiple arguments with an array of the arguments * Replacing optional member/call with non-optional versions * Replacing member expression with the object. If computed, also try replacing w the key * And a bunch more strategies, see the code

Joseph Savona committed Feb 2, 2026 at 19:03 UTC d4a325df4dbc595cceede81555dd177fc791e8e1
4 files changed +2189 -35
compiler/packages/snap/package.json
+2
@@ -22,6 +22,7 @@
22 },
23 "dependencies": {
24 "@babel/code-frame": "^7.22.5",
25 + "@babel/generator": "^7.19.1",
26 "@babel/plugin-syntax-jsx": "^7.18.6",
27 "@babel/preset-flow": "^7.7.4",
28 "@babel/preset-typescript": "^7.26.0",
@@ -58,6 +59,7 @@
59 "resolutions": {
60 "./**/@babel/parser": "7.7.4",
61 "./**/@babel/types": "7.7.4",
62 + "@babel/generator": "7.2.0",
63 "@babel/preset-flow": "7.22.5"
64 }
65 }
compiler/packages/snap/src/minimize.ts new
+2070
@@ -0,0 +1,2070 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import type {PluginObj} from '@babel/core';
9 +import {transformFromAstSync} from '@babel/core';
10 +import generate from '@babel/generator';
11 +import traverse from '@babel/traverse';
12 +import * as t from '@babel/types';
13 +import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils';
14 +import fs from 'fs';
15 +import path from 'path';
16 +import {parseInput, parseLanguage, parseSourceType} from './compiler.js';
17 +import {PARSE_CONFIG_PRAGMA_IMPORT, PROJECT_SRC} from './constants.js';
18 +
19 +type MinimizeOptions = {
20 + path: string;
21 +};
22 +
23 +type CompileSuccess = {kind: 'success'};
24 +type CompileParseError = {kind: 'parse_error'; message: string};
25 +type CompileErrors = {
26 + kind: 'errors';
27 + errors: Array<{category: string; reason: string}>;
28 +};
29 +type CompileResult = CompileSuccess | CompileParseError | CompileErrors;
30 +
31 +/**
32 + * Compile code and extract error information
33 + */
34 +function compileAndGetError(
35 + code: string,
36 + filename: string,
37 + language: 'flow' | 'typescript',
38 + sourceType: 'module' | 'script',
39 + plugin: PluginObj,
40 + parseConfigPragmaFn: typeof ParseConfigPragma,
41 +): CompileResult {
42 + let ast: t.File;
43 + try {
44 + ast = parseInput(code, filename, language, sourceType);
45 + } catch (e: unknown) {
46 + return {kind: 'parse_error', message: (e as Error).message};
47 + }
48 +
49 + const firstLine = code.substring(0, code.indexOf('\n'));
50 + const config = parseConfigPragmaFn(firstLine, {compilationMode: 'all'});
51 + const options = {
52 + ...config,
53 + environment: {
54 + ...config.environment,
55 + },
56 + logger: {
57 + logEvent: () => {},
58 + debugLogIRs: () => {},
59 + },
60 + enableReanimatedCheck: false,
61 + };
62 +
63 + try {
64 + transformFromAstSync(ast, code, {
65 + filename: '/' + filename,
66 + highlightCode: false,
67 + retainLines: true,
68 + compact: true,
69 + plugins: [[plugin, options]],
70 + sourceType: 'module',
71 + ast: false,
72 + cloneInputAst: true,
73 + configFile: false,
74 + babelrc: false,
75 + });
76 + return {kind: 'success'};
77 + } catch (e: unknown) {
78 + const error = e as Error & {
79 + details?: Array<{category: string; reason: string}>;
80 + };
81 + // Check if this is a CompilerError with details
82 + if (error.details && error.details.length > 0) {
83 + return {
84 + kind: 'errors',
85 + errors: error.details.map(detail => ({
86 + category: detail.category,
87 + reason: detail.reason,
88 + })),
89 + };
90 + }
91 + // Fallback for other errors - use error name/message
92 + return {
93 + kind: 'errors',
94 + errors: [
95 + {
96 + category: error.name ?? 'Error',
97 + reason: error.message,
98 + },
99 + ],
100 + };
101 + }
102 +}
103 +
104 +/**
105 + * Check if two compile errors match
106 + */
107 +function errorsMatch(a: CompileErrors, b: CompileResult): boolean {
108 + if (b.kind !== 'errors') {
109 + return false;
110 + }
111 + if (a.errors.length !== b.errors.length) {
112 + return false;
113 + }
114 + for (let i = 0; i < a.errors.length; i++) {
115 + if (
116 + a.errors[i].category !== b.errors[i].category ||
117 + a.errors[i].reason !== b.errors[i].reason
118 + ) {
119 + return false;
120 + }
121 + }
122 + return true;
123 +}
124 +
125 +/**
126 + * Convert AST to code string
127 + */
128 +function astToCode(ast: t.File): string {
129 + return generate(ast).code;
130 +}
131 +
132 +/**
133 + * Clone an AST node deeply
134 + */
135 +function cloneAst(ast: t.File): t.File {
136 + return t.cloneNode(ast, true);
137 +}
138 +
139 +/**
140 + * Generator that yields ASTs with statements removed one at a time
141 + */
142 +function* removeStatements(ast: t.File): Generator<t.File> {
143 + // Collect all statement locations: which container (by index) and which statement index
144 + const statementLocations: Array<{containerIndex: number; stmtIndex: number}> =
145 + [];
146 + let containerIndex = 0;
147 +
148 + t.traverseFast(ast, node => {
149 + if (t.isBlockStatement(node) || t.isProgram(node)) {
150 + const body = node.body as t.Statement[];
151 + // Iterate in reverse order so removing later statements first
152 + for (let i = body.length - 1; i >= 0; i--) {
153 + statementLocations.push({containerIndex, stmtIndex: i});
154 + }
155 + containerIndex++;
156 + }
157 + });
158 +
159 + for (const {
160 + containerIndex: targetContainerIdx,
161 + stmtIndex,
162 + } of statementLocations) {
163 + const cloned = cloneAst(ast);
164 + let idx = 0;
165 + let modified = false;
166 +
167 + t.traverseFast(cloned, node => {
168 + if (modified) return;
169 + if (t.isBlockStatement(node) || t.isProgram(node)) {
170 + if (idx === targetContainerIdx) {
171 + const body = node.body as t.Statement[];
172 + if (stmtIndex < body.length) {
173 + body.splice(stmtIndex, 1);
174 + modified = true;
175 + }
176 + }
177 + idx++;
178 + }
179 + });
180 +
181 + if (modified) {
182 + yield cloned;
183 + }
184 + }
185 +}
186 +
187 +/**
188 + * Generator that yields ASTs with call arguments removed one at a time
189 + */
190 +function* removeCallArguments(ast: t.File): Generator<t.File> {
191 + // Collect all call expressions with their argument counts
192 + const callSites: Array<{callIndex: number; argCount: number}> = [];
193 + let callIndex = 0;
194 + t.traverseFast(ast, node => {
195 + if (t.isCallExpression(node) && node.arguments.length > 0) {
196 + callSites.push({callIndex, argCount: node.arguments.length});
197 + callIndex++;
198 + }
199 + });
200 +
201 + // For each call site, try removing each argument one at a time (from end to start)
202 + for (const {callIndex: targetCallIdx, argCount} of callSites) {
203 + for (let argIdx = argCount - 1; argIdx >= 0; argIdx--) {
204 + const cloned = cloneAst(ast);
205 + let idx = 0;
206 + let modified = false;
207 +
208 + t.traverseFast(cloned, node => {
209 + if (modified) return;
210 + if (t.isCallExpression(node) && node.arguments.length > 0) {
211 + if (idx === targetCallIdx && argIdx < node.arguments.length) {
212 + node.arguments.splice(argIdx, 1);
213 + modified = true;
214 + }
215 + idx++;
216 + }
217 + });
218 +
219 + if (modified) {
220 + yield cloned;
221 + }
222 + }
223 + }
224 +}
225 +
226 +/**
227 + * Generator that simplifies call expressions by replacing them with their arguments.
228 + * For single argument: foo(x) -> x
229 + * For multiple arguments: foo(x, y) -> [x, y]
230 + */
231 +function* simplifyCallExpressions(ast: t.File): Generator<t.File> {
232 + // Count call expressions with arguments
233 + let callCount = 0;
234 + t.traverseFast(ast, node => {
235 + if (t.isCallExpression(node) && node.arguments.length > 0) {
236 + callCount++;
237 + }
238 + });
239 +
240 + // For each call, try replacing with arguments
241 + for (let targetIdx = 0; targetIdx < callCount; targetIdx++) {
242 + const cloned = cloneAst(ast);
243 + let idx = 0;
244 + let modified = false;
245 +
246 + traverse(cloned, {
247 + CallExpression(path) {
248 + if (modified) return;
249 + if (path.node.arguments.length > 0 && idx === targetIdx) {
250 + const args = path.node.arguments;
251 + // Filter to only Expression arguments (not SpreadElement)
252 + const exprArgs = args.filter((arg): arg is t.Expression =>
253 + t.isExpression(arg),
254 + );
255 + if (exprArgs.length === 0) {
256 + idx++;
257 + return;
258 + }
259 + if (exprArgs.length === 1) {
260 + // Single argument: replace call with the argument
261 + path.replaceWith(exprArgs[0]);
262 + } else {
263 + // Multiple arguments: replace call with array of arguments
264 + path.replaceWith(t.arrayExpression(exprArgs));
265 + }
266 + modified = true;
267 + }
268 + idx++;
269 + },
270 + });
271 +
272 + if (modified) {
273 + yield cloned;
274 + }
275 + }
276 +
277 + // Also try replacing with each individual argument for multi-arg calls
278 + for (let targetIdx = 0; targetIdx < callCount; targetIdx++) {
279 + // First, find the arg count for this call
280 + let argCount = 0;
281 + let currentIdx = 0;
282 + t.traverseFast(ast, node => {
283 + if (t.isCallExpression(node) && node.arguments.length > 0) {
284 + if (currentIdx === targetIdx) {
285 + argCount = node.arguments.length;
286 + }
287 + currentIdx++;
288 + }
289 + });
290 +
291 + // Try replacing with each argument individually
292 + for (let argIdx = 0; argIdx < argCount; argIdx++) {
293 + const cloned = cloneAst(ast);
294 + let idx = 0;
295 + let modified = false;
296 +
297 + traverse(cloned, {
298 + CallExpression(path) {
299 + if (modified) return;
300 + if (path.node.arguments.length > 0 && idx === targetIdx) {
301 + const arg = path.node.arguments[argIdx];
302 + if (t.isExpression(arg)) {
303 + path.replaceWith(arg);
304 + modified = true;
305 + }
306 + }
307 + idx++;
308 + },
309 + });
310 +
311 + if (modified) {
312 + yield cloned;
313 + }
314 + }
315 + }
316 +}
317 +
318 +/**
319 + * Generator that simplifies conditional expressions (a ? b : c) -> a, b, or c
320 + */
321 +function* simplifyConditionals(ast: t.File): Generator<t.File> {
322 + // Count conditionals
323 + let condCount = 0;
324 + t.traverseFast(ast, node => {
325 + if (t.isConditionalExpression(node)) {
326 + condCount++;
327 + }
328 + });
329 +
330 + // Try replacing with test condition
331 + for (let targetIdx = 0; targetIdx < condCount; targetIdx++) {
332 + const cloned = cloneAst(ast);
333 + let modified = false;
334 + let idx = 0;
335 +
336 + traverse(cloned, {
337 + ConditionalExpression(path) {
338 + if (modified) return;
339 + if (idx === targetIdx) {
340 + path.replaceWith(path.node.test);
341 + modified = true;
342 + }
343 + idx++;
344 + },
345 + });
346 +
347 + if (modified) {
348 + yield cloned;
349 + }
350 + }
351 +
352 + // Try replacing with consequent
353 + for (let targetIdx = 0; targetIdx < condCount; targetIdx++) {
354 + const cloned = cloneAst(ast);
355 + let modified = false;
356 + let idx = 0;
357 +
358 + traverse(cloned, {
359 + ConditionalExpression(path) {
360 + if (modified) return;
361 + if (idx === targetIdx) {
362 + path.replaceWith(path.node.consequent);
363 + modified = true;
364 + }
365 + idx++;
366 + },
367 + });
368 +
369 + if (modified) {
370 + yield cloned;
371 + }
372 + }
373 +
374 + // Also try replacing with alternate
375 + for (let targetIdx = 0; targetIdx < condCount; targetIdx++) {
376 + const cloned = cloneAst(ast);
377 + let modified = false;
378 + let idx = 0;
379 +
380 + traverse(cloned, {
381 + ConditionalExpression(path) {
382 + if (modified) return;
383 + if (idx === targetIdx) {
384 + path.replaceWith(path.node.alternate);
385 + modified = true;
386 + }
387 + idx++;
388 + },
389 + });
390 +
391 + if (modified) {
392 + yield cloned;
393 + }
394 + }
395 +}
396 +
397 +/**
398 + * Generator that simplifies logical expressions (a && b) -> a or b
399 + */
400 +function* simplifyLogicalExpressions(ast: t.File): Generator<t.File> {
401 + // Count logical expressions
402 + let logicalCount = 0;
403 + t.traverseFast(ast, node => {
404 + if (t.isLogicalExpression(node)) {
405 + logicalCount++;
406 + }
407 + });
408 +
409 + // Try replacing with left side
410 + for (let targetIdx = 0; targetIdx < logicalCount; targetIdx++) {
411 + const cloned = cloneAst(ast);
412 + let idx = 0;
413 + let modified = false;
414 +
415 + traverse(cloned, {
416 + LogicalExpression(path) {
417 + if (modified) return;
418 + if (idx === targetIdx) {
419 + path.replaceWith(path.node.left);
420 + modified = true;
421 + }
422 + idx++;
423 + },
424 + });
425 +
426 + if (modified) {
427 + yield cloned;
428 + }
429 + }
430 +
431 + // Try replacing with right side
432 + for (let targetIdx = 0; targetIdx < logicalCount; targetIdx++) {
433 + const cloned = cloneAst(ast);
434 + let idx = 0;
435 + let modified = false;
436 +
437 + traverse(cloned, {
438 + LogicalExpression(path) {
439 + if (modified) return;
440 + if (idx === targetIdx) {
441 + path.replaceWith(path.node.right);
442 + modified = true;
443 + }
444 + idx++;
445 + },
446 + });
447 +
448 + if (modified) {
449 + yield cloned;
450 + }
451 + }
452 +}
453 +
454 +/**
455 + * Generator that simplifies optional chains (a?.b) -> a.b
456 + */
457 +function* simplifyOptionalChains(ast: t.File): Generator<t.File> {
458 + // Count optional expressions
459 + let optionalCount = 0;
460 + t.traverseFast(ast, node => {
461 + if (
462 + t.isOptionalMemberExpression(node) ||
463 + t.isOptionalCallExpression(node)
464 + ) {
465 + optionalCount++;
466 + }
467 + });
468 +
469 + for (let targetIdx = 0; targetIdx < optionalCount; targetIdx++) {
470 + const cloned = cloneAst(ast);
471 + let idx = 0;
472 + let modified = false;
473 +
474 + traverse(cloned, {
475 + OptionalMemberExpression(path) {
476 + if (modified) return;
477 + if (idx === targetIdx) {
478 + const {object, property, computed} = path.node;
479 + path.replaceWith(t.memberExpression(object, property, computed));
480 + modified = true;
481 + }
482 + idx++;
483 + },
484 + OptionalCallExpression(path) {
485 + if (modified) return;
486 + if (idx === targetIdx) {
487 + const {callee, arguments: args} = path.node;
488 + if (t.isExpression(callee)) {
489 + path.replaceWith(t.callExpression(callee, args));
490 + modified = true;
491 + }
492 + }
493 + idx++;
494 + },
495 + });
496 +
497 + if (modified) {
498 + yield cloned;
499 + }
500 + }
501 +}
502 +
503 +/**
504 + * Generator that simplifies await expressions: await expr -> expr
505 + */
506 +function* simplifyAwaitExpressions(ast: t.File): Generator<t.File> {
507 + // Count await expressions
508 + let awaitCount = 0;
509 + t.traverseFast(ast, node => {
510 + if (t.isAwaitExpression(node)) {
511 + awaitCount++;
512 + }
513 + });
514 +
515 + for (let targetIdx = 0; targetIdx < awaitCount; targetIdx++) {
516 + const cloned = cloneAst(ast);
517 + let idx = 0;
518 + let modified = false;
519 +
520 + traverse(cloned, {
521 + AwaitExpression(path) {
522 + if (modified) return;
523 + if (idx === targetIdx) {
524 + path.replaceWith(path.node.argument);
525 + modified = true;
526 + }
527 + idx++;
528 + },
529 + });
530 +
531 + if (modified) {
532 + yield cloned;
533 + }
534 + }
535 +}
536 +
537 +/**
538 + * Generator that simplifies if statements:
539 + * - Replace with test expression (as expression statement)
540 + * - Replace with consequent block
541 + * - Replace with alternate block (if present)
542 + */
543 +function* simplifyIfStatements(ast: t.File): Generator<t.File> {
544 + // Count if statements
545 + let ifCount = 0;
546 + t.traverseFast(ast, node => {
547 + if (t.isIfStatement(node)) {
548 + ifCount++;
549 + }
550 + });
551 +
552 + // Try replacing with test expression
553 + for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) {
554 + const cloned = cloneAst(ast);
555 + let idx = 0;
556 + let modified = false;
557 +
558 + traverse(cloned, {
559 + IfStatement(path) {
560 + if (modified) return;
561 + if (idx === targetIdx) {
562 + path.replaceWith(t.expressionStatement(path.node.test));
563 + modified = true;
564 + }
565 + idx++;
566 + },
567 + });
568 +
569 + if (modified) {
570 + yield cloned;
571 + }
572 + }
573 +
574 + // Try replacing with consequent
575 + for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) {
576 + const cloned = cloneAst(ast);
577 + let idx = 0;
578 + let modified = false;
579 +
580 + traverse(cloned, {
581 + IfStatement(path) {
582 + if (modified) return;
583 + if (idx === targetIdx) {
584 + path.replaceWith(path.node.consequent);
585 + modified = true;
586 + }
587 + idx++;
588 + },
589 + });
590 +
591 + if (modified) {
592 + yield cloned;
593 + }
594 + }
595 +
596 + // Try replacing with alternate (if present)
597 + for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) {
598 + const cloned = cloneAst(ast);
599 + let idx = 0;
600 + let modified = false;
601 +
602 + traverse(cloned, {
603 + IfStatement(path) {
604 + if (modified) return;
605 + if (idx === targetIdx && path.node.alternate) {
606 + path.replaceWith(path.node.alternate);
607 + modified = true;
608 + }
609 + idx++;
610 + },
611 + });
612 +
613 + if (modified) {
614 + yield cloned;
615 + }
616 + }
617 +}
618 +
619 +/**
620 + * Generator that simplifies switch statements:
621 + * - Replace with discriminant expression
622 + * - Replace with each case's consequent statements
623 + */
624 +function* simplifySwitchStatements(ast: t.File): Generator<t.File> {
625 + // Count switch statements
626 + let switchCount = 0;
627 + t.traverseFast(ast, node => {
628 + if (t.isSwitchStatement(node)) {
629 + switchCount++;
630 + }
631 + });
632 +
633 + // Try replacing with discriminant
634 + for (let targetIdx = 0; targetIdx < switchCount; targetIdx++) {
635 + const cloned = cloneAst(ast);
636 + let idx = 0;
637 + let modified = false;
638 +
639 + traverse(cloned, {
640 + SwitchStatement(path) {
641 + if (modified) return;
642 + if (idx === targetIdx) {
643 + path.replaceWith(t.expressionStatement(path.node.discriminant));
644 + modified = true;
645 + }
646 + idx++;
647 + },
648 + });
649 +
650 + if (modified) {
651 + yield cloned;
652 + }
653 + }
654 +
655 + // For each switch, try replacing with each case's body
656 + for (let targetIdx = 0; targetIdx < switchCount; targetIdx++) {
657 + // Find case count for this switch
658 + let caseCount = 0;
659 + let currentIdx = 0;
660 + t.traverseFast(ast, node => {
661 + if (t.isSwitchStatement(node)) {
662 + if (currentIdx === targetIdx) {
663 + caseCount = node.cases.length;
664 + }
665 + currentIdx++;
666 + }
667 + });
668 +
669 + for (let caseIdx = 0; caseIdx < caseCount; caseIdx++) {
670 + const cloned = cloneAst(ast);
671 + let idx = 0;
672 + let modified = false;
673 +
674 + traverse(cloned, {
675 + SwitchStatement(path) {
676 + if (modified) return;
677 + if (idx === targetIdx) {
678 + const switchCase = path.node.cases[caseIdx];
679 + if (switchCase && switchCase.consequent.length > 0) {
680 + path.replaceWithMultiple(switchCase.consequent);
681 + modified = true;
682 + }
683 + }
684 + idx++;
685 + },
686 + });
687 +
688 + if (modified) {
689 + yield cloned;
690 + }
691 + }
692 + }
693 +}
694 +
695 +/**
696 + * Generator that simplifies while statements:
697 + * - Replace with test expression
698 + * - Replace with body
699 + */
700 +function* simplifyWhileStatements(ast: t.File): Generator<t.File> {
701 + // Count while statements
702 + let whileCount = 0;
703 + t.traverseFast(ast, node => {
704 + if (t.isWhileStatement(node)) {
705 + whileCount++;
706 + }
707 + });
708 +
709 + // Try replacing with test
710 + for (let targetIdx = 0; targetIdx < whileCount; targetIdx++) {
711 + const cloned = cloneAst(ast);
712 + let idx = 0;
713 + let modified = false;
714 +
715 + traverse(cloned, {
716 + WhileStatement(path) {
717 + if (modified) return;
718 + if (idx === targetIdx) {
719 + path.replaceWith(t.expressionStatement(path.node.test));
720 + modified = true;
721 + }
722 + idx++;
723 + },
724 + });
725 +
726 + if (modified) {
727 + yield cloned;
728 + }
729 + }
730 +
731 + // Try replacing with body
732 + for (let targetIdx = 0; targetIdx < whileCount; targetIdx++) {
733 + const cloned = cloneAst(ast);
734 + let idx = 0;
735 + let modified = false;
736 +
737 + traverse(cloned, {
738 + WhileStatement(path) {
739 + if (modified) return;
740 + if (idx === targetIdx) {
741 + path.replaceWith(path.node.body);
742 + modified = true;
743 + }
744 + idx++;
745 + },
746 + });
747 +
748 + if (modified) {
749 + yield cloned;
750 + }
751 + }
752 +}
753 +
754 +/**
755 + * Generator that simplifies do-while statements:
756 + * - Replace with test expression
757 + * - Replace with body
758 + */
759 +function* simplifyDoWhileStatements(ast: t.File): Generator<t.File> {
760 + // Count do-while statements
761 + let doWhileCount = 0;
762 + t.traverseFast(ast, node => {
763 + if (t.isDoWhileStatement(node)) {
764 + doWhileCount++;
765 + }
766 + });
767 +
768 + // Try replacing with test
769 + for (let targetIdx = 0; targetIdx < doWhileCount; targetIdx++) {
770 + const cloned = cloneAst(ast);
771 + let idx = 0;
772 + let modified = false;
773 +
774 + traverse(cloned, {
775 + DoWhileStatement(path) {
776 + if (modified) return;
777 + if (idx === targetIdx) {
778 + path.replaceWith(t.expressionStatement(path.node.test));
779 + modified = true;
780 + }
781 + idx++;
782 + },
783 + });
784 +
785 + if (modified) {
786 + yield cloned;
787 + }
788 + }
789 +
790 + // Try replacing with body
791 + for (let targetIdx = 0; targetIdx < doWhileCount; targetIdx++) {
792 + const cloned = cloneAst(ast);
793 + let idx = 0;
794 + let modified = false;
795 +
796 + traverse(cloned, {
797 + DoWhileStatement(path) {
798 + if (modified) return;
799 + if (idx === targetIdx) {
800 + path.replaceWith(path.node.body);
801 + modified = true;
802 + }
803 + idx++;
804 + },
805 + });
806 +
807 + if (modified) {
808 + yield cloned;
809 + }
810 + }
811 +}
812 +
813 +/**
814 + * Generator that simplifies for statements:
815 + * - Replace with init (if expression)
816 + * - Replace with test expression
817 + * - Replace with update expression
818 + * - Replace with body
819 + */
820 +function* simplifyForStatements(ast: t.File): Generator<t.File> {
821 + // Count for statements
822 + let forCount = 0;
823 + t.traverseFast(ast, node => {
824 + if (t.isForStatement(node)) {
825 + forCount++;
826 + }
827 + });
828 +
829 + // Try replacing with init (if it's an expression)
830 + for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
831 + const cloned = cloneAst(ast);
832 + let idx = 0;
833 + let modified = false;
834 +
835 + traverse(cloned, {
836 + ForStatement(path) {
837 + if (modified) return;
838 + if (idx === targetIdx && path.node.init) {
839 + if (t.isExpression(path.node.init)) {
840 + path.replaceWith(t.expressionStatement(path.node.init));
841 + } else {
842 + // It's a VariableDeclaration
843 + path.replaceWith(path.node.init);
844 + }
845 + modified = true;
846 + }
847 + idx++;
848 + },
849 + });
850 +
851 + if (modified) {
852 + yield cloned;
853 + }
854 + }
855 +
856 + // Try replacing with test
857 + for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
858 + const cloned = cloneAst(ast);
859 + let idx = 0;
860 + let modified = false;
861 +
862 + traverse(cloned, {
863 + ForStatement(path) {
864 + if (modified) return;
865 + if (idx === targetIdx && path.node.test) {
866 + path.replaceWith(t.expressionStatement(path.node.test));
867 + modified = true;
868 + }
869 + idx++;
870 + },
871 + });
872 +
873 + if (modified) {
874 + yield cloned;
875 + }
876 + }
877 +
878 + // Try replacing with update
879 + for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
880 + const cloned = cloneAst(ast);
881 + let idx = 0;
882 + let modified = false;
883 +
884 + traverse(cloned, {
885 + ForStatement(path) {
886 + if (modified) return;
887 + if (idx === targetIdx && path.node.update) {
888 + path.replaceWith(t.expressionStatement(path.node.update));
889 + modified = true;
890 + }
891 + idx++;
892 + },
893 + });
894 +
895 + if (modified) {
896 + yield cloned;
897 + }
898 + }
899 +
900 + // Try replacing with body
901 + for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
902 + const cloned = cloneAst(ast);
903 + let idx = 0;
904 + let modified = false;
905 +
906 + traverse(cloned, {
907 + ForStatement(path) {
908 + if (modified) return;
909 + if (idx === targetIdx) {
910 + path.replaceWith(path.node.body);
911 + modified = true;
912 + }
913 + idx++;
914 + },
915 + });
916 +
917 + if (modified) {
918 + yield cloned;
919 + }
920 + }
921 +}
922 +
923 +/**
924 + * Generator that simplifies for-in statements:
925 + * - Replace with left (variable declaration or expression)
926 + * - Replace with right expression
927 + * - Replace with body
928 + */
929 +function* simplifyForInStatements(ast: t.File): Generator<t.File> {
930 + // Count for-in statements
931 + let forInCount = 0;
932 + t.traverseFast(ast, node => {
933 + if (t.isForInStatement(node)) {
934 + forInCount++;
935 + }
936 + });
937 +
938 + // Try replacing with left
939 + for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
940 + const cloned = cloneAst(ast);
941 + let idx = 0;
942 + let modified = false;
943 +
944 + traverse(cloned, {
945 + ForInStatement(path) {
946 + if (modified) return;
947 + if (idx === targetIdx) {
948 + const left = path.node.left;
949 + if (t.isExpression(left)) {
950 + path.replaceWith(t.expressionStatement(left));
951 + } else {
952 + path.replaceWith(left);
953 + }
954 + modified = true;
955 + }
956 + idx++;
957 + },
958 + });
959 +
960 + if (modified) {
961 + yield cloned;
962 + }
963 + }
964 +
965 + // Try replacing with right
966 + for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
967 + const cloned = cloneAst(ast);
968 + let idx = 0;
969 + let modified = false;
970 +
971 + traverse(cloned, {
972 + ForInStatement(path) {
973 + if (modified) return;
974 + if (idx === targetIdx) {
975 + path.replaceWith(t.expressionStatement(path.node.right));
976 + modified = true;
977 + }
978 + idx++;
979 + },
980 + });
981 +
982 + if (modified) {
983 + yield cloned;
984 + }
985 + }
986 +
987 + // Try replacing with body
988 + for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
989 + const cloned = cloneAst(ast);
990 + let idx = 0;
991 + let modified = false;
992 +
993 + traverse(cloned, {
994 + ForInStatement(path) {
995 + if (modified) return;
996 + if (idx === targetIdx) {
997 + path.replaceWith(path.node.body);
998 + modified = true;
999 + }
1000 + idx++;
1001 + },
1002 + });
1003 +
1004 + if (modified) {
1005 + yield cloned;
1006 + }
1007 + }
1008 +}
1009 +
1010 +/**
1011 + * Generator that simplifies for-of statements:
1012 + * - Replace with left (variable declaration or expression)
1013 + * - Replace with right expression
1014 + * - Replace with body
1015 + */
1016 +function* simplifyForOfStatements(ast: t.File): Generator<t.File> {
1017 + // Count for-of statements
1018 + let forOfCount = 0;
1019 + t.traverseFast(ast, node => {
1020 + if (t.isForOfStatement(node)) {
1021 + forOfCount++;
1022 + }
1023 + });
1024 +
1025 + // Try replacing with left
1026 + for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
1027 + const cloned = cloneAst(ast);
1028 + let idx = 0;
1029 + let modified = false;
1030 +
1031 + traverse(cloned, {
1032 + ForOfStatement(path) {
1033 + if (modified) return;
1034 + if (idx === targetIdx) {
1035 + const left = path.node.left;
1036 + if (t.isExpression(left)) {
1037 + path.replaceWith(t.expressionStatement(left));
1038 + } else {
1039 + path.replaceWith(left);
1040 + }
1041 + modified = true;
1042 + }
1043 + idx++;
1044 + },
1045 + });
1046 +
1047 + if (modified) {
1048 + yield cloned;
1049 + }
1050 + }
1051 +
1052 + // Try replacing with right
1053 + for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
1054 + const cloned = cloneAst(ast);
1055 + let idx = 0;
1056 + let modified = false;
1057 +
1058 + traverse(cloned, {
1059 + ForOfStatement(path) {
1060 + if (modified) return;
1061 + if (idx === targetIdx) {
1062 + path.replaceWith(t.expressionStatement(path.node.right));
1063 + modified = true;
1064 + }
1065 + idx++;
1066 + },
1067 + });
1068 +
1069 + if (modified) {
1070 + yield cloned;
1071 + }
1072 + }
1073 +
1074 + // Try replacing with body
1075 + for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
1076 + const cloned = cloneAst(ast);
1077 + let idx = 0;
1078 + let modified = false;
1079 +
1080 + traverse(cloned, {
1081 + ForOfStatement(path) {
1082 + if (modified) return;
1083 + if (idx === targetIdx) {
1084 + path.replaceWith(path.node.body);
1085 + modified = true;
1086 + }
1087 + idx++;
1088 + },
1089 + });
1090 +
1091 + if (modified) {
1092 + yield cloned;
1093 + }
1094 + }
1095 +}
1096 +
1097 +/**
1098 + * Generator that simplifies variable declarations by removing init expressions.
1099 + * let x = expr; -> let x;
1100 + * var x = expr; -> var x;
1101 + * Note: const without init is invalid, so we skip const declarations.
1102 + */
1103 +function* simplifyVariableDeclarations(ast: t.File): Generator<t.File> {
1104 + // Collect all variable declarators with init expressions (excluding const)
1105 + const declaratorSites: Array<{declIndex: number}> = [];
1106 + let declIndex = 0;
1107 + t.traverseFast(ast, node => {
1108 + if (t.isVariableDeclaration(node) && node.kind !== 'const') {
1109 + for (const declarator of node.declarations) {
1110 + if (declarator.init) {
1111 + declaratorSites.push({declIndex});
1112 + declIndex++;
1113 + }
1114 + }
1115 + }
1116 + });
1117 +
1118 + // Try removing init from each declarator
1119 + for (const {declIndex: targetDeclIdx} of declaratorSites) {
1120 + const cloned = cloneAst(ast);
1121 + let idx = 0;
1122 + let modified = false;
1123 +
1124 + t.traverseFast(cloned, node => {
1125 + if (modified) return;
1126 + if (t.isVariableDeclaration(node) && node.kind !== 'const') {
1127 + for (const declarator of node.declarations) {
1128 + if (declarator.init) {
1129 + if (idx === targetDeclIdx) {
1130 + declarator.init = null;
1131 + modified = true;
1132 + return;
1133 + }
1134 + idx++;
1135 + }
1136 + }
1137 + }
1138 + });
1139 +
1140 + if (modified) {
1141 + yield cloned;
1142 + }
1143 + }
1144 +}
1145 +
1146 +/**
1147 + * Generator that simplifies try/catch/finally statements:
1148 + * - Replace with try block contents
1149 + * - Replace with catch block contents (if present)
1150 + * - Replace with finally block contents (if present)
1151 + */
1152 +function* simplifyTryStatements(ast: t.File): Generator<t.File> {
1153 + // Count try statements
1154 + let tryCount = 0;
1155 + t.traverseFast(ast, node => {
1156 + if (t.isTryStatement(node)) {
1157 + tryCount++;
1158 + }
1159 + });
1160 +
1161 + // Try replacing with try block contents
1162 + for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) {
1163 + const cloned = cloneAst(ast);
1164 + let idx = 0;
1165 + let modified = false;
1166 +
1167 + traverse(cloned, {
1168 + TryStatement(path) {
1169 + if (modified) return;
1170 + if (idx === targetIdx) {
1171 + path.replaceWith(path.node.block);
1172 + modified = true;
1173 + }
1174 + idx++;
1175 + },
1176 + });
1177 +
1178 + if (modified) {
1179 + yield cloned;
1180 + }
1181 + }
1182 +
1183 + // Try replacing with catch block contents (if present)
1184 + for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) {
1185 + const cloned = cloneAst(ast);
1186 + let idx = 0;
1187 + let modified = false;
1188 +
1189 + traverse(cloned, {
1190 + TryStatement(path) {
1191 + if (modified) return;
1192 + if (idx === targetIdx && path.node.handler) {
1193 + path.replaceWith(path.node.handler.body);
1194 + modified = true;
1195 + }
1196 + idx++;
1197 + },
1198 + });
1199 +
1200 + if (modified) {
1201 + yield cloned;
1202 + }
1203 + }
1204 +
1205 + // Try replacing with finally block contents (if present)
1206 + for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) {
1207 + const cloned = cloneAst(ast);
1208 + let idx = 0;
1209 + let modified = false;
1210 +
1211 + traverse(cloned, {
1212 + TryStatement(path) {
1213 + if (modified) return;
1214 + if (idx === targetIdx && path.node.finalizer) {
1215 + path.replaceWith(path.node.finalizer);
1216 + modified = true;
1217 + }
1218 + idx++;
1219 + },
1220 + });
1221 +
1222 + if (modified) {
1223 + yield cloned;
1224 + }
1225 + }
1226 +}
1227 +
1228 +/**
1229 + * Generator that simplifies single-statement block statements:
1230 + * { statement } -> statement
1231 + */
1232 +function* simplifySingleStatementBlocks(ast: t.File): Generator<t.File> {
1233 + // Count block statements with exactly one statement
1234 + let blockCount = 0;
1235 + t.traverseFast(ast, node => {
1236 + if (t.isBlockStatement(node) && node.body.length === 1) {
1237 + blockCount++;
1238 + }
1239 + });
1240 +
1241 + for (let targetIdx = 0; targetIdx < blockCount; targetIdx++) {
1242 + const cloned = cloneAst(ast);
1243 + let idx = 0;
1244 + let modified = false;
1245 +
1246 + traverse(cloned, {
1247 + BlockStatement(path) {
1248 + if (modified) return;
1249 + if (path.node.body.length === 1 && idx === targetIdx) {
1250 + // Don't unwrap blocks that require BlockStatement syntax
1251 + if (
1252 + t.isFunction(path.parent) ||
1253 + t.isCatchClause(path.parent) ||
1254 + t.isClassMethod(path.parent) ||
1255 + t.isObjectMethod(path.parent) ||
1256 + t.isTryStatement(path.parent)
1257 + ) {
1258 + idx++;
1259 + return;
1260 + }
1261 + path.replaceWith(path.node.body[0]);
1262 + modified = true;
1263 + }
1264 + idx++;
1265 + },
1266 + });
1267 +
1268 + if (modified) {
1269 + yield cloned;
1270 + }
1271 + }
1272 +}
1273 +
1274 +/**
1275 + * Generator that removes array elements one at a time
1276 + */
1277 +function* removeArrayElements(ast: t.File): Generator<t.File> {
1278 + // Collect all array expressions with their element counts
1279 + const arraySites: Array<{arrayIndex: number; elementCount: number}> = [];
1280 + let arrayIndex = 0;
1281 + t.traverseFast(ast, node => {
1282 + if (t.isArrayExpression(node) && node.elements.length > 0) {
1283 + arraySites.push({arrayIndex, elementCount: node.elements.length});
1284 + arrayIndex++;
1285 + }
1286 + });
1287 +
1288 + // For each array, try removing each element one at a time (from end to start)
1289 + for (const {arrayIndex: targetArrayIdx, elementCount} of arraySites) {
1290 + for (let elemIdx = elementCount - 1; elemIdx >= 0; elemIdx--) {
1291 + const cloned = cloneAst(ast);
1292 + let idx = 0;
1293 + let modified = false;
1294 +
1295 + t.traverseFast(cloned, node => {
1296 + if (modified) return;
1297 + if (t.isArrayExpression(node) && node.elements.length > 0) {
1298 + if (idx === targetArrayIdx && elemIdx < node.elements.length) {
1299 + node.elements.splice(elemIdx, 1);
1300 + modified = true;
1301 + }
1302 + idx++;
1303 + }
1304 + });
1305 +
1306 + if (modified) {
1307 + yield cloned;
1308 + }
1309 + }
1310 + }
1311 +}
1312 +
1313 +/**
1314 + * Generator that removes JSX element attributes (props) one at a time
1315 + */
1316 +function* removeJSXAttributes(ast: t.File): Generator<t.File> {
1317 + // Collect all JSX elements with their attribute counts
1318 + const jsxSites: Array<{jsxIndex: number; attrCount: number}> = [];
1319 + let jsxIndex = 0;
1320 + t.traverseFast(ast, node => {
1321 + if (t.isJSXOpeningElement(node) && node.attributes.length > 0) {
1322 + jsxSites.push({jsxIndex, attrCount: node.attributes.length});
1323 + jsxIndex++;
1324 + }
1325 + });
1326 +
1327 + // For each JSX element, try removing each attribute one at a time (from end to start)
1328 + for (const {jsxIndex: targetJsxIdx, attrCount} of jsxSites) {
1329 + for (let attrIdx = attrCount - 1; attrIdx >= 0; attrIdx--) {
1330 + const cloned = cloneAst(ast);
1331 + let idx = 0;
1332 + let modified = false;
1333 +
1334 + t.traverseFast(cloned, node => {
1335 + if (modified) return;
1336 + if (t.isJSXOpeningElement(node) && node.attributes.length > 0) {
1337 + if (idx === targetJsxIdx && attrIdx < node.attributes.length) {
1338 + node.attributes.splice(attrIdx, 1);
1339 + modified = true;
1340 + }
1341 + idx++;
1342 + }
1343 + });
1344 +
1345 + if (modified) {
1346 + yield cloned;
1347 + }
1348 + }
1349 + }
1350 +}
1351 +
1352 +/**
1353 + * Generator that removes JSX element children one at a time
1354 + */
1355 +function* removeJSXChildren(ast: t.File): Generator<t.File> {
1356 + // Collect all JSX elements with children
1357 + const jsxSites: Array<{jsxIndex: number; childCount: number}> = [];
1358 + let jsxIndex = 0;
1359 + t.traverseFast(ast, node => {
1360 + if (t.isJSXElement(node) && node.children.length > 0) {
1361 + jsxSites.push({jsxIndex, childCount: node.children.length});
1362 + jsxIndex++;
1363 + }
1364 + });
1365 +
1366 + // For each JSX element, try removing each child one at a time (from end to start)
1367 + for (const {jsxIndex: targetJsxIdx, childCount} of jsxSites) {
1368 + for (let childIdx = childCount - 1; childIdx >= 0; childIdx--) {
1369 + const cloned = cloneAst(ast);
1370 + let idx = 0;
1371 + let modified = false;
1372 +
1373 + t.traverseFast(cloned, node => {
1374 + if (modified) return;
1375 + if (t.isJSXElement(node) && node.children.length > 0) {
1376 + if (idx === targetJsxIdx && childIdx < node.children.length) {
1377 + node.children.splice(childIdx, 1);
1378 + modified = true;
1379 + }
1380 + idx++;
1381 + }
1382 + });
1383 +
1384 + if (modified) {
1385 + yield cloned;
1386 + }
1387 + }
1388 + }
1389 +}
1390 +
1391 +/**
1392 + * Generator that removes JSX fragment children one at a time
1393 + */
1394 +function* removeJSXFragmentChildren(ast: t.File): Generator<t.File> {
1395 + // Collect all JSX fragments with children
1396 + const fragmentSites: Array<{fragIndex: number; childCount: number}> = [];
1397 + let fragIndex = 0;
1398 + t.traverseFast(ast, node => {
1399 + if (t.isJSXFragment(node) && node.children.length > 0) {
1400 + fragmentSites.push({fragIndex, childCount: node.children.length});
1401 + fragIndex++;
1402 + }
1403 + });
1404 +
1405 + // For each fragment, try removing each child one at a time (from end to start)
1406 + for (const {fragIndex: targetFragIdx, childCount} of fragmentSites) {
1407 + for (let childIdx = childCount - 1; childIdx >= 0; childIdx--) {
1408 + const cloned = cloneAst(ast);
1409 + let idx = 0;
1410 + let modified = false;
1411 +
1412 + t.traverseFast(cloned, node => {
1413 + if (modified) return;
1414 + if (t.isJSXFragment(node) && node.children.length > 0) {
1415 + if (idx === targetFragIdx && childIdx < node.children.length) {
1416 + node.children.splice(childIdx, 1);
1417 + modified = true;
1418 + }
1419 + idx++;
1420 + }
1421 + });
1422 +
1423 + if (modified) {
1424 + yield cloned;
1425 + }
1426 + }
1427 + }
1428 +}
1429 +
1430 +/**
1431 + * Generator that replaces single-element arrays with the element itself
1432 + */
1433 +function* simplifySingleElementArrays(ast: t.File): Generator<t.File> {
1434 + // Count single-element arrays
1435 + let arrayCount = 0;
1436 + t.traverseFast(ast, node => {
1437 + if (t.isArrayExpression(node) && node.elements.length === 1) {
1438 + arrayCount++;
1439 + }
1440 + });
1441 +
1442 + for (let targetIdx = 0; targetIdx < arrayCount; targetIdx++) {
1443 + const cloned = cloneAst(ast);
1444 + let idx = 0;
1445 + let modified = false;
1446 +
1447 + traverse(cloned, {
1448 + ArrayExpression(path) {
1449 + if (modified) return;
1450 + if (path.node.elements.length === 1 && idx === targetIdx) {
1451 + const elem = path.node.elements[0];
1452 + if (t.isExpression(elem)) {
1453 + path.replaceWith(elem);
1454 + modified = true;
1455 + }
1456 + }
1457 + idx++;
1458 + },
1459 + });
1460 +
1461 + if (modified) {
1462 + yield cloned;
1463 + }
1464 + }
1465 +}
1466 +
1467 +/**
1468 + * Generator that replaces single-property objects with the property value.
1469 + * For regular properties: {key: value} -> value
1470 + * For computed properties: {[key]: value} -> key (also try value)
1471 + */
1472 +function* simplifySinglePropertyObjects(ast: t.File): Generator<t.File> {
1473 + // Count single-property objects
1474 + let objectCount = 0;
1475 + t.traverseFast(ast, node => {
1476 + if (t.isObjectExpression(node) && node.properties.length === 1) {
1477 + objectCount++;
1478 + }
1479 + });
1480 +
1481 + // Try replacing with value
1482 + for (let targetIdx = 0; targetIdx < objectCount; targetIdx++) {
1483 + const cloned = cloneAst(ast);
1484 + let idx = 0;
1485 + let modified = false;
1486 +
1487 + traverse(cloned, {
1488 + ObjectExpression(path) {
1489 + if (modified) return;
1490 + if (path.node.properties.length === 1 && idx === targetIdx) {
1491 + const prop = path.node.properties[0];
1492 + if (t.isObjectProperty(prop) && t.isExpression(prop.value)) {
1493 + path.replaceWith(prop.value);
1494 + modified = true;
1495 + }
1496 + }
1497 + idx++;
1498 + },
1499 + });
1500 +
1501 + if (modified) {
1502 + yield cloned;
1503 + }
1504 + }
1505 +
1506 + // For computed properties, also try replacing with key
1507 + for (let targetIdx = 0; targetIdx < objectCount; targetIdx++) {
1508 + const cloned = cloneAst(ast);
1509 + let idx = 0;
1510 + let modified = false;
1511 +
1512 + traverse(cloned, {
1513 + ObjectExpression(path) {
1514 + if (modified) return;
1515 + if (path.node.properties.length === 1 && idx === targetIdx) {
1516 + const prop = path.node.properties[0];
1517 + if (
1518 + t.isObjectProperty(prop) &&
1519 + prop.computed &&
1520 + t.isExpression(prop.key)
1521 + ) {
1522 + path.replaceWith(prop.key);
1523 + modified = true;
1524 + }
1525 + }
1526 + idx++;
1527 + },
1528 + });
1529 +
1530 + if (modified) {
1531 + yield cloned;
1532 + }
1533 + }
1534 +}
1535 +
1536 +/**
1537 + * Generator that removes object properties one at a time
1538 + */
1539 +function* removeObjectProperties(ast: t.File): Generator<t.File> {
1540 + // Collect all object expressions with their property counts
1541 + const objectSites: Array<{objectIndex: number; propCount: number}> = [];
1542 + let objectIndex = 0;
1543 + t.traverseFast(ast, node => {
1544 + if (t.isObjectExpression(node) && node.properties.length > 0) {
1545 + objectSites.push({objectIndex, propCount: node.properties.length});
1546 + objectIndex++;
1547 + }
1548 + });
1549 +
1550 + // For each object, try removing each property one at a time (from end to start)
1551 + for (const {objectIndex: targetObjIdx, propCount} of objectSites) {
1552 + for (let propIdx = propCount - 1; propIdx >= 0; propIdx--) {
1553 + const cloned = cloneAst(ast);
1554 + let idx = 0;
1555 + let modified = false;
1556 +
1557 + t.traverseFast(cloned, node => {
1558 + if (modified) return;
1559 + if (t.isObjectExpression(node) && node.properties.length > 0) {
1560 + if (idx === targetObjIdx && propIdx < node.properties.length) {
1561 + node.properties.splice(propIdx, 1);
1562 + modified = true;
1563 + }
1564 + idx++;
1565 + }
1566 + });
1567 +
1568 + if (modified) {
1569 + yield cloned;
1570 + }
1571 + }
1572 + }
1573 +}
1574 +
1575 +/**
1576 + * Generator that simplifies assignment expressions (a = b) -> a or b
1577 + */
1578 +function* simplifyAssignmentExpressions(ast: t.File): Generator<t.File> {
1579 + // Count assignment expressions
1580 + let assignmentCount = 0;
1581 + t.traverseFast(ast, node => {
1582 + if (t.isAssignmentExpression(node)) {
1583 + assignmentCount++;
1584 + }
1585 + });
1586 +
1587 + // Try replacing with left side (assignment target)
1588 + for (let targetIdx = 0; targetIdx < assignmentCount; targetIdx++) {
1589 + const cloned = cloneAst(ast);
1590 + let idx = 0;
1591 + let modified = false;
1592 +
1593 + traverse(cloned, {
1594 + AssignmentExpression(path) {
1595 + if (modified) return;
1596 + if (idx === targetIdx) {
1597 + const left = path.node.left;
1598 + if (t.isExpression(left)) {
1599 + path.replaceWith(left);
1600 + modified = true;
1601 + }
1602 + }
1603 + idx++;
1604 + },
1605 + });
1606 +
1607 + if (modified) {
1608 + yield cloned;
1609 + }
1610 + }
1611 +
1612 + // Try replacing with right side (assignment value)
1613 + for (let targetIdx = 0; targetIdx < assignmentCount; targetIdx++) {
1614 + const cloned = cloneAst(ast);
1615 + let idx = 0;
1616 + let modified = false;
1617 +
1618 + traverse(cloned, {
1619 + AssignmentExpression(path) {
1620 + if (modified) return;
1621 + if (idx === targetIdx) {
1622 + path.replaceWith(path.node.right);
1623 + modified = true;
1624 + }
1625 + idx++;
1626 + },
1627 + });
1628 +
1629 + if (modified) {
1630 + yield cloned;
1631 + }
1632 + }
1633 +}
1634 +
1635 +/**
1636 + * Generator that simplifies binary expressions (a + b) -> a or b
1637 + */
1638 +function* simplifyBinaryExpressions(ast: t.File): Generator<t.File> {
1639 + // Count binary expressions
1640 + let binaryCount = 0;
1641 + t.traverseFast(ast, node => {
1642 + if (t.isBinaryExpression(node)) {
1643 + binaryCount++;
1644 + }
1645 + });
1646 +
1647 + // Try replacing with left side
1648 + for (let targetIdx = 0; targetIdx < binaryCount; targetIdx++) {
1649 + const cloned = cloneAst(ast);
1650 + let idx = 0;
1651 + let modified = false;
1652 +
1653 + traverse(cloned, {
1654 + BinaryExpression(path) {
1655 + if (modified) return;
1656 + if (idx === targetIdx) {
1657 + path.replaceWith(path.node.left);
1658 + modified = true;
1659 + }
1660 + idx++;
1661 + },
1662 + });
1663 +
1664 + if (modified) {
1665 + yield cloned;
1666 + }
1667 + }
1668 +
1669 + // Try replacing with right side
1670 + for (let targetIdx = 0; targetIdx < binaryCount; targetIdx++) {
1671 + const cloned = cloneAst(ast);
1672 + let idx = 0;
1673 + let modified = false;
1674 +
1675 + traverse(cloned, {
1676 + BinaryExpression(path) {
1677 + if (modified) return;
1678 + if (idx === targetIdx) {
1679 + path.replaceWith(path.node.right);
1680 + modified = true;
1681 + }
1682 + idx++;
1683 + },
1684 + });
1685 +
1686 + if (modified) {
1687 + yield cloned;
1688 + }
1689 + }
1690 +}
1691 +
1692 +/**
1693 + * Generator that simplifies member expressions (obj.value) -> obj
1694 + * For computed expressions: obj[key] -> obj or key
1695 + */
1696 +function* simplifyMemberExpressions(ast: t.File): Generator<t.File> {
1697 + // Count member expressions
1698 + let memberCount = 0;
1699 + t.traverseFast(ast, node => {
1700 + if (t.isMemberExpression(node)) {
1701 + memberCount++;
1702 + }
1703 + });
1704 +
1705 + // Try replacing with object
1706 + for (let targetIdx = 0; targetIdx < memberCount; targetIdx++) {
1707 + const cloned = cloneAst(ast);
1708 + let idx = 0;
1709 + let modified = false;
1710 +
1711 + traverse(cloned, {
1712 + MemberExpression(path) {
1713 + if (modified) return;
1714 + if (idx === targetIdx) {
1715 + path.replaceWith(path.node.object);
1716 + modified = true;
1717 + }
1718 + idx++;
1719 + },
1720 + });
1721 +
1722 + if (modified) {
1723 + yield cloned;
1724 + }
1725 + }
1726 +
1727 + // For computed expressions, also try replacing with key
1728 + for (let targetIdx = 0; targetIdx < memberCount; targetIdx++) {
1729 + const cloned = cloneAst(ast);
1730 + let idx = 0;
1731 + let modified = false;
1732 +
1733 + traverse(cloned, {
1734 + MemberExpression(path) {
1735 + if (modified) return;
1736 + if (idx === targetIdx && path.node.computed) {
1737 + const property = path.node.property;
1738 + if (t.isExpression(property)) {
1739 + path.replaceWith(property);
1740 + modified = true;
1741 + }
1742 + }
1743 + idx++;
1744 + },
1745 + });
1746 +
1747 + if (modified) {
1748 + yield cloned;
1749 + }
1750 + }
1751 +}
1752 +
1753 +/**
1754 + * Helper to collect all unique identifier names in the AST
1755 + */
1756 +function collectUniqueIdentifierNames(ast: t.File): Set<string> {
1757 + const names = new Set<string>();
1758 + t.traverseFast(ast, node => {
1759 + if (t.isIdentifier(node)) {
1760 + names.add(node.name);
1761 + }
1762 + });
1763 + return names;
1764 +}
1765 +
1766 +/**
1767 + * Helper to rename all occurrences of an identifier throughout the AST
1768 + */
1769 +function renameAllIdentifiers(
1770 + ast: t.File,
1771 + oldName: string,
1772 + newName: string,
1773 +): boolean {
1774 + let modified = false;
1775 + t.traverseFast(ast, node => {
1776 + if (t.isIdentifier(node) && node.name === oldName) {
1777 + node.name = newName;
1778 + modified = true;
1779 + }
1780 + });
1781 + return modified;
1782 +}
1783 +
1784 +/**
1785 + * Generator that simplifies identifiers by removing "on" prefix.
1786 + * onClick -> Click
1787 + */
1788 +function* simplifyIdentifiersRemoveOnPrefix(ast: t.File): Generator<t.File> {
1789 + const names = collectUniqueIdentifierNames(ast);
1790 +
1791 + for (const name of names) {
1792 + // Check if name starts with "on" followed by uppercase letter
1793 + if (
1794 + name.length > 2 &&
1795 + name.startsWith('on') &&
1796 + name[2] === name[2].toUpperCase()
1797 + ) {
1798 + const newName = name.slice(2);
1799 + // Skip if the new name would conflict with an existing identifier
1800 + if (names.has(newName)) {
1801 + continue;
1802 + }
1803 + const cloned = cloneAst(ast);
1804 + if (renameAllIdentifiers(cloned, name, newName)) {
1805 + yield cloned;
1806 + }
1807 + }
1808 + }
1809 +}
1810 +
1811 +/**
1812 + * Generator that simplifies identifiers by removing "Ref" suffix.
1813 + * inputRef -> input
1814 + */
1815 +function* simplifyIdentifiersRemoveRefSuffix(ast: t.File): Generator<t.File> {
1816 + const names = collectUniqueIdentifierNames(ast);
1817 +
1818 + for (const name of names) {
1819 + // Check if name ends with "Ref" and has more characters before it
1820 + if (name.length > 3 && name.endsWith('Ref')) {
1821 + const newName = name.slice(0, -3);
1822 + // Skip if the new name would conflict with an existing identifier
1823 + if (names.has(newName)) {
1824 + continue;
1825 + }
1826 + // Skip if new name would be empty or just whitespace
1827 + if (newName.length === 0) {
1828 + continue;
1829 + }
1830 + const cloned = cloneAst(ast);
1831 + if (renameAllIdentifiers(cloned, name, newName)) {
1832 + yield cloned;
1833 + }
1834 + }
1835 + }
1836 +}
1837 +
1838 +/**
1839 + * Generator that rewrites "ref" identifier to "ref_" to avoid conflicts.
1840 + */
1841 +function* simplifyIdentifiersRenameRef(ast: t.File): Generator<t.File> {
1842 + const names = collectUniqueIdentifierNames(ast);
1843 +
1844 + if (names.has('ref')) {
1845 + // Only rename if ref_ doesn't already exist
1846 + if (!names.has('ref_')) {
1847 + const cloned = cloneAst(ast);
1848 + if (renameAllIdentifiers(cloned, 'ref', 'ref_')) {
1849 + yield cloned;
1850 + }
1851 + }
1852 + }
1853 +}
1854 +
1855 +/**
1856 + * All simplification strategies in order of priority (coarse to fine)
1857 + */
1858 +const simplificationStrategies = [
1859 + {name: 'removeStatements', generator: removeStatements},
1860 + {name: 'removeCallArguments', generator: removeCallArguments},
1861 + {name: 'removeArrayElements', generator: removeArrayElements},
1862 + {name: 'removeObjectProperties', generator: removeObjectProperties},
1863 + {name: 'removeJSXAttributes', generator: removeJSXAttributes},
1864 + {name: 'removeJSXChildren', generator: removeJSXChildren},
1865 + {name: 'removeJSXFragmentChildren', generator: removeJSXFragmentChildren},
1866 + {name: 'simplifyCallExpressions', generator: simplifyCallExpressions},
1867 + {name: 'simplifyConditionals', generator: simplifyConditionals},
1868 + {name: 'simplifyLogicalExpressions', generator: simplifyLogicalExpressions},
1869 + {name: 'simplifyBinaryExpressions', generator: simplifyBinaryExpressions},
1870 + {
1871 + name: 'simplifyAssignmentExpressions',
1872 + generator: simplifyAssignmentExpressions,
1873 + },
1874 + {name: 'simplifySingleElementArrays', generator: simplifySingleElementArrays},
1875 + {
1876 + name: 'simplifySinglePropertyObjects',
1877 + generator: simplifySinglePropertyObjects,
1878 + },
1879 + {name: 'simplifyMemberExpressions', generator: simplifyMemberExpressions},
1880 + {name: 'simplifyOptionalChains', generator: simplifyOptionalChains},
1881 + {name: 'simplifyAwaitExpressions', generator: simplifyAwaitExpressions},
1882 + {name: 'simplifyIfStatements', generator: simplifyIfStatements},
1883 + {name: 'simplifySwitchStatements', generator: simplifySwitchStatements},
1884 + {name: 'simplifyWhileStatements', generator: simplifyWhileStatements},
1885 + {name: 'simplifyDoWhileStatements', generator: simplifyDoWhileStatements},
1886 + {name: 'simplifyForStatements', generator: simplifyForStatements},
1887 + {name: 'simplifyForInStatements', generator: simplifyForInStatements},
1888 + {name: 'simplifyForOfStatements', generator: simplifyForOfStatements},
1889 + {
1890 + name: 'simplifyVariableDeclarations',
1891 + generator: simplifyVariableDeclarations,
1892 + },
1893 + {name: 'simplifyTryStatements', generator: simplifyTryStatements},
1894 + {
1895 + name: 'simplifySingleStatementBlocks',
1896 + generator: simplifySingleStatementBlocks,
1897 + },
1898 + {
1899 + name: 'simplifyIdentifiersRemoveOnPrefix',
1900 + generator: simplifyIdentifiersRemoveOnPrefix,
1901 + },
1902 + {
1903 + name: 'simplifyIdentifiersRemoveRefSuffix',
1904 + generator: simplifyIdentifiersRemoveRefSuffix,
1905 + },
1906 + {
1907 + name: 'simplifyIdentifiersRenameRef',
1908 + generator: simplifyIdentifiersRenameRef,
1909 + },
1910 +];
1911 +
1912 +type MinimizeResult =
1913 + | {kind: 'success'}
1914 + | {kind: 'minimal'}
1915 + | {kind: 'minimized'; source: string};
1916 +
1917 +/**
1918 + * Core minimization loop that attempts to reduce the input source code
1919 + * while preserving the compiler error.
1920 + */
1921 +export function minimize(
1922 + input: string,
1923 + filename: string,
1924 + language: 'flow' | 'typescript',
1925 + sourceType: 'module' | 'script',
1926 +): MinimizeResult {
1927 + // Load the compiler plugin
1928 + const importedCompilerPlugin = require(PROJECT_SRC) as Record<
1929 + string,
1930 + unknown
1931 + >;
1932 + const BabelPluginReactCompiler = importedCompilerPlugin[
1933 + 'default'
1934 + ] as PluginObj;
1935 + const parseConfigPragmaForTests = importedCompilerPlugin[
1936 + PARSE_CONFIG_PRAGMA_IMPORT
1937 + ] as typeof ParseConfigPragma;
1938 +
1939 + // Get the initial error
1940 + const initialResult = compileAndGetError(
1941 + input,
1942 + filename,
1943 + language,
1944 + sourceType,
1945 + BabelPluginReactCompiler,
1946 + parseConfigPragmaForTests,
1947 + );
1948 +
1949 + if (initialResult.kind === 'success') {
1950 + return {kind: 'success'};
1951 + }
1952 +
1953 + if (initialResult.kind === 'parse_error') {
1954 + return {kind: 'success'};
1955 + }
1956 +
1957 + const targetError = initialResult;
1958 +
1959 + // Parse the initial AST
1960 + let currentAst = parseInput(input, filename, language, sourceType);
1961 + let currentCode = input;
1962 + let changed = true;
1963 + let iterations = 0;
1964 + const maxIterations = 1000; // Safety limit
1965 +
1966 + process.stdout.write('\nMinimizing');
1967 +
1968 + while (changed && iterations < maxIterations) {
1969 + changed = false;
1970 + iterations++;
1971 +
1972 + // Try each simplification strategy
1973 + for (const strategy of simplificationStrategies) {
1974 + const generator = strategy.generator(currentAst);
1975 +
1976 + for (const candidateAst of generator) {
1977 + let candidateCode: string;
1978 + try {
1979 + candidateCode = astToCode(candidateAst);
1980 + } catch {
1981 + // If code generation fails, skip this candidate
1982 + continue;
1983 + }
1984 +
1985 + const result = compileAndGetError(
1986 + candidateCode,
1987 + filename,
1988 + language,
1989 + sourceType,
1990 + BabelPluginReactCompiler,
1991 + parseConfigPragmaForTests,
1992 + );
1993 +
1994 + if (errorsMatch(targetError, result)) {
1995 + // This simplification preserves the error, keep it
1996 + currentAst = candidateAst;
1997 + currentCode = candidateCode;
1998 + changed = true;
1999 + process.stdout.write('.');
2000 + break; // Restart from the beginning with the new AST
2001 + }
2002 + }
2003 +
2004 + if (changed) {
2005 + break; // Restart the outer loop
2006 + }
2007 + }
2008 + }
2009 +
2010 + console.log('\n');
2011 +
2012 + // Check if any minimization was achieved
2013 + if (currentCode === input) {
2014 + return {kind: 'minimal'};
2015 + }
2016 +
2017 + return {kind: 'minimized', source: currentCode};
2018 +}
2019 +
2020 +/**
2021 + * Main minimize function that reads the input file, runs minimization,
2022 + * and reports results.
2023 + */
2024 +export async function runMinimize(options: MinimizeOptions): Promise<void> {
2025 + // Resolve the input path
2026 + const inputPath = path.isAbsolute(options.path)
2027 + ? options.path
2028 + : path.resolve(process.cwd(), options.path);
2029 +
2030 + // Check if file exists
2031 + if (!fs.existsSync(inputPath)) {
2032 + console.error(`Error: File not found: ${inputPath}`);
2033 + process.exit(1);
2034 + }
2035 +
2036 + // Read the input file
2037 + const input = fs.readFileSync(inputPath, 'utf-8');
2038 + const filename = path.basename(inputPath);
2039 + const firstLine = input.substring(0, input.indexOf('\n'));
2040 + const language = parseLanguage(firstLine);
2041 + const sourceType = parseSourceType(firstLine);
2042 +
2043 + console.log(`Minimizing: ${inputPath}`);
2044 +
2045 + const originalLines = input.split('\n').length;
2046 +
2047 + // Run the minimization
2048 + const result = minimize(input, filename, language, sourceType);
2049 +
2050 + if (result.kind === 'success') {
2051 + console.log('Could not minimize: the input compiles successfully.');
2052 + process.exit(0);
2053 + }
2054 +
2055 + if (result.kind === 'minimal') {
2056 + console.log(
2057 + 'Could not minimize: the input fails but is already minimal and cannot be reduced further.',
2058 + );
2059 + process.exit(0);
2060 + }
2061 +
2062 + // Output the minimized code
2063 + console.log('--- Minimized Code ---');
2064 + console.log(result.source);
2065 +
2066 + const minimizedLines = result.source.split('\n').length;
2067 + console.log(
2068 + `\nReduced from ${originalLines} lines to ${minimizedLines} lines`,
2069 + );
2070 +}
compiler/packages/snap/src/runner.ts
+77 -34
@@ -23,6 +23,7 @@ import {
23 } from './runner-watch';
24 import * as runnerWorker from './runner-worker';
25 import {execSync} from 'child_process';
26 +import {runMinimize} from './minimize';
27
28 const WORKER_PATH = require.resolve('./runner-worker.js');
29 const NUM_WORKERS = cpus().length - 1;
@@ -38,40 +39,76 @@ type RunnerOptions = {
39 debug: boolean;
40 };
41
41 -const opts: RunnerOptions = yargs
42 - .boolean('sync')
43 - .describe(
44 - 'sync',
45 - 'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.',
46 - )
47 - .default('sync', false)
48 - .boolean('worker-threads')
49 - .describe(
50 - 'worker-threads',
51 - 'Run compiler in worker threads (instead of subprocesses). Defaults to true.',
42 +async function runTestCommand(opts: RunnerOptions): Promise<void> {
43 + await main(opts);
44 +}
45 +
46 +async function runMinimizeCommand(path: string): Promise<void> {
47 + await runMinimize({path});
48 +}
49 +
50 +yargs(hideBin(process.argv))
51 + .command(
52 + ['test', '$0'],
53 + 'Run compiler tests',
54 + yargs => {
55 + return yargs
56 + .boolean('sync')
57 + .describe(
58 + 'sync',
59 + 'Run compiler in main thread (instead of using worker threads or subprocesses). Defaults to false.',
60 + )
61 + .default('sync', false)
62 + .boolean('worker-threads')
63 + .describe(
64 + 'worker-threads',
65 + 'Run compiler in worker threads (instead of subprocesses). Defaults to true.',
66 + )
67 + .default('worker-threads', true)
68 + .boolean('watch')
69 + .describe(
70 + 'watch',
71 + 'Run compiler in watch mode, re-running after changes',
72 + )
73 + .alias('w', 'watch')
74 + .default('watch', false)
75 + .boolean('update')
76 + .alias('u', 'update')
77 + .describe('update', 'Update fixtures')
78 + .default('update', false)
79 + .string('pattern')
80 + .alias('p', 'pattern')
81 + .describe(
82 + 'pattern',
83 + 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
84 + )
85 + .boolean('debug')
86 + .alias('d', 'debug')
87 + .describe('debug', 'Enable debug logging to print HIR for each pass')
88 + .default('debug', false);
89 + },
90 + async argv => {
91 + await runTestCommand(argv as RunnerOptions);
92 + },
93 )
53 - .default('worker-threads', true)
54 - .boolean('watch')
55 - .describe('watch', 'Run compiler in watch mode, re-running after changes')
56 - .alias('w', 'watch')
57 - .default('watch', false)
58 - .boolean('update')
59 - .alias('u', 'update')
60 - .describe('update', 'Update fixtures')
61 - .default('update', false)
62 - .string('pattern')
63 - .alias('p', 'pattern')
64 - .describe(
65 - 'pattern',
66 - 'Optional glob pattern to filter fixtures (e.g., "error.*", "use-memo")',
94 + .command(
95 + 'minimize',
96 + 'Minimize a test case to reproduce a compiler error',
97 + yargs => {
98 + return yargs
99 + .string('path')
100 + .alias('p', 'path')
101 + .describe('path', 'Path to the file to minimize')
102 + .demandOption('path');
103 + },
104 + async argv => {
105 + await runMinimizeCommand(argv.path as string);
106 + },
107 )
68 - .boolean('debug')
69 - .alias('d', 'debug')
70 - .describe('debug', 'Enable debug logging to print HIR for each pass')
71 - .default('debug', false)
108 .help('help')
109 .strict()
74 - .parseSync(hideBin(process.argv)) as RunnerOptions;
110 + .demandCommand()
111 + .parse();
112
113 /**
114 * Do a test run and return the test results
@@ -82,6 +119,7 @@ async function runFixtures(
119 compilerVersion: number,
120 debug: boolean,
121 requireSingleFixture: boolean,
122 + sync: boolean,
123 ): Promise<TestResults> {
124 // We could in theory be fancy about tracking the contents of the fixtures
125 // directory via our file subscription, but it's simpler to just re-read
@@ -91,7 +129,7 @@ async function runFixtures(
129 const shouldLog = debug && (!requireSingleFixture || isOnlyFixture);
130
131 let entries: Array<[string, TestResult]>;
94 - if (!opts.sync) {
132 + if (!sync) {
133 // Note: promise.all to ensure parallelism when enabled
134 const work: Array<Promise<[string, TestResult]>> = [];
135 for (const [fixtureName, fixture] of fixtures) {
@@ -123,6 +161,7 @@ async function runFixtures(
161 async function onChange(
162 worker: Worker & typeof runnerWorker,
163 state: RunnerState,
164 + sync: boolean,
165 ) {
166 const {compilerVersion, isCompilerBuildValid, mode, filter, debug} = state;
167 if (isCompilerBuildValid) {
@@ -140,6 +179,7 @@ async function onChange(
179 compilerVersion,
180 debug,
181 true, // requireSingleFixture in watch mode
182 + sync,
183 );
184 const end = performance.now();
185
@@ -192,7 +232,11 @@ export async function main(opts: RunnerOptions): Promise<void> {
232 const shouldWatch = opts.watch;
233
234 if (shouldWatch) {
195 - makeWatchRunner(state => onChange(worker, state), opts.debug, opts.pattern);
235 + makeWatchRunner(
236 + state => onChange(worker, state, opts.sync),
237 + opts.debug,
238 + opts.pattern,
239 + );
240 if (opts.pattern) {
241 /**
242 * Warm up wormers when in watch mode. Loading the Forget babel plugin
@@ -251,6 +295,7 @@ export async function main(opts: RunnerOptions): Promise<void> {
295 0,
296 opts.debug,
297 false, // no requireSingleFixture in non-watch mode
298 + opts.sync,
299 );
300 if (opts.update) {
301 update(results);
@@ -269,5 +314,3 @@ export async function main(opts: RunnerOptions): Promise<void> {
314 );
315 }
316 }
272 -
273 -main(opts).catch(error => console.error(error));
compiler/yarn.lock
+40 -1
@@ -268,6 +268,17 @@
268 source-map "^0.5.0"
269 trim-right "^1.0.1"
270
271 +"@babel/generator@^7.19.1":
272 + version "7.28.6"
273 + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1"
274 + integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==
275 + dependencies:
276 + "@babel/parser" "^7.28.6"
277 + "@babel/types" "^7.28.6"
278 + "@jridgewell/gen-mapping" "^0.3.12"
279 + "@jridgewell/trace-mapping" "^0.3.28"
280 + jsesc "^3.0.2"
281 +
282 "@babel/generator@^7.2.0", "@babel/generator@^7.26.0", "@babel/generator@^7.26.10", "@babel/generator@^7.26.3", "@babel/generator@^7.27.0", "@babel/generator@^7.7.2", "@babel/generator@^7.7.4":
283 version "7.27.0"
284 resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz"
@@ -662,6 +673,13 @@
673 resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz"
674 integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==
675
676 +"@babel/parser@^7.28.6":
677 + version "7.28.6"
678 + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd"
679 + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==
680 + dependencies:
681 + "@babel/types" "^7.28.6"
682 +
683 "@babel/parser@^7.7.4":
684 version "7.21.4"
685 resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz"
@@ -1592,7 +1610,7 @@
1610 debug "^4.3.1"
1611 globals "^11.1.0"
1612
1595 -"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
1613 +"@babel/types@7.26.3", "@babel/types@^7.0.0", "@babel/types@^7.19.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.21.2", "@babel/types@^7.24.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.3", "@babel/types@^7.27.0", "@babel/types@^7.27.1", "@babel/types@^7.28.6", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.4":
1614 version "7.26.3"
1615 resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0"
1616 integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==
@@ -2732,6 +2750,14 @@
2750 "@types/yargs" "^17.0.8"
2751 chalk "^4.0.0"
2752
2753 +"@jridgewell/gen-mapping@^0.3.12":
2754 + version "0.3.13"
2755 + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
2756 + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
2757 + dependencies:
2758 + "@jridgewell/sourcemap-codec" "^1.5.0"
2759 + "@jridgewell/trace-mapping" "^0.3.24"
2760 +
2761 "@jridgewell/gen-mapping@^0.3.2":
2762 version "0.3.8"
2763 resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz"
@@ -2765,6 +2791,11 @@
2791 resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz"
2792 integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
2793
2794 +"@jridgewell/sourcemap-codec@^1.5.0":
2795 + version "1.5.5"
2796 + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
2797 + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
2798 +
2799 "@jridgewell/trace-mapping@0.3.9":
2800 version "0.3.9"
2801 resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz"
@@ -2789,6 +2820,14 @@
2820 "@jridgewell/resolve-uri" "3.1.0"
2821 "@jridgewell/sourcemap-codec" "1.4.14"
2822
2823 +"@jridgewell/trace-mapping@^0.3.28":
2824 + version "0.3.31"
2825 + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
2826 + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
2827 + dependencies:
2828 + "@jridgewell/resolve-uri" "^3.1.0"
2829 + "@jridgewell/sourcemap-codec" "^1.4.14"
2830 +
2831 "@modelcontextprotocol/sdk@^1.9.0":
2832 version "1.9.0"
2833 resolved "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz"