main
ts 2,299 lines 57.7 KB
Raw
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 {parseInput} from './compiler.js';
15 import {
16 PARSE_CONFIG_PRAGMA_IMPORT,
17 BABEL_PLUGIN_SRC,
18 BABEL_PLUGIN_RUST_SRC,
19 } from './constants.js';
20
21 type CompileSuccess = {kind: 'success'};
22 type CompileParseError = {kind: 'parse_error'; message: string};
23 type CompileErrors = {
24 kind: 'errors';
25 errors: Array<{category: string; reason: string; description: string | null}>;
26 };
27 type CompileResult = CompileSuccess | CompileParseError | CompileErrors;
28
29 /**
30 * Compile code and extract error information
31 */
32 function compileAndGetError(
33 code: string,
34 filename: string,
35 language: 'flow' | 'typescript',
36 sourceType: 'module' | 'script',
37 plugin: PluginObj,
38 parseConfigPragmaFn: typeof ParseConfigPragma,
39 ): CompileResult {
40 let ast: t.File;
41 try {
42 ast = parseInput(code, filename, language, sourceType);
43 } catch (e: unknown) {
44 return {kind: 'parse_error', message: (e as Error).message};
45 }
46
47 const firstLine = code.substring(0, code.indexOf('\n'));
48 const config = parseConfigPragmaFn(firstLine, {compilationMode: 'all'});
49 const options = {
50 ...config,
51 environment: {
52 ...config.environment,
53 },
54 logger: {
55 logEvent: () => {},
56 debugLogIRs: () => {},
57 },
58 enableReanimatedCheck: false,
59 };
60
61 try {
62 transformFromAstSync(ast, code, {
63 filename: '/' + filename,
64 highlightCode: false,
65 retainLines: true,
66 compact: true,
67 plugins: [[plugin, options]],
68 sourceType: 'module',
69 ast: false,
70 cloneInputAst: true,
71 configFile: false,
72 babelrc: false,
73 });
74 return {kind: 'success'};
75 } catch (e: unknown) {
76 const error = e as Error & {
77 details?: Array<{
78 category: string;
79 reason: string;
80 description: string | null;
81 }>;
82 };
83 // Check if this is a CompilerError with details
84 if (error.details && error.details.length > 0) {
85 return {
86 kind: 'errors',
87 errors: error.details.map(detail => ({
88 category: detail.category,
89 reason: detail.reason,
90 description: detail.description,
91 })),
92 };
93 }
94 // Fallback for other errors - use error name/message
95 return {
96 kind: 'errors',
97 errors: [
98 {
99 category: error.name ?? 'Error',
100 reason: error.message,
101 description: null,
102 },
103 ],
104 };
105 }
106 }
107
108 /**
109 * Check if two compile errors match
110 */
111 function errorsMatch(a: CompileErrors, b: CompileResult): boolean {
112 if (b.kind !== 'errors') {
113 return false;
114 }
115 if (a.errors.length !== b.errors.length) {
116 return false;
117 }
118 for (let i = 0; i < a.errors.length; i++) {
119 if (
120 a.errors[i].category !== b.errors[i].category ||
121 a.errors[i].reason !== b.errors[i].reason ||
122 a.errors[i].description !== b.errors[i].description
123 ) {
124 return false;
125 }
126 }
127 return true;
128 }
129
130 /**
131 * Convert AST to code string
132 */
133 function astToCode(ast: t.File): string {
134 return generate(ast).code;
135 }
136
137 /**
138 * Clone an AST node deeply
139 */
140 function cloneAst(ast: t.File): t.File {
141 return t.cloneNode(ast, true);
142 }
143
144 /**
145 * Generator that yields ASTs with statements removed one at a time
146 */
147 function* removeStatements(ast: t.File): Generator<t.File> {
148 // Collect all statement locations: which container (by index) and which statement index
149 const statementLocations: Array<{containerIndex: number; stmtIndex: number}> =
150 [];
151 let containerIndex = 0;
152
153 t.traverseFast(ast, node => {
154 if (t.isBlockStatement(node) || t.isProgram(node)) {
155 const body = node.body as t.Statement[];
156 // Iterate in reverse order so removing later statements first
157 for (let i = body.length - 1; i >= 0; i--) {
158 statementLocations.push({containerIndex, stmtIndex: i});
159 }
160 containerIndex++;
161 }
162 });
163
164 for (const {
165 containerIndex: targetContainerIdx,
166 stmtIndex,
167 } of statementLocations) {
168 const cloned = cloneAst(ast);
169 let idx = 0;
170 let modified = false;
171
172 t.traverseFast(cloned, node => {
173 if (modified) return;
174 if (t.isBlockStatement(node) || t.isProgram(node)) {
175 if (idx === targetContainerIdx) {
176 const body = node.body as t.Statement[];
177 if (stmtIndex < body.length) {
178 body.splice(stmtIndex, 1);
179 modified = true;
180 }
181 }
182 idx++;
183 }
184 });
185
186 if (modified) {
187 yield cloned;
188 }
189 }
190 }
191
192 /**
193 * Generator that yields ASTs with call arguments removed one at a time
194 */
195 function* removeCallArguments(ast: t.File): Generator<t.File> {
196 // Collect all call expressions with their argument counts
197 const callSites: Array<{callIndex: number; argCount: number}> = [];
198 let callIndex = 0;
199 t.traverseFast(ast, node => {
200 if (t.isCallExpression(node) && node.arguments.length > 0) {
201 callSites.push({callIndex, argCount: node.arguments.length});
202 callIndex++;
203 }
204 });
205
206 // For each call site, try removing each argument one at a time (from end to start)
207 for (const {callIndex: targetCallIdx, argCount} of callSites) {
208 for (let argIdx = argCount - 1; argIdx >= 0; argIdx--) {
209 const cloned = cloneAst(ast);
210 let idx = 0;
211 let modified = false;
212
213 t.traverseFast(cloned, node => {
214 if (modified) return;
215 if (t.isCallExpression(node) && node.arguments.length > 0) {
216 if (idx === targetCallIdx && argIdx < node.arguments.length) {
217 node.arguments.splice(argIdx, 1);
218 modified = true;
219 }
220 idx++;
221 }
222 });
223
224 if (modified) {
225 yield cloned;
226 }
227 }
228 }
229 }
230
231 /**
232 * Generator that yields ASTs with function parameters removed one at a time
233 */
234 function* removeFunctionParameters(ast: t.File): Generator<t.File> {
235 // Collect all functions with parameters
236 const funcSites: Array<{funcIndex: number; paramCount: number}> = [];
237 let funcIndex = 0;
238 t.traverseFast(ast, node => {
239 if (t.isFunction(node) && node.params.length > 0) {
240 funcSites.push({funcIndex, paramCount: node.params.length});
241 funcIndex++;
242 }
243 });
244
245 // For each function, try removing each parameter (from end to start)
246 for (const {funcIndex: targetFuncIdx, paramCount} of funcSites) {
247 for (let paramIdx = paramCount - 1; paramIdx >= 0; paramIdx--) {
248 const cloned = cloneAst(ast);
249 let idx = 0;
250 let modified = false;
251
252 t.traverseFast(cloned, node => {
253 if (modified) return;
254 if (t.isFunction(node) && node.params.length > 0) {
255 if (idx === targetFuncIdx && paramIdx < node.params.length) {
256 node.params.splice(paramIdx, 1);
257 modified = true;
258 }
259 idx++;
260 }
261 });
262
263 if (modified) {
264 yield cloned;
265 }
266 }
267 }
268 }
269
270 /**
271 * Generator that simplifies call expressions by replacing them with their arguments.
272 * For single argument: foo(x) -> x
273 * For multiple arguments: foo(x, y) -> [x, y]
274 */
275 function* simplifyCallExpressions(ast: t.File): Generator<t.File> {
276 // Count call expressions with arguments
277 let callCount = 0;
278 t.traverseFast(ast, node => {
279 if (t.isCallExpression(node) && node.arguments.length > 0) {
280 callCount++;
281 }
282 });
283
284 // For each call, try replacing with arguments
285 for (let targetIdx = 0; targetIdx < callCount; targetIdx++) {
286 const cloned = cloneAst(ast);
287 let idx = 0;
288 let modified = false;
289
290 traverse(cloned, {
291 CallExpression(path) {
292 if (modified) return;
293 if (path.node.arguments.length > 0 && idx === targetIdx) {
294 const args = path.node.arguments;
295 // Filter to only Expression arguments (not SpreadElement)
296 const exprArgs = args.filter((arg): arg is t.Expression =>
297 t.isExpression(arg),
298 );
299 if (exprArgs.length === 0) {
300 idx++;
301 return;
302 }
303 if (exprArgs.length === 1) {
304 // Single argument: replace call with the argument
305 path.replaceWith(exprArgs[0]);
306 } else {
307 // Multiple arguments: replace call with array of arguments
308 path.replaceWith(t.arrayExpression(exprArgs));
309 }
310 modified = true;
311 }
312 idx++;
313 },
314 });
315
316 if (modified) {
317 yield cloned;
318 }
319 }
320
321 // Also try replacing with each individual argument for multi-arg calls
322 for (let targetIdx = 0; targetIdx < callCount; targetIdx++) {
323 // First, find the arg count for this call
324 let argCount = 0;
325 let currentIdx = 0;
326 t.traverseFast(ast, node => {
327 if (t.isCallExpression(node) && node.arguments.length > 0) {
328 if (currentIdx === targetIdx) {
329 argCount = node.arguments.length;
330 }
331 currentIdx++;
332 }
333 });
334
335 // Try replacing with each argument individually
336 for (let argIdx = 0; argIdx < argCount; argIdx++) {
337 const cloned = cloneAst(ast);
338 let idx = 0;
339 let modified = false;
340
341 traverse(cloned, {
342 CallExpression(path) {
343 if (modified) return;
344 if (path.node.arguments.length > 0 && idx === targetIdx) {
345 const arg = path.node.arguments[argIdx];
346 if (t.isExpression(arg)) {
347 path.replaceWith(arg);
348 modified = true;
349 }
350 }
351 idx++;
352 },
353 });
354
355 if (modified) {
356 yield cloned;
357 }
358 }
359 }
360 }
361
362 /**
363 * Generator that simplifies conditional expressions (a ? b : c) -> a, b, or c
364 */
365 function* simplifyConditionals(ast: t.File): Generator<t.File> {
366 // Count conditionals
367 let condCount = 0;
368 t.traverseFast(ast, node => {
369 if (t.isConditionalExpression(node)) {
370 condCount++;
371 }
372 });
373
374 // Try replacing with test condition
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.test);
385 modified = true;
386 }
387 idx++;
388 },
389 });
390
391 if (modified) {
392 yield cloned;
393 }
394 }
395
396 // Try replacing with consequent
397 for (let targetIdx = 0; targetIdx < condCount; targetIdx++) {
398 const cloned = cloneAst(ast);
399 let modified = false;
400 let idx = 0;
401
402 traverse(cloned, {
403 ConditionalExpression(path) {
404 if (modified) return;
405 if (idx === targetIdx) {
406 path.replaceWith(path.node.consequent);
407 modified = true;
408 }
409 idx++;
410 },
411 });
412
413 if (modified) {
414 yield cloned;
415 }
416 }
417
418 // Also try replacing with alternate
419 for (let targetIdx = 0; targetIdx < condCount; targetIdx++) {
420 const cloned = cloneAst(ast);
421 let modified = false;
422 let idx = 0;
423
424 traverse(cloned, {
425 ConditionalExpression(path) {
426 if (modified) return;
427 if (idx === targetIdx) {
428 path.replaceWith(path.node.alternate);
429 modified = true;
430 }
431 idx++;
432 },
433 });
434
435 if (modified) {
436 yield cloned;
437 }
438 }
439 }
440
441 /**
442 * Generator that simplifies logical expressions (a && b) -> a or b
443 */
444 function* simplifyLogicalExpressions(ast: t.File): Generator<t.File> {
445 // Count logical expressions
446 let logicalCount = 0;
447 t.traverseFast(ast, node => {
448 if (t.isLogicalExpression(node)) {
449 logicalCount++;
450 }
451 });
452
453 // Try replacing with left side
454 for (let targetIdx = 0; targetIdx < logicalCount; targetIdx++) {
455 const cloned = cloneAst(ast);
456 let idx = 0;
457 let modified = false;
458
459 traverse(cloned, {
460 LogicalExpression(path) {
461 if (modified) return;
462 if (idx === targetIdx) {
463 path.replaceWith(path.node.left);
464 modified = true;
465 }
466 idx++;
467 },
468 });
469
470 if (modified) {
471 yield cloned;
472 }
473 }
474
475 // Try replacing with right side
476 for (let targetIdx = 0; targetIdx < logicalCount; targetIdx++) {
477 const cloned = cloneAst(ast);
478 let idx = 0;
479 let modified = false;
480
481 traverse(cloned, {
482 LogicalExpression(path) {
483 if (modified) return;
484 if (idx === targetIdx) {
485 path.replaceWith(path.node.right);
486 modified = true;
487 }
488 idx++;
489 },
490 });
491
492 if (modified) {
493 yield cloned;
494 }
495 }
496 }
497
498 /**
499 * Generator that simplifies optional chains (a?.b) -> a.b
500 */
501 function* simplifyOptionalChains(ast: t.File): Generator<t.File> {
502 // Count optional expressions
503 let optionalCount = 0;
504 t.traverseFast(ast, node => {
505 if (
506 t.isOptionalMemberExpression(node) ||
507 t.isOptionalCallExpression(node)
508 ) {
509 optionalCount++;
510 }
511 });
512
513 for (let targetIdx = 0; targetIdx < optionalCount; targetIdx++) {
514 const cloned = cloneAst(ast);
515 let idx = 0;
516 let modified = false;
517
518 traverse(cloned, {
519 OptionalMemberExpression(path) {
520 if (modified) return;
521 if (idx === targetIdx) {
522 const {object, property, computed} = path.node;
523 path.replaceWith(t.memberExpression(object, property, computed));
524 modified = true;
525 }
526 idx++;
527 },
528 OptionalCallExpression(path) {
529 if (modified) return;
530 if (idx === targetIdx) {
531 const {callee, arguments: args} = path.node;
532 if (t.isExpression(callee)) {
533 path.replaceWith(t.callExpression(callee, args));
534 modified = true;
535 }
536 }
537 idx++;
538 },
539 });
540
541 if (modified) {
542 yield cloned;
543 }
544 }
545 }
546
547 /**
548 * Generator that simplifies await expressions: await expr -> expr
549 */
550 function* simplifyAwaitExpressions(ast: t.File): Generator<t.File> {
551 // Count await expressions
552 let awaitCount = 0;
553 t.traverseFast(ast, node => {
554 if (t.isAwaitExpression(node)) {
555 awaitCount++;
556 }
557 });
558
559 for (let targetIdx = 0; targetIdx < awaitCount; targetIdx++) {
560 const cloned = cloneAst(ast);
561 let idx = 0;
562 let modified = false;
563
564 traverse(cloned, {
565 AwaitExpression(path) {
566 if (modified) return;
567 if (idx === targetIdx) {
568 path.replaceWith(path.node.argument);
569 modified = true;
570 }
571 idx++;
572 },
573 });
574
575 if (modified) {
576 yield cloned;
577 }
578 }
579 }
580
581 /**
582 * Generator that simplifies if statements:
583 * - Replace with test expression (as expression statement)
584 * - Replace with consequent block
585 * - Replace with alternate block (if present)
586 */
587 function* simplifyIfStatements(ast: t.File): Generator<t.File> {
588 // Count if statements
589 let ifCount = 0;
590 t.traverseFast(ast, node => {
591 if (t.isIfStatement(node)) {
592 ifCount++;
593 }
594 });
595
596 // Try replacing with test expression
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) {
606 path.replaceWith(t.expressionStatement(path.node.test));
607 modified = true;
608 }
609 idx++;
610 },
611 });
612
613 if (modified) {
614 yield cloned;
615 }
616 }
617
618 // Try replacing with consequent
619 for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) {
620 const cloned = cloneAst(ast);
621 let idx = 0;
622 let modified = false;
623
624 traverse(cloned, {
625 IfStatement(path) {
626 if (modified) return;
627 if (idx === targetIdx) {
628 path.replaceWith(path.node.consequent);
629 modified = true;
630 }
631 idx++;
632 },
633 });
634
635 if (modified) {
636 yield cloned;
637 }
638 }
639
640 // Try replacing with alternate (if present)
641 for (let targetIdx = 0; targetIdx < ifCount; targetIdx++) {
642 const cloned = cloneAst(ast);
643 let idx = 0;
644 let modified = false;
645
646 traverse(cloned, {
647 IfStatement(path) {
648 if (modified) return;
649 if (idx === targetIdx && path.node.alternate) {
650 path.replaceWith(path.node.alternate);
651 modified = true;
652 }
653 idx++;
654 },
655 });
656
657 if (modified) {
658 yield cloned;
659 }
660 }
661 }
662
663 /**
664 * Generator that simplifies switch statements:
665 * - Replace with discriminant expression
666 * - Replace with each case's consequent statements
667 */
668 function* simplifySwitchStatements(ast: t.File): Generator<t.File> {
669 // Count switch statements
670 let switchCount = 0;
671 t.traverseFast(ast, node => {
672 if (t.isSwitchStatement(node)) {
673 switchCount++;
674 }
675 });
676
677 // Try replacing with discriminant
678 for (let targetIdx = 0; targetIdx < switchCount; targetIdx++) {
679 const cloned = cloneAst(ast);
680 let idx = 0;
681 let modified = false;
682
683 traverse(cloned, {
684 SwitchStatement(path) {
685 if (modified) return;
686 if (idx === targetIdx) {
687 path.replaceWith(t.expressionStatement(path.node.discriminant));
688 modified = true;
689 }
690 idx++;
691 },
692 });
693
694 if (modified) {
695 yield cloned;
696 }
697 }
698
699 // For each switch, try replacing with each case's body
700 for (let targetIdx = 0; targetIdx < switchCount; targetIdx++) {
701 // Find case count for this switch
702 let caseCount = 0;
703 let currentIdx = 0;
704 t.traverseFast(ast, node => {
705 if (t.isSwitchStatement(node)) {
706 if (currentIdx === targetIdx) {
707 caseCount = node.cases.length;
708 }
709 currentIdx++;
710 }
711 });
712
713 for (let caseIdx = 0; caseIdx < caseCount; caseIdx++) {
714 const cloned = cloneAst(ast);
715 let idx = 0;
716 let modified = false;
717
718 traverse(cloned, {
719 SwitchStatement(path) {
720 if (modified) return;
721 if (idx === targetIdx) {
722 const switchCase = path.node.cases[caseIdx];
723 if (switchCase && switchCase.consequent.length > 0) {
724 path.replaceWithMultiple(switchCase.consequent);
725 modified = true;
726 }
727 }
728 idx++;
729 },
730 });
731
732 if (modified) {
733 yield cloned;
734 }
735 }
736 }
737 }
738
739 /**
740 * Generator that simplifies while statements:
741 * - Replace with test expression
742 * - Replace with body
743 */
744 function* simplifyWhileStatements(ast: t.File): Generator<t.File> {
745 // Count while statements
746 let whileCount = 0;
747 t.traverseFast(ast, node => {
748 if (t.isWhileStatement(node)) {
749 whileCount++;
750 }
751 });
752
753 // Try replacing with test
754 for (let targetIdx = 0; targetIdx < whileCount; targetIdx++) {
755 const cloned = cloneAst(ast);
756 let idx = 0;
757 let modified = false;
758
759 traverse(cloned, {
760 WhileStatement(path) {
761 if (modified) return;
762 if (idx === targetIdx) {
763 path.replaceWith(t.expressionStatement(path.node.test));
764 modified = true;
765 }
766 idx++;
767 },
768 });
769
770 if (modified) {
771 yield cloned;
772 }
773 }
774
775 // Try replacing with body
776 for (let targetIdx = 0; targetIdx < whileCount; targetIdx++) {
777 const cloned = cloneAst(ast);
778 let idx = 0;
779 let modified = false;
780
781 traverse(cloned, {
782 WhileStatement(path) {
783 if (modified) return;
784 if (idx === targetIdx) {
785 path.replaceWith(path.node.body);
786 modified = true;
787 }
788 idx++;
789 },
790 });
791
792 if (modified) {
793 yield cloned;
794 }
795 }
796 }
797
798 /**
799 * Generator that simplifies do-while statements:
800 * - Replace with test expression
801 * - Replace with body
802 */
803 function* simplifyDoWhileStatements(ast: t.File): Generator<t.File> {
804 // Count do-while statements
805 let doWhileCount = 0;
806 t.traverseFast(ast, node => {
807 if (t.isDoWhileStatement(node)) {
808 doWhileCount++;
809 }
810 });
811
812 // Try replacing with test
813 for (let targetIdx = 0; targetIdx < doWhileCount; targetIdx++) {
814 const cloned = cloneAst(ast);
815 let idx = 0;
816 let modified = false;
817
818 traverse(cloned, {
819 DoWhileStatement(path) {
820 if (modified) return;
821 if (idx === targetIdx) {
822 path.replaceWith(t.expressionStatement(path.node.test));
823 modified = true;
824 }
825 idx++;
826 },
827 });
828
829 if (modified) {
830 yield cloned;
831 }
832 }
833
834 // Try replacing with body
835 for (let targetIdx = 0; targetIdx < doWhileCount; targetIdx++) {
836 const cloned = cloneAst(ast);
837 let idx = 0;
838 let modified = false;
839
840 traverse(cloned, {
841 DoWhileStatement(path) {
842 if (modified) return;
843 if (idx === targetIdx) {
844 path.replaceWith(path.node.body);
845 modified = true;
846 }
847 idx++;
848 },
849 });
850
851 if (modified) {
852 yield cloned;
853 }
854 }
855 }
856
857 /**
858 * Generator that simplifies for statements:
859 * - Replace with init (if expression)
860 * - Replace with test expression
861 * - Replace with update expression
862 * - Replace with body
863 */
864 function* simplifyForStatements(ast: t.File): Generator<t.File> {
865 // Count for statements
866 let forCount = 0;
867 t.traverseFast(ast, node => {
868 if (t.isForStatement(node)) {
869 forCount++;
870 }
871 });
872
873 // Try replacing with init (if it's an expression)
874 for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
875 const cloned = cloneAst(ast);
876 let idx = 0;
877 let modified = false;
878
879 traverse(cloned, {
880 ForStatement(path) {
881 if (modified) return;
882 if (idx === targetIdx && path.node.init) {
883 if (t.isExpression(path.node.init)) {
884 path.replaceWith(t.expressionStatement(path.node.init));
885 } else {
886 // It's a VariableDeclaration
887 path.replaceWith(path.node.init);
888 }
889 modified = true;
890 }
891 idx++;
892 },
893 });
894
895 if (modified) {
896 yield cloned;
897 }
898 }
899
900 // Try replacing with test
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 && path.node.test) {
910 path.replaceWith(t.expressionStatement(path.node.test));
911 modified = true;
912 }
913 idx++;
914 },
915 });
916
917 if (modified) {
918 yield cloned;
919 }
920 }
921
922 // Try replacing with update
923 for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
924 const cloned = cloneAst(ast);
925 let idx = 0;
926 let modified = false;
927
928 traverse(cloned, {
929 ForStatement(path) {
930 if (modified) return;
931 if (idx === targetIdx && path.node.update) {
932 path.replaceWith(t.expressionStatement(path.node.update));
933 modified = true;
934 }
935 idx++;
936 },
937 });
938
939 if (modified) {
940 yield cloned;
941 }
942 }
943
944 // Try replacing with body
945 for (let targetIdx = 0; targetIdx < forCount; targetIdx++) {
946 const cloned = cloneAst(ast);
947 let idx = 0;
948 let modified = false;
949
950 traverse(cloned, {
951 ForStatement(path) {
952 if (modified) return;
953 if (idx === targetIdx) {
954 path.replaceWith(path.node.body);
955 modified = true;
956 }
957 idx++;
958 },
959 });
960
961 if (modified) {
962 yield cloned;
963 }
964 }
965 }
966
967 /**
968 * Generator that simplifies for-in statements:
969 * - Replace with left (variable declaration or expression)
970 * - Replace with right expression
971 * - Replace with body
972 */
973 function* simplifyForInStatements(ast: t.File): Generator<t.File> {
974 // Count for-in statements
975 let forInCount = 0;
976 t.traverseFast(ast, node => {
977 if (t.isForInStatement(node)) {
978 forInCount++;
979 }
980 });
981
982 // Try replacing with left
983 for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
984 const cloned = cloneAst(ast);
985 let idx = 0;
986 let modified = false;
987
988 traverse(cloned, {
989 ForInStatement(path) {
990 if (modified) return;
991 if (idx === targetIdx) {
992 const left = path.node.left;
993 if (t.isExpression(left)) {
994 path.replaceWith(t.expressionStatement(left));
995 } else {
996 path.replaceWith(left);
997 }
998 modified = true;
999 }
1000 idx++;
1001 },
1002 });
1003
1004 if (modified) {
1005 yield cloned;
1006 }
1007 }
1008
1009 // Try replacing with right
1010 for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
1011 const cloned = cloneAst(ast);
1012 let idx = 0;
1013 let modified = false;
1014
1015 traverse(cloned, {
1016 ForInStatement(path) {
1017 if (modified) return;
1018 if (idx === targetIdx) {
1019 path.replaceWith(t.expressionStatement(path.node.right));
1020 modified = true;
1021 }
1022 idx++;
1023 },
1024 });
1025
1026 if (modified) {
1027 yield cloned;
1028 }
1029 }
1030
1031 // Try replacing with body
1032 for (let targetIdx = 0; targetIdx < forInCount; targetIdx++) {
1033 const cloned = cloneAst(ast);
1034 let idx = 0;
1035 let modified = false;
1036
1037 traverse(cloned, {
1038 ForInStatement(path) {
1039 if (modified) return;
1040 if (idx === targetIdx) {
1041 path.replaceWith(path.node.body);
1042 modified = true;
1043 }
1044 idx++;
1045 },
1046 });
1047
1048 if (modified) {
1049 yield cloned;
1050 }
1051 }
1052 }
1053
1054 /**
1055 * Generator that simplifies for-of statements:
1056 * - Replace with left (variable declaration or expression)
1057 * - Replace with right expression
1058 * - Replace with body
1059 */
1060 function* simplifyForOfStatements(ast: t.File): Generator<t.File> {
1061 // Count for-of statements
1062 let forOfCount = 0;
1063 t.traverseFast(ast, node => {
1064 if (t.isForOfStatement(node)) {
1065 forOfCount++;
1066 }
1067 });
1068
1069 // Try replacing with left
1070 for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
1071 const cloned = cloneAst(ast);
1072 let idx = 0;
1073 let modified = false;
1074
1075 traverse(cloned, {
1076 ForOfStatement(path) {
1077 if (modified) return;
1078 if (idx === targetIdx) {
1079 const left = path.node.left;
1080 if (t.isExpression(left)) {
1081 path.replaceWith(t.expressionStatement(left));
1082 } else {
1083 path.replaceWith(left);
1084 }
1085 modified = true;
1086 }
1087 idx++;
1088 },
1089 });
1090
1091 if (modified) {
1092 yield cloned;
1093 }
1094 }
1095
1096 // Try replacing with right
1097 for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
1098 const cloned = cloneAst(ast);
1099 let idx = 0;
1100 let modified = false;
1101
1102 traverse(cloned, {
1103 ForOfStatement(path) {
1104 if (modified) return;
1105 if (idx === targetIdx) {
1106 path.replaceWith(t.expressionStatement(path.node.right));
1107 modified = true;
1108 }
1109 idx++;
1110 },
1111 });
1112
1113 if (modified) {
1114 yield cloned;
1115 }
1116 }
1117
1118 // Try replacing with body
1119 for (let targetIdx = 0; targetIdx < forOfCount; targetIdx++) {
1120 const cloned = cloneAst(ast);
1121 let idx = 0;
1122 let modified = false;
1123
1124 traverse(cloned, {
1125 ForOfStatement(path) {
1126 if (modified) return;
1127 if (idx === targetIdx) {
1128 path.replaceWith(path.node.body);
1129 modified = true;
1130 }
1131 idx++;
1132 },
1133 });
1134
1135 if (modified) {
1136 yield cloned;
1137 }
1138 }
1139 }
1140
1141 /**
1142 * Generator that simplifies variable declarations by removing init expressions.
1143 * let x = expr; -> let x;
1144 * var x = expr; -> var x;
1145 * Note: const without init is invalid, so we skip const declarations.
1146 */
1147 function* simplifyVariableDeclarations(ast: t.File): Generator<t.File> {
1148 // Collect all variable declarators with init expressions (excluding const)
1149 const declaratorSites: Array<{declIndex: number}> = [];
1150 let declIndex = 0;
1151 t.traverseFast(ast, node => {
1152 if (t.isVariableDeclaration(node) && node.kind !== 'const') {
1153 for (const declarator of node.declarations) {
1154 if (declarator.init) {
1155 declaratorSites.push({declIndex});
1156 declIndex++;
1157 }
1158 }
1159 }
1160 });
1161
1162 // Try removing init from each declarator
1163 for (const {declIndex: targetDeclIdx} of declaratorSites) {
1164 const cloned = cloneAst(ast);
1165 let idx = 0;
1166 let modified = false;
1167
1168 t.traverseFast(cloned, node => {
1169 if (modified) return;
1170 if (t.isVariableDeclaration(node) && node.kind !== 'const') {
1171 for (const declarator of node.declarations) {
1172 if (declarator.init) {
1173 if (idx === targetDeclIdx) {
1174 declarator.init = null;
1175 modified = true;
1176 return;
1177 }
1178 idx++;
1179 }
1180 }
1181 }
1182 });
1183
1184 if (modified) {
1185 yield cloned;
1186 }
1187 }
1188 }
1189
1190 /**
1191 * Generator that simplifies try/catch/finally statements:
1192 * - Replace with try block contents
1193 * - Replace with catch block contents (if present)
1194 * - Replace with finally block contents (if present)
1195 */
1196 function* simplifyTryStatements(ast: t.File): Generator<t.File> {
1197 // Count try statements
1198 let tryCount = 0;
1199 t.traverseFast(ast, node => {
1200 if (t.isTryStatement(node)) {
1201 tryCount++;
1202 }
1203 });
1204
1205 // Try replacing with try block contents
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) {
1215 path.replaceWith(path.node.block);
1216 modified = true;
1217 }
1218 idx++;
1219 },
1220 });
1221
1222 if (modified) {
1223 yield cloned;
1224 }
1225 }
1226
1227 // Try replacing with catch block contents (if present)
1228 for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) {
1229 const cloned = cloneAst(ast);
1230 let idx = 0;
1231 let modified = false;
1232
1233 traverse(cloned, {
1234 TryStatement(path) {
1235 if (modified) return;
1236 if (idx === targetIdx && path.node.handler) {
1237 path.replaceWith(path.node.handler.body);
1238 modified = true;
1239 }
1240 idx++;
1241 },
1242 });
1243
1244 if (modified) {
1245 yield cloned;
1246 }
1247 }
1248
1249 // Try replacing with finally block contents (if present)
1250 for (let targetIdx = 0; targetIdx < tryCount; targetIdx++) {
1251 const cloned = cloneAst(ast);
1252 let idx = 0;
1253 let modified = false;
1254
1255 traverse(cloned, {
1256 TryStatement(path) {
1257 if (modified) return;
1258 if (idx === targetIdx && path.node.finalizer) {
1259 path.replaceWith(path.node.finalizer);
1260 modified = true;
1261 }
1262 idx++;
1263 },
1264 });
1265
1266 if (modified) {
1267 yield cloned;
1268 }
1269 }
1270 }
1271
1272 /**
1273 * Generator that simplifies single-statement block statements:
1274 * { statement } -> statement
1275 */
1276 function* simplifySingleStatementBlocks(ast: t.File): Generator<t.File> {
1277 // Count block statements with exactly one statement
1278 let blockCount = 0;
1279 t.traverseFast(ast, node => {
1280 if (t.isBlockStatement(node) && node.body.length === 1) {
1281 blockCount++;
1282 }
1283 });
1284
1285 for (let targetIdx = 0; targetIdx < blockCount; targetIdx++) {
1286 const cloned = cloneAst(ast);
1287 let idx = 0;
1288 let modified = false;
1289
1290 traverse(cloned, {
1291 BlockStatement(path) {
1292 if (modified) return;
1293 if (path.node.body.length === 1 && idx === targetIdx) {
1294 // Don't unwrap blocks that require BlockStatement syntax
1295 if (
1296 t.isFunction(path.parent) ||
1297 t.isCatchClause(path.parent) ||
1298 t.isClassMethod(path.parent) ||
1299 t.isObjectMethod(path.parent) ||
1300 t.isTryStatement(path.parent)
1301 ) {
1302 idx++;
1303 return;
1304 }
1305 path.replaceWith(path.node.body[0]);
1306 modified = true;
1307 }
1308 idx++;
1309 },
1310 });
1311
1312 if (modified) {
1313 yield cloned;
1314 }
1315 }
1316 }
1317
1318 /**
1319 * Generator that removes array elements one at a time
1320 */
1321 function* removeArrayElements(ast: t.File): Generator<t.File> {
1322 // Collect all array expressions with their element counts
1323 const arraySites: Array<{arrayIndex: number; elementCount: number}> = [];
1324 let arrayIndex = 0;
1325 t.traverseFast(ast, node => {
1326 if (t.isArrayExpression(node) && node.elements.length > 0) {
1327 arraySites.push({arrayIndex, elementCount: node.elements.length});
1328 arrayIndex++;
1329 }
1330 });
1331
1332 // For each array, try removing each element one at a time (from end to start)
1333 for (const {arrayIndex: targetArrayIdx, elementCount} of arraySites) {
1334 for (let elemIdx = elementCount - 1; elemIdx >= 0; elemIdx--) {
1335 const cloned = cloneAst(ast);
1336 let idx = 0;
1337 let modified = false;
1338
1339 t.traverseFast(cloned, node => {
1340 if (modified) return;
1341 if (t.isArrayExpression(node) && node.elements.length > 0) {
1342 if (idx === targetArrayIdx && elemIdx < node.elements.length) {
1343 node.elements.splice(elemIdx, 1);
1344 modified = true;
1345 }
1346 idx++;
1347 }
1348 });
1349
1350 if (modified) {
1351 yield cloned;
1352 }
1353 }
1354 }
1355 }
1356
1357 /**
1358 * Generator that removes JSX element attributes (props) one at a time
1359 */
1360 function* removeJSXAttributes(ast: t.File): Generator<t.File> {
1361 // Collect all JSX elements with their attribute counts
1362 const jsxSites: Array<{jsxIndex: number; attrCount: number}> = [];
1363 let jsxIndex = 0;
1364 t.traverseFast(ast, node => {
1365 if (t.isJSXOpeningElement(node) && node.attributes.length > 0) {
1366 jsxSites.push({jsxIndex, attrCount: node.attributes.length});
1367 jsxIndex++;
1368 }
1369 });
1370
1371 // For each JSX element, try removing each attribute one at a time (from end to start)
1372 for (const {jsxIndex: targetJsxIdx, attrCount} of jsxSites) {
1373 for (let attrIdx = attrCount - 1; attrIdx >= 0; attrIdx--) {
1374 const cloned = cloneAst(ast);
1375 let idx = 0;
1376 let modified = false;
1377
1378 t.traverseFast(cloned, node => {
1379 if (modified) return;
1380 if (t.isJSXOpeningElement(node) && node.attributes.length > 0) {
1381 if (idx === targetJsxIdx && attrIdx < node.attributes.length) {
1382 node.attributes.splice(attrIdx, 1);
1383 modified = true;
1384 }
1385 idx++;
1386 }
1387 });
1388
1389 if (modified) {
1390 yield cloned;
1391 }
1392 }
1393 }
1394 }
1395
1396 /**
1397 * Generator that removes JSX element children one at a time
1398 */
1399 function* removeJSXChildren(ast: t.File): Generator<t.File> {
1400 // Collect all JSX elements with children
1401 const jsxSites: Array<{jsxIndex: number; childCount: number}> = [];
1402 let jsxIndex = 0;
1403 t.traverseFast(ast, node => {
1404 if (t.isJSXElement(node) && node.children.length > 0) {
1405 jsxSites.push({jsxIndex, childCount: node.children.length});
1406 jsxIndex++;
1407 }
1408 });
1409
1410 // For each JSX element, try removing each child one at a time (from end to start)
1411 for (const {jsxIndex: targetJsxIdx, childCount} of jsxSites) {
1412 for (let childIdx = childCount - 1; childIdx >= 0; childIdx--) {
1413 const cloned = cloneAst(ast);
1414 let idx = 0;
1415 let modified = false;
1416
1417 t.traverseFast(cloned, node => {
1418 if (modified) return;
1419 if (t.isJSXElement(node) && node.children.length > 0) {
1420 if (idx === targetJsxIdx && childIdx < node.children.length) {
1421 node.children.splice(childIdx, 1);
1422 modified = true;
1423 }
1424 idx++;
1425 }
1426 });
1427
1428 if (modified) {
1429 yield cloned;
1430 }
1431 }
1432 }
1433 }
1434
1435 /**
1436 * Generator that removes JSX fragment children one at a time
1437 */
1438 function* removeJSXFragmentChildren(ast: t.File): Generator<t.File> {
1439 // Collect all JSX fragments with children
1440 const fragmentSites: Array<{fragIndex: number; childCount: number}> = [];
1441 let fragIndex = 0;
1442 t.traverseFast(ast, node => {
1443 if (t.isJSXFragment(node) && node.children.length > 0) {
1444 fragmentSites.push({fragIndex, childCount: node.children.length});
1445 fragIndex++;
1446 }
1447 });
1448
1449 // For each fragment, try removing each child one at a time (from end to start)
1450 for (const {fragIndex: targetFragIdx, childCount} of fragmentSites) {
1451 for (let childIdx = childCount - 1; childIdx >= 0; childIdx--) {
1452 const cloned = cloneAst(ast);
1453 let idx = 0;
1454 let modified = false;
1455
1456 t.traverseFast(cloned, node => {
1457 if (modified) return;
1458 if (t.isJSXFragment(node) && node.children.length > 0) {
1459 if (idx === targetFragIdx && childIdx < node.children.length) {
1460 node.children.splice(childIdx, 1);
1461 modified = true;
1462 }
1463 idx++;
1464 }
1465 });
1466
1467 if (modified) {
1468 yield cloned;
1469 }
1470 }
1471 }
1472 }
1473
1474 /**
1475 * Generator that replaces single-element arrays with the element itself
1476 */
1477 function* simplifySingleElementArrays(ast: t.File): Generator<t.File> {
1478 // Count single-element arrays
1479 let arrayCount = 0;
1480 t.traverseFast(ast, node => {
1481 if (t.isArrayExpression(node) && node.elements.length === 1) {
1482 arrayCount++;
1483 }
1484 });
1485
1486 for (let targetIdx = 0; targetIdx < arrayCount; targetIdx++) {
1487 const cloned = cloneAst(ast);
1488 let idx = 0;
1489 let modified = false;
1490
1491 traverse(cloned, {
1492 ArrayExpression(path) {
1493 if (modified) return;
1494 if (path.node.elements.length === 1 && idx === targetIdx) {
1495 const elem = path.node.elements[0];
1496 if (t.isExpression(elem)) {
1497 path.replaceWith(elem);
1498 modified = true;
1499 }
1500 }
1501 idx++;
1502 },
1503 });
1504
1505 if (modified) {
1506 yield cloned;
1507 }
1508 }
1509 }
1510
1511 /**
1512 * Generator that replaces single-property objects with the property value.
1513 * For regular properties: {key: value} -> value
1514 * For computed properties: {[key]: value} -> key (also try value)
1515 */
1516 function* simplifySinglePropertyObjects(ast: t.File): Generator<t.File> {
1517 // Count single-property objects
1518 let objectCount = 0;
1519 t.traverseFast(ast, node => {
1520 if (t.isObjectExpression(node) && node.properties.length === 1) {
1521 objectCount++;
1522 }
1523 });
1524
1525 // Try replacing with value
1526 for (let targetIdx = 0; targetIdx < objectCount; targetIdx++) {
1527 const cloned = cloneAst(ast);
1528 let idx = 0;
1529 let modified = false;
1530
1531 traverse(cloned, {
1532 ObjectExpression(path) {
1533 if (modified) return;
1534 if (path.node.properties.length === 1 && idx === targetIdx) {
1535 const prop = path.node.properties[0];
1536 if (t.isObjectProperty(prop) && t.isExpression(prop.value)) {
1537 path.replaceWith(prop.value);
1538 modified = true;
1539 }
1540 }
1541 idx++;
1542 },
1543 });
1544
1545 if (modified) {
1546 yield cloned;
1547 }
1548 }
1549
1550 // For computed properties, also try replacing with key
1551 for (let targetIdx = 0; targetIdx < objectCount; targetIdx++) {
1552 const cloned = cloneAst(ast);
1553 let idx = 0;
1554 let modified = false;
1555
1556 traverse(cloned, {
1557 ObjectExpression(path) {
1558 if (modified) return;
1559 if (path.node.properties.length === 1 && idx === targetIdx) {
1560 const prop = path.node.properties[0];
1561 if (
1562 t.isObjectProperty(prop) &&
1563 prop.computed &&
1564 t.isExpression(prop.key)
1565 ) {
1566 path.replaceWith(prop.key);
1567 modified = true;
1568 }
1569 }
1570 idx++;
1571 },
1572 });
1573
1574 if (modified) {
1575 yield cloned;
1576 }
1577 }
1578 }
1579
1580 /**
1581 * Generator that removes object properties one at a time
1582 */
1583 function* removeObjectProperties(ast: t.File): Generator<t.File> {
1584 // Collect all object expressions with their property counts
1585 const objectSites: Array<{objectIndex: number; propCount: number}> = [];
1586 let objectIndex = 0;
1587 t.traverseFast(ast, node => {
1588 if (t.isObjectExpression(node) && node.properties.length > 0) {
1589 objectSites.push({objectIndex, propCount: node.properties.length});
1590 objectIndex++;
1591 }
1592 });
1593
1594 // For each object, try removing each property one at a time (from end to start)
1595 for (const {objectIndex: targetObjIdx, propCount} of objectSites) {
1596 for (let propIdx = propCount - 1; propIdx >= 0; propIdx--) {
1597 const cloned = cloneAst(ast);
1598 let idx = 0;
1599 let modified = false;
1600
1601 t.traverseFast(cloned, node => {
1602 if (modified) return;
1603 if (t.isObjectExpression(node) && node.properties.length > 0) {
1604 if (idx === targetObjIdx && propIdx < node.properties.length) {
1605 node.properties.splice(propIdx, 1);
1606 modified = true;
1607 }
1608 idx++;
1609 }
1610 });
1611
1612 if (modified) {
1613 yield cloned;
1614 }
1615 }
1616 }
1617 }
1618
1619 /**
1620 * Generator that removes elements from array destructuring patterns one at a time
1621 */
1622 function* removeArrayPatternElements(ast: t.File): Generator<t.File> {
1623 // Collect all array patterns with elements
1624 const patternSites: Array<{patternIndex: number; elementCount: number}> = [];
1625 let patternIndex = 0;
1626 t.traverseFast(ast, node => {
1627 if (t.isArrayPattern(node) && node.elements.length > 0) {
1628 patternSites.push({patternIndex, elementCount: node.elements.length});
1629 patternIndex++;
1630 }
1631 });
1632
1633 // For each pattern, try removing each element (from end to start)
1634 for (const {patternIndex: targetPatternIdx, elementCount} of patternSites) {
1635 for (let elemIdx = elementCount - 1; elemIdx >= 0; elemIdx--) {
1636 const cloned = cloneAst(ast);
1637 let idx = 0;
1638 let modified = false;
1639
1640 t.traverseFast(cloned, node => {
1641 if (modified) return;
1642 if (t.isArrayPattern(node) && node.elements.length > 0) {
1643 if (idx === targetPatternIdx && elemIdx < node.elements.length) {
1644 node.elements.splice(elemIdx, 1);
1645 modified = true;
1646 }
1647 idx++;
1648 }
1649 });
1650
1651 if (modified) {
1652 yield cloned;
1653 }
1654 }
1655 }
1656 }
1657
1658 /**
1659 * Generator that removes properties from object destructuring patterns one at a time
1660 */
1661 function* removeObjectPatternProperties(ast: t.File): Generator<t.File> {
1662 // Collect all object patterns with properties
1663 const patternSites: Array<{patternIndex: number; propCount: number}> = [];
1664 let patternIndex = 0;
1665 t.traverseFast(ast, node => {
1666 if (t.isObjectPattern(node) && node.properties.length > 0) {
1667 patternSites.push({patternIndex, propCount: node.properties.length});
1668 patternIndex++;
1669 }
1670 });
1671
1672 // For each pattern, try removing each property (from end to start)
1673 for (const {patternIndex: targetPatternIdx, propCount} of patternSites) {
1674 for (let propIdx = propCount - 1; propIdx >= 0; propIdx--) {
1675 const cloned = cloneAst(ast);
1676 let idx = 0;
1677 let modified = false;
1678
1679 t.traverseFast(cloned, node => {
1680 if (modified) return;
1681 if (t.isObjectPattern(node) && node.properties.length > 0) {
1682 if (idx === targetPatternIdx && propIdx < node.properties.length) {
1683 node.properties.splice(propIdx, 1);
1684 modified = true;
1685 }
1686 idx++;
1687 }
1688 });
1689
1690 if (modified) {
1691 yield cloned;
1692 }
1693 }
1694 }
1695 }
1696
1697 /**
1698 * Generator that simplifies assignment expressions (a = b) -> a or b
1699 */
1700 function* simplifyAssignmentExpressions(ast: t.File): Generator<t.File> {
1701 // Count assignment expressions
1702 let assignmentCount = 0;
1703 t.traverseFast(ast, node => {
1704 if (t.isAssignmentExpression(node)) {
1705 assignmentCount++;
1706 }
1707 });
1708
1709 // Try replacing with left side (assignment target)
1710 for (let targetIdx = 0; targetIdx < assignmentCount; targetIdx++) {
1711 const cloned = cloneAst(ast);
1712 let idx = 0;
1713 let modified = false;
1714
1715 traverse(cloned, {
1716 AssignmentExpression(path) {
1717 if (modified) return;
1718 if (idx === targetIdx) {
1719 const left = path.node.left;
1720 if (t.isExpression(left)) {
1721 path.replaceWith(left);
1722 modified = true;
1723 }
1724 }
1725 idx++;
1726 },
1727 });
1728
1729 if (modified) {
1730 yield cloned;
1731 }
1732 }
1733
1734 // Try replacing with right side (assignment value)
1735 for (let targetIdx = 0; targetIdx < assignmentCount; targetIdx++) {
1736 const cloned = cloneAst(ast);
1737 let idx = 0;
1738 let modified = false;
1739
1740 traverse(cloned, {
1741 AssignmentExpression(path) {
1742 if (modified) return;
1743 if (idx === targetIdx) {
1744 path.replaceWith(path.node.right);
1745 modified = true;
1746 }
1747 idx++;
1748 },
1749 });
1750
1751 if (modified) {
1752 yield cloned;
1753 }
1754 }
1755 }
1756
1757 /**
1758 * Generator that simplifies binary expressions (a + b) -> a or b
1759 */
1760 function* simplifyBinaryExpressions(ast: t.File): Generator<t.File> {
1761 // Count binary expressions
1762 let binaryCount = 0;
1763 t.traverseFast(ast, node => {
1764 if (t.isBinaryExpression(node)) {
1765 binaryCount++;
1766 }
1767 });
1768
1769 // Try replacing with left side
1770 for (let targetIdx = 0; targetIdx < binaryCount; targetIdx++) {
1771 const cloned = cloneAst(ast);
1772 let idx = 0;
1773 let modified = false;
1774
1775 traverse(cloned, {
1776 BinaryExpression(path) {
1777 if (modified) return;
1778 if (idx === targetIdx) {
1779 path.replaceWith(path.node.left);
1780 modified = true;
1781 }
1782 idx++;
1783 },
1784 });
1785
1786 if (modified) {
1787 yield cloned;
1788 }
1789 }
1790
1791 // Try replacing with right side
1792 for (let targetIdx = 0; targetIdx < binaryCount; targetIdx++) {
1793 const cloned = cloneAst(ast);
1794 let idx = 0;
1795 let modified = false;
1796
1797 traverse(cloned, {
1798 BinaryExpression(path) {
1799 if (modified) return;
1800 if (idx === targetIdx) {
1801 path.replaceWith(path.node.right);
1802 modified = true;
1803 }
1804 idx++;
1805 },
1806 });
1807
1808 if (modified) {
1809 yield cloned;
1810 }
1811 }
1812 }
1813
1814 /**
1815 * Generator that simplifies member expressions (obj.value) -> obj
1816 * For computed expressions: obj[key] -> obj or key
1817 */
1818 function* simplifyMemberExpressions(ast: t.File): Generator<t.File> {
1819 // Count member expressions
1820 let memberCount = 0;
1821 t.traverseFast(ast, node => {
1822 if (t.isMemberExpression(node)) {
1823 memberCount++;
1824 }
1825 });
1826
1827 // Try replacing with object
1828 for (let targetIdx = 0; targetIdx < memberCount; targetIdx++) {
1829 const cloned = cloneAst(ast);
1830 let idx = 0;
1831 let modified = false;
1832
1833 traverse(cloned, {
1834 MemberExpression(path) {
1835 if (modified) return;
1836 if (idx === targetIdx) {
1837 path.replaceWith(path.node.object);
1838 modified = true;
1839 }
1840 idx++;
1841 },
1842 });
1843
1844 if (modified) {
1845 yield cloned;
1846 }
1847 }
1848
1849 // For computed expressions, also try replacing with key
1850 for (let targetIdx = 0; targetIdx < memberCount; targetIdx++) {
1851 const cloned = cloneAst(ast);
1852 let idx = 0;
1853 let modified = false;
1854
1855 traverse(cloned, {
1856 MemberExpression(path) {
1857 if (modified) return;
1858 if (idx === targetIdx && path.node.computed) {
1859 const property = path.node.property;
1860 if (t.isExpression(property)) {
1861 path.replaceWith(property);
1862 modified = true;
1863 }
1864 }
1865 idx++;
1866 },
1867 });
1868
1869 if (modified) {
1870 yield cloned;
1871 }
1872 }
1873 }
1874
1875 /**
1876 * Helper to collect all unique identifier names in the AST
1877 */
1878 function collectUniqueIdentifierNames(ast: t.File): Set<string> {
1879 const names = new Set<string>();
1880 t.traverseFast(ast, node => {
1881 if (t.isIdentifier(node)) {
1882 names.add(node.name);
1883 }
1884 });
1885 return names;
1886 }
1887
1888 /**
1889 * Helper to rename all occurrences of an identifier throughout the AST
1890 */
1891 function renameAllIdentifiers(
1892 ast: t.File,
1893 oldName: string,
1894 newName: string,
1895 ): boolean {
1896 let modified = false;
1897 t.traverseFast(ast, node => {
1898 if (t.isIdentifier(node) && node.name === oldName) {
1899 node.name = newName;
1900 modified = true;
1901 }
1902 });
1903 return modified;
1904 }
1905
1906 /**
1907 * Generator that simplifies identifiers by removing "on" prefix.
1908 * onClick -> Click
1909 */
1910 function* simplifyIdentifiersRemoveOnPrefix(ast: t.File): Generator<t.File> {
1911 const names = collectUniqueIdentifierNames(ast);
1912
1913 for (const name of names) {
1914 // Check if name starts with "on" followed by uppercase letter
1915 if (
1916 name.length > 2 &&
1917 name.startsWith('on') &&
1918 name[2] === name[2].toUpperCase()
1919 ) {
1920 const newName = name.slice(2);
1921 // Skip if the new name would conflict with an existing identifier
1922 if (names.has(newName)) {
1923 continue;
1924 }
1925 const cloned = cloneAst(ast);
1926 if (renameAllIdentifiers(cloned, name, newName)) {
1927 yield cloned;
1928 }
1929 }
1930 }
1931 }
1932
1933 /**
1934 * Generator that simplifies identifiers by removing "Ref" suffix.
1935 * inputRef -> input
1936 */
1937 function* simplifyIdentifiersRemoveRefSuffix(ast: t.File): Generator<t.File> {
1938 const names = collectUniqueIdentifierNames(ast);
1939
1940 for (const name of names) {
1941 // Check if name ends with "Ref" and has more characters before it
1942 if (name.length > 3 && name.endsWith('Ref')) {
1943 const newName = name.slice(0, -3);
1944 // Skip if the new name would conflict with an existing identifier
1945 if (names.has(newName)) {
1946 continue;
1947 }
1948 // Skip if new name would be empty or just whitespace
1949 if (newName.length === 0) {
1950 continue;
1951 }
1952 const cloned = cloneAst(ast);
1953 if (renameAllIdentifiers(cloned, name, newName)) {
1954 yield cloned;
1955 }
1956 }
1957 }
1958 }
1959
1960 /**
1961 * Generator that rewrites "ref" identifier to "ref_" to avoid conflicts.
1962 */
1963 function* simplifyIdentifiersRenameRef(ast: t.File): Generator<t.File> {
1964 const names = collectUniqueIdentifierNames(ast);
1965
1966 if (names.has('ref')) {
1967 // Only rename if ref_ doesn't already exist
1968 if (!names.has('ref_')) {
1969 const cloned = cloneAst(ast);
1970 if (renameAllIdentifiers(cloned, 'ref', 'ref_')) {
1971 yield cloned;
1972 }
1973 }
1974 }
1975 }
1976
1977 /**
1978 * All simplification strategies in order of priority (coarse to fine)
1979 */
1980 const simplificationStrategies = [
1981 {name: 'removeStatements', generator: removeStatements},
1982 {name: 'removeCallArguments', generator: removeCallArguments},
1983 {name: 'removeFunctionParameters', generator: removeFunctionParameters},
1984 {name: 'removeArrayElements', generator: removeArrayElements},
1985 {name: 'removeObjectProperties', generator: removeObjectProperties},
1986 {name: 'removeArrayPatternElements', generator: removeArrayPatternElements},
1987 {
1988 name: 'removeObjectPatternProperties',
1989 generator: removeObjectPatternProperties,
1990 },
1991 {name: 'removeJSXAttributes', generator: removeJSXAttributes},
1992 {name: 'removeJSXChildren', generator: removeJSXChildren},
1993 {name: 'removeJSXFragmentChildren', generator: removeJSXFragmentChildren},
1994 {name: 'simplifyCallExpressions', generator: simplifyCallExpressions},
1995 {name: 'simplifyConditionals', generator: simplifyConditionals},
1996 {name: 'simplifyLogicalExpressions', generator: simplifyLogicalExpressions},
1997 {name: 'simplifyBinaryExpressions', generator: simplifyBinaryExpressions},
1998 {
1999 name: 'simplifyAssignmentExpressions',
2000 generator: simplifyAssignmentExpressions,
2001 },
2002 {name: 'simplifySingleElementArrays', generator: simplifySingleElementArrays},
2003 {
2004 name: 'simplifySinglePropertyObjects',
2005 generator: simplifySinglePropertyObjects,
2006 },
2007 {name: 'simplifyMemberExpressions', generator: simplifyMemberExpressions},
2008 {name: 'simplifyOptionalChains', generator: simplifyOptionalChains},
2009 {name: 'simplifyAwaitExpressions', generator: simplifyAwaitExpressions},
2010 {name: 'simplifyIfStatements', generator: simplifyIfStatements},
2011 {name: 'simplifySwitchStatements', generator: simplifySwitchStatements},
2012 {name: 'simplifyWhileStatements', generator: simplifyWhileStatements},
2013 {name: 'simplifyDoWhileStatements', generator: simplifyDoWhileStatements},
2014 {name: 'simplifyForStatements', generator: simplifyForStatements},
2015 {name: 'simplifyForInStatements', generator: simplifyForInStatements},
2016 {name: 'simplifyForOfStatements', generator: simplifyForOfStatements},
2017 {
2018 name: 'simplifyVariableDeclarations',
2019 generator: simplifyVariableDeclarations,
2020 },
2021 {name: 'simplifyTryStatements', generator: simplifyTryStatements},
2022 {
2023 name: 'simplifySingleStatementBlocks',
2024 generator: simplifySingleStatementBlocks,
2025 },
2026 {
2027 name: 'simplifyIdentifiersRemoveOnPrefix',
2028 generator: simplifyIdentifiersRemoveOnPrefix,
2029 },
2030 {
2031 name: 'simplifyIdentifiersRemoveRefSuffix',
2032 generator: simplifyIdentifiersRemoveRefSuffix,
2033 },
2034 {
2035 name: 'simplifyIdentifiersRenameRef',
2036 generator: simplifyIdentifiersRenameRef,
2037 },
2038 ];
2039
2040 type MinimizeResult =
2041 | {kind: 'success'}
2042 | {kind: 'minimal'}
2043 | {kind: 'minimized'; source: string};
2044
2045 /**
2046 * Core minimization loop that attempts to reduce the input source code
2047 * while preserving the compiler error.
2048 */
2049 export function minimize(
2050 input: string,
2051 filename: string,
2052 language: 'flow' | 'typescript',
2053 sourceType: 'module' | 'script',
2054 useRust: boolean = false,
2055 ): MinimizeResult {
2056 // Load the compiler plugin
2057 const pluginSrc = useRust ? BABEL_PLUGIN_RUST_SRC : BABEL_PLUGIN_SRC;
2058 const importedCompilerPlugin = require(pluginSrc) as Record<string, unknown>;
2059 const BabelPluginReactCompiler = importedCompilerPlugin[
2060 'default'
2061 ] as PluginObj;
2062 const parseConfigPragmaForTests = importedCompilerPlugin[
2063 PARSE_CONFIG_PRAGMA_IMPORT
2064 ] as typeof ParseConfigPragma;
2065
2066 // Get the initial error
2067 const initialResult = compileAndGetError(
2068 input,
2069 filename,
2070 language,
2071 sourceType,
2072 BabelPluginReactCompiler,
2073 parseConfigPragmaForTests,
2074 );
2075
2076 if (initialResult.kind === 'success') {
2077 return {kind: 'success'};
2078 }
2079
2080 if (initialResult.kind === 'parse_error') {
2081 return {kind: 'success'};
2082 }
2083
2084 const targetError = initialResult;
2085
2086 // Parse the initial AST
2087 let currentAst = parseInput(input, filename, language, sourceType);
2088 let currentCode = input;
2089 let changed = true;
2090 let iterations = 0;
2091 const maxIterations = 1000; // Safety limit
2092
2093 process.stdout.write('\nMinimizing');
2094
2095 while (changed && iterations < maxIterations) {
2096 changed = false;
2097 iterations++;
2098
2099 // Try each simplification strategy
2100 for (const strategy of simplificationStrategies) {
2101 const generator = strategy.generator(currentAst);
2102
2103 for (const candidateAst of generator) {
2104 let candidateCode: string;
2105 try {
2106 candidateCode = astToCode(candidateAst);
2107 } catch {
2108 // If code generation fails, skip this candidate
2109 continue;
2110 }
2111
2112 const result = compileAndGetError(
2113 candidateCode,
2114 filename,
2115 language,
2116 sourceType,
2117 BabelPluginReactCompiler,
2118 parseConfigPragmaForTests,
2119 );
2120
2121 if (errorsMatch(targetError, result)) {
2122 // This simplification preserves the error, keep it
2123 currentAst = candidateAst;
2124 currentCode = candidateCode;
2125 changed = true;
2126 process.stdout.write('.');
2127 break; // Restart from the beginning with the new AST
2128 }
2129 }
2130
2131 if (changed) {
2132 break; // Restart the outer loop
2133 }
2134 }
2135 }
2136
2137 console.log('\n');
2138
2139 // Check if any minimization was achieved
2140 if (currentCode === input) {
2141 return {kind: 'minimal'};
2142 }
2143
2144 return {kind: 'minimized', source: currentCode};
2145 }
2146
2147 /**
2148 * Compile code with a given plugin and return the output code (or error string).
2149 */
2150 function compileAndGetOutput(
2151 code: string,
2152 filename: string,
2153 language: 'flow' | 'typescript',
2154 sourceType: 'module' | 'script',
2155 plugin: PluginObj,
2156 parseConfigPragmaFn: typeof ParseConfigPragma,
2157 ): string | null {
2158 let ast: t.File;
2159 try {
2160 ast = parseInput(code, filename, language, sourceType);
2161 } catch {
2162 return null;
2163 }
2164
2165 const firstLine = code.substring(0, code.indexOf('\n'));
2166 const config = parseConfigPragmaFn(firstLine, {compilationMode: 'all'});
2167 const options = {
2168 ...config,
2169 environment: {
2170 ...config.environment,
2171 },
2172 logger: {
2173 logEvent: () => {},
2174 debugLogIRs: () => {},
2175 },
2176 enableReanimatedCheck: false,
2177 };
2178
2179 try {
2180 const result = transformFromAstSync(ast, code, {
2181 filename: '/' + filename,
2182 highlightCode: false,
2183 retainLines: true,
2184 compact: true,
2185 plugins: [[plugin, options]],
2186 sourceType: 'module',
2187 ast: false,
2188 cloneInputAst: true,
2189 configFile: false,
2190 babelrc: false,
2191 });
2192 return result?.code ?? null;
2193 } catch (e: unknown) {
2194 return `ERROR: ${(e as Error).message}`;
2195 }
2196 }
2197
2198 type MinimizeRustDeltaResult =
2199 | {kind: 'no_delta'}
2200 | {kind: 'minimal'}
2201 | {kind: 'minimized'; source: string};
2202
2203 /**
2204 * Core minimization loop that attempts to reduce the input source code
2205 * while preserving a delta (difference in output) between the TS and Rust compilers.
2206 */
2207 export function minimizeRustDelta(
2208 input: string,
2209 filename: string,
2210 language: 'flow' | 'typescript',
2211 sourceType: 'module' | 'script',
2212 ): MinimizeRustDeltaResult {
2213 // Load both compiler plugins
2214 const importedTsPlugin = require(BABEL_PLUGIN_SRC) as Record<string, unknown>;
2215 const importedRustPlugin = require(BABEL_PLUGIN_RUST_SRC) as Record<
2216 string,
2217 unknown
2218 >;
2219 const tsPlugin = importedTsPlugin['default'] as PluginObj;
2220 const rustPlugin = importedRustPlugin['default'] as PluginObj;
2221 const parseConfigPragmaForTests = importedTsPlugin[
2222 PARSE_CONFIG_PRAGMA_IMPORT
2223 ] as typeof ParseConfigPragma;
2224
2225 // Helper: check if TS and Rust outputs differ for given code
2226 function hasDelta(code: string): boolean {
2227 const tsOutput = compileAndGetOutput(
2228 code,
2229 filename,
2230 language,
2231 sourceType,
2232 tsPlugin,
2233 parseConfigPragmaForTests,
2234 );
2235 const rustOutput = compileAndGetOutput(
2236 code,
2237 filename,
2238 language,
2239 sourceType,
2240 rustPlugin,
2241 parseConfigPragmaForTests,
2242 );
2243 // Both null (e.g. parse error) means no delta
2244 if (tsOutput == null && rustOutput == null) return false;
2245 return tsOutput !== rustOutput;
2246 }
2247
2248 // Verify the initial input has a delta
2249 if (!hasDelta(input)) {
2250 return {kind: 'no_delta'};
2251 }
2252
2253 // Parse the initial AST
2254 let currentAst = parseInput(input, filename, language, sourceType);
2255 let currentCode = input;
2256 let changed = true;
2257 let iterations = 0;
2258 const maxIterations = 1000;
2259
2260 process.stdout.write('\nMinimizing');
2261
2262 while (changed && iterations < maxIterations) {
2263 changed = false;
2264 iterations++;
2265
2266 for (const strategy of simplificationStrategies) {
2267 const generator = strategy.generator(currentAst);
2268
2269 for (const candidateAst of generator) {
2270 let candidateCode: string;
2271 try {
2272 candidateCode = astToCode(candidateAst);
2273 } catch {
2274 continue;
2275 }
2276
2277 if (hasDelta(candidateCode)) {
2278 currentAst = candidateAst;
2279 currentCode = candidateCode;
2280 changed = true;
2281 process.stdout.write('.');
2282 break;
2283 }
2284 }
2285
2286 if (changed) {
2287 break;
2288 }
2289 }
2290 }
2291
2292 console.log('\n');
2293
2294 if (currentCode === input) {
2295 return {kind: 'minimal'};
2296 }
2297
2298 return {kind: 'minimized', source: currentCode};
2299 }