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
+}