@openrewrite/rewrite 8.66.1 → 8.66.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/java/tree.d.ts +10 -1
  2. package/dist/java/tree.d.ts.map +1 -1
  3. package/dist/java/tree.js +21 -5
  4. package/dist/java/tree.js.map +1 -1
  5. package/dist/java/type-visitor.d.ts +1 -1
  6. package/dist/java/type-visitor.d.ts.map +1 -1
  7. package/dist/java/visitor.d.ts +2 -2
  8. package/dist/java/visitor.d.ts.map +1 -1
  9. package/dist/java/visitor.js +8 -2
  10. package/dist/java/visitor.js.map +1 -1
  11. package/dist/javascript/assertions.d.ts +6 -0
  12. package/dist/javascript/assertions.d.ts.map +1 -1
  13. package/dist/javascript/assertions.js +14 -6
  14. package/dist/javascript/assertions.js.map +1 -1
  15. package/dist/javascript/comparator.d.ts +154 -7
  16. package/dist/javascript/comparator.d.ts.map +1 -1
  17. package/dist/javascript/comparator.js +623 -180
  18. package/dist/javascript/comparator.js.map +1 -1
  19. package/dist/javascript/format.d.ts +5 -3
  20. package/dist/javascript/format.d.ts.map +1 -1
  21. package/dist/javascript/format.js +85 -43
  22. package/dist/javascript/format.js.map +1 -1
  23. package/dist/javascript/index.d.ts +1 -0
  24. package/dist/javascript/index.d.ts.map +1 -1
  25. package/dist/javascript/index.js +1 -0
  26. package/dist/javascript/index.js.map +1 -1
  27. package/dist/javascript/parser.d.ts +2 -1
  28. package/dist/javascript/parser.d.ts.map +1 -1
  29. package/dist/javascript/parser.js +39 -30
  30. package/dist/javascript/parser.js.map +1 -1
  31. package/dist/javascript/templating/capture.d.ts +81 -14
  32. package/dist/javascript/templating/capture.d.ts.map +1 -1
  33. package/dist/javascript/templating/capture.js +98 -8
  34. package/dist/javascript/templating/capture.js.map +1 -1
  35. package/dist/javascript/templating/comparator.d.ts +125 -15
  36. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  37. package/dist/javascript/templating/comparator.js +946 -118
  38. package/dist/javascript/templating/comparator.js.map +1 -1
  39. package/dist/javascript/templating/engine.d.ts +58 -25
  40. package/dist/javascript/templating/engine.d.ts.map +1 -1
  41. package/dist/javascript/templating/engine.js +527 -94
  42. package/dist/javascript/templating/engine.js.map +1 -1
  43. package/dist/javascript/templating/index.d.ts +3 -3
  44. package/dist/javascript/templating/index.d.ts.map +1 -1
  45. package/dist/javascript/templating/index.js +3 -1
  46. package/dist/javascript/templating/index.js.map +1 -1
  47. package/dist/javascript/templating/pattern.d.ts +121 -16
  48. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  49. package/dist/javascript/templating/pattern.js +528 -257
  50. package/dist/javascript/templating/pattern.js.map +1 -1
  51. package/dist/javascript/templating/placeholder-replacement.d.ts +30 -5
  52. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  53. package/dist/javascript/templating/placeholder-replacement.js +183 -81
  54. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  55. package/dist/javascript/templating/rewrite.d.ts +56 -11
  56. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  57. package/dist/javascript/templating/rewrite.js +143 -16
  58. package/dist/javascript/templating/rewrite.js.map +1 -1
  59. package/dist/javascript/templating/template.d.ts +31 -5
  60. package/dist/javascript/templating/template.d.ts.map +1 -1
  61. package/dist/javascript/templating/template.js +89 -15
  62. package/dist/javascript/templating/template.js.map +1 -1
  63. package/dist/javascript/templating/types.d.ts +359 -12
  64. package/dist/javascript/templating/types.d.ts.map +1 -1
  65. package/dist/javascript/templating/utils.d.ts +52 -35
  66. package/dist/javascript/templating/utils.d.ts.map +1 -1
  67. package/dist/javascript/templating/utils.js +107 -109
  68. package/dist/javascript/templating/utils.js.map +1 -1
  69. package/dist/javascript/type-mapping.d.ts.map +1 -1
  70. package/dist/javascript/type-mapping.js +21 -11
  71. package/dist/javascript/type-mapping.js.map +1 -1
  72. package/dist/json/rpc.js +2 -2
  73. package/dist/json/rpc.js.map +1 -1
  74. package/dist/recipe/order-imports.js.map +1 -1
  75. package/dist/test/rewrite-test.d.ts.map +1 -1
  76. package/dist/test/rewrite-test.js +10 -6
  77. package/dist/test/rewrite-test.js.map +1 -1
  78. package/dist/version.txt +1 -1
  79. package/dist/visitor.d.ts +4 -4
  80. package/dist/visitor.d.ts.map +1 -1
  81. package/dist/visitor.js +8 -3
  82. package/dist/visitor.js.map +1 -1
  83. package/package.json +4 -2
  84. package/src/java/tree.ts +10 -3
  85. package/src/java/type-visitor.ts +1 -1
  86. package/src/java/visitor.ts +11 -5
  87. package/src/javascript/assertions.ts +9 -3
  88. package/src/javascript/comparator.ts +676 -185
  89. package/src/javascript/format.ts +72 -34
  90. package/src/javascript/index.ts +1 -0
  91. package/src/javascript/parser.ts +51 -31
  92. package/src/javascript/templating/capture.ts +107 -15
  93. package/src/javascript/templating/comparator.ts +1087 -134
  94. package/src/javascript/templating/engine.ts +601 -103
  95. package/src/javascript/templating/index.ts +9 -2
  96. package/src/javascript/templating/pattern.ts +655 -281
  97. package/src/javascript/templating/placeholder-replacement.ts +183 -80
  98. package/src/javascript/templating/rewrite.ts +152 -18
  99. package/src/javascript/templating/template.ts +110 -22
  100. package/src/javascript/templating/types.ts +386 -12
  101. package/src/javascript/templating/utils.ts +116 -102
  102. package/src/javascript/type-mapping.ts +20 -11
  103. package/src/json/rpc.ts +2 -2
  104. package/src/recipe/order-imports.ts +1 -1
  105. package/src/test/rewrite-test.ts +12 -7
  106. package/src/visitor.ts +14 -6
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import {JavaScriptVisitor} from './visitor';
17
- import {J, Type} from '../java';
17
+ import {J, Type, Expression, Statement, isIdentifier} from '../java';
18
18
  import {JS, JSX} from './tree';
19
19
  import {Cursor, Tree} from "../tree";
20
20
 
@@ -29,15 +29,27 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
29
29
  */
30
30
  protected match: boolean = true;
31
31
 
32
+ /**
33
+ * Cursor tracking the current position in the target tree.
34
+ * Maintained in parallel with the pattern tree cursor (this.cursor).
35
+ */
36
+ protected targetCursor?: Cursor;
37
+
32
38
  /**
33
39
  * Compares two AST trees.
34
- *
35
- * @param tree1 The first tree to compare
36
- * @param tree2 The second tree to compare
40
+ *
41
+ * @param tree1 The first tree to compare (pattern tree)
42
+ * @param tree2 The second tree to compare (target tree)
43
+ * @param parentCursor1 Optional parent cursor for the pattern tree (for navigating to root)
44
+ * @param parentCursor2 Optional parent cursor for the target tree (for navigating to root)
37
45
  * @returns true if the trees match, false otherwise
38
46
  */
39
- async compare(tree1: J, tree2: J): Promise<boolean> {
47
+ async compare(tree1: J, tree2: J, parentCursor1?: Cursor, parentCursor2?: Cursor): Promise<boolean> {
40
48
  this.match = true;
49
+ // Initialize targetCursor with parent if provided, otherwise undefined (will be set by visit())
50
+ this.targetCursor = parentCursor2;
51
+ // Initialize this.cursor (pattern cursor) with parent if provided
52
+ this.cursor = parentCursor1 || new Cursor(undefined, undefined);
41
53
  await this.visit(tree1, tree2);
42
54
  return this.match;
43
55
  }
@@ -55,25 +67,158 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
55
67
 
56
68
  /**
57
69
  * Aborts the visit operation by setting the match flag to false.
70
+ *
71
+ * @param t The node being compared
72
+ * @param reason Optional reason for the mismatch (e.g., 'kind-mismatch', 'property-mismatch')
73
+ * @param propertyName Optional property name where mismatch occurred
74
+ * @param expected Optional expected value
75
+ * @param actual Optional actual value
58
76
  */
59
- protected abort<T>(t: T): T {
77
+ protected abort<T>(t: T, reason?: string, propertyName?: string, expected?: any, actual?: any): T {
60
78
  this.match = false;
61
79
  return t;
62
80
  }
63
81
 
82
+ /**
83
+ * Specialized abort methods for common mismatch scenarios.
84
+ * These provide a cleaner API at call sites.
85
+ * Can be overridden in subclasses to extract values from cursors and provide richer error messages.
86
+ */
87
+
88
+ protected kindMismatch() {
89
+ const pattern = this.cursor?.value as any;
90
+ return this.abort(pattern, 'kind-mismatch');
91
+ }
92
+
93
+ protected structuralMismatch(propertyName?: string) {
94
+ const pattern = this.cursor?.value as any;
95
+ return this.abort(pattern, 'structural-mismatch', propertyName);
96
+ }
97
+
98
+ protected arrayLengthMismatch(propertyName: string) {
99
+ const pattern = this.cursor?.value as any;
100
+ return this.abort(pattern, 'array-length-mismatch', propertyName);
101
+ }
102
+
103
+ protected valueMismatch(propertyName?: string, expected?: any, actual?: any) {
104
+ const pattern = this.cursor?.value as any;
105
+ // If values not provided, try to extract from cursors (only if propertyName is available)
106
+ const expectedVal = expected !== undefined ? expected : (propertyName ? (pattern as any)?.[propertyName] : pattern);
107
+ const actualVal = actual !== undefined ? actual : (propertyName ? (this.targetCursor?.value as any)?.[propertyName] : this.targetCursor?.value);
108
+ return this.abort(pattern, 'value-mismatch', propertyName, expectedVal, actualVal);
109
+ }
110
+
111
+ protected typeMismatch(propertyName?: string) {
112
+ const pattern = this.cursor?.value as any;
113
+ const target = this.targetCursor?.value as any;
114
+ return this.abort(pattern, 'type-mismatch', propertyName, pattern?.type, target?.type);
115
+ }
116
+
117
+ /**
118
+ * Helper method to visit an array property by iterating through both arrays in lock-step.
119
+ * Checks length mismatch first, then visits each element pair.
120
+ * Can be overridden in subclasses to add path tracking or other instrumentation.
121
+ *
122
+ * @param parent The parent node containing the array property
123
+ * @param propertyName The name of the array property
124
+ * @param array1 The array from the first tree
125
+ * @param array2 The array from the second tree
126
+ * @param visitor Function to visit each element pair (no need to return anything)
127
+ * @returns undefined, modifying this.match if a mismatch occurs
128
+ */
129
+ protected async visitArrayProperty<T>(
130
+ parent: J,
131
+ propertyName: string,
132
+ array1: T[],
133
+ array2: T[],
134
+ visitor: (item1: T, item2: T, index: number) => Promise<void>
135
+ ): Promise<void> {
136
+ // Check length mismatch
137
+ if (array1.length !== array2.length) {
138
+ this.arrayLengthMismatch(propertyName);
139
+ return;
140
+ }
141
+
142
+ // Visit each element in lock step
143
+ for (let i = 0; i < array1.length; i++) {
144
+ await visitor(array1[i], array2[i], i);
145
+ if (!this.match) return;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Helper method to visit a container property with proper context.
151
+ * Can be overridden in subclasses to add path tracking or other instrumentation.
152
+ *
153
+ * @param parent The parent node containing the container property
154
+ * @param propertyName The name of the container property
155
+ * @param container The container from the first tree
156
+ * @param otherContainer The container from the second tree
157
+ * @returns The container from the first tree
158
+ */
159
+ protected async visitContainerProperty<T extends J>(
160
+ propertyName: string,
161
+ container: J.Container<T>,
162
+ otherContainer: J.Container<T>
163
+ ): Promise<J.Container<T>> {
164
+ // Default implementation just calls visitContainer
165
+ // Subclasses can override to add property context
166
+ await this.visitContainer(container, otherContainer as any);
167
+ return container;
168
+ }
169
+
170
+ /**
171
+ * Helper to visit a RightPadded property with property context.
172
+ * This allows subclasses to track which property is being visited.
173
+ *
174
+ * @param propertyName The property name for context
175
+ * @param rightPadded The RightPadded from the first tree
176
+ * @param otherRightPadded The RightPadded from the second tree
177
+ * @returns The RightPadded from the first tree
178
+ */
179
+ protected async visitRightPaddedProperty<T extends J | boolean>(
180
+ propertyName: string,
181
+ rightPadded: J.RightPadded<T>,
182
+ otherRightPadded: J.RightPadded<T>
183
+ ): Promise<J.RightPadded<T>> {
184
+ // Default implementation just calls visitRightPadded
185
+ // Subclasses can override to add property context
186
+ return await this.visitRightPadded(rightPadded, otherRightPadded as any);
187
+ }
188
+
189
+ /**
190
+ * Helper to visit a LeftPadded property with property context.
191
+ * This allows subclasses to track which property is being visited.
192
+ *
193
+ * @param propertyName The property name for context
194
+ * @param leftPadded The LeftPadded from the first tree
195
+ * @param otherLeftPadded The LeftPadded from the second tree
196
+ * @returns The LeftPadded from the first tree
197
+ */
198
+ protected async visitLeftPaddedProperty<T extends J | J.Space | number | string | boolean>(
199
+ propertyName: string,
200
+ leftPadded: J.LeftPadded<T>,
201
+ otherLeftPadded: J.LeftPadded<T>
202
+ ): Promise<J.LeftPadded<T>> {
203
+ // Default implementation just calls visitLeftPadded
204
+ // Subclasses can override to add property context
205
+ return await this.visitLeftPadded(leftPadded, otherLeftPadded as any);
206
+ }
207
+
64
208
  /**
65
209
  * Generic method to visit a property value using the appropriate visitor method.
66
210
  * This ensures wrappers (RightPadded, LeftPadded, Container) are properly tracked on the cursor.
67
211
  *
68
212
  * @param j The property value from the first tree
69
213
  * @param other The corresponding property value from the second tree
214
+ * @param propertyName Optional property name for error reporting
70
215
  * @returns The visited property value from the first tree
71
216
  */
72
- protected async visitProperty(j: any, other: any): Promise<any> {
217
+ protected async visitProperty(j: any, other: any, propertyName?: string): Promise<any> {
73
218
  // Handle null/undefined (but not other falsy values like 0, false, '')
74
219
  if (j == null || other == null) {
75
220
  if (j !== other) {
76
- this.abort(j);
221
+ return this.structuralMismatch(propertyName);
77
222
  }
78
223
  return j;
79
224
  }
@@ -82,14 +227,20 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
82
227
 
83
228
  // Check wrappers by kind
84
229
  if (kind === J.Kind.RightPadded) {
85
- return await this.visitRightPadded(j, other);
230
+ return propertyName ? await this.visitRightPaddedProperty(propertyName, j, other) :
231
+ await this.visitRightPadded(j, other);
86
232
  }
87
233
 
88
234
  if (kind === J.Kind.LeftPadded) {
89
- return await this.visitLeftPadded(j, other);
235
+ return propertyName ? await this.visitLeftPaddedProperty(propertyName, j, other) :
236
+ await this.visitLeftPadded(j, other);
90
237
  }
91
238
 
92
239
  if (kind === J.Kind.Container) {
240
+ // Use visitContainerProperty when propertyName is provided for proper context tracking
241
+ if (propertyName) {
242
+ return await this.visitContainerProperty(propertyName, j, other);
243
+ }
93
244
  return await this.visitContainer(j, other);
94
245
  }
95
246
 
@@ -109,7 +260,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
109
260
 
110
261
  // For primitive values, compare directly
111
262
  if (j !== other) {
112
- this.abort(j);
263
+ return this.valueMismatch(propertyName, j, other);
113
264
  }
114
265
  return j;
115
266
  }
@@ -124,19 +275,17 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
124
275
  * @returns The visited element from the first tree
125
276
  */
126
277
  protected async visitElement<T extends J>(j: T, other: T): Promise<T> {
127
- if (!this.match) {
128
- return j;
129
- }
278
+ if (!this.match) return j;
130
279
 
131
280
  // Check if kinds match
132
281
  if (j.kind !== other.kind) {
133
- return this.abort(j);
282
+ return this.kindMismatch();
134
283
  }
135
284
 
136
285
  // Iterate over all properties
137
286
  for (const key of Object.keys(j)) {
138
287
  // Skip internal/private properties, id property, and markers property
139
- if (key.startsWith('_') || key === 'id' || key === 'markers') {
288
+ if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers') {
140
289
  continue;
141
290
  }
142
291
 
@@ -146,22 +295,18 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
146
295
  // Handle arrays - compare element by element
147
296
  if (Array.isArray(jValue)) {
148
297
  if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
149
- return this.abort(j);
298
+ return this.arrayLengthMismatch(key);
150
299
  }
151
300
 
152
301
  for (let i = 0; i < jValue.length; i++) {
153
- await this.visitProperty(jValue[i], otherValue[i]);
154
- if (!this.match) {
155
- return j;
156
- }
302
+ await this.visitProperty(jValue[i], otherValue[i], `${key}[${i}]`);
303
+ if (!this.match) return j;
157
304
  }
158
305
  } else {
159
306
  // Visit the property (which will handle wrappers, trees, primitives, etc.)
160
- await this.visitProperty(jValue, otherValue);
307
+ await this.visitProperty(jValue, otherValue, key);
161
308
 
162
- if (!this.match) {
163
- return j;
164
- }
309
+ if (!this.match) return j;
165
310
  }
166
311
  }
167
312
 
@@ -170,41 +315,51 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
170
315
 
171
316
  override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
172
317
  // If we've already found a mismatch, abort further processing
173
- if (!this.match) {
174
- return j as R;
175
- }
318
+ if (!this.match) return j as R;
176
319
 
177
320
  // Check if the nodes have the same kind
178
321
  if (!this.hasSameKind(j as J, p)) {
179
- return this.abort(j) as R;
322
+ return this.kindMismatch() as R;
180
323
  }
181
324
 
182
- // Continue with normal visitation, passing the other node as context
183
- return super.visit(j, p);
325
+ // Update targetCursor to track the target node in parallel with the pattern cursor
326
+ // (Can be overridden by subclasses if they need cursor access before calling super)
327
+ const savedTargetCursor = this.targetCursor;
328
+ this.targetCursor = new Cursor(p, this.targetCursor);
329
+ try {
330
+ // Continue with normal visitation, passing the other node as context
331
+ return await super.visit(j, p);
332
+ } finally {
333
+ this.targetCursor = savedTargetCursor;
334
+ }
184
335
  }
185
336
 
186
337
  /**
187
338
  * Override visitRightPadded to compare only the elements, not markers or spacing.
188
339
  * The context parameter p contains the corresponding element from the other tree.
189
340
  * Pushes the wrapper onto the cursor stack so captures can access it.
341
+ * Also updates targetCursor in parallel.
190
342
  */
191
343
  public async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: J): Promise<J.RightPadded<T>> {
192
- if (!this.match) {
193
- return right;
194
- }
344
+ if (!this.match) return right;
195
345
 
196
346
  // Extract the other element if it's also a RightPadded
197
- const otherElement = (p as any).kind === J.Kind.RightPadded
198
- ? ((p as unknown) as J.RightPadded<T>).element
199
- : p;
347
+ const isRightPadded = (p as any).kind === J.Kind.RightPadded;
348
+ const otherWrapper = isRightPadded ? (p as unknown) as J.RightPadded<T> : undefined;
349
+ const otherElement = isRightPadded ? otherWrapper!.element : p;
200
350
 
201
- // Push wrapper onto cursor, then compare only the elements, not markers or spacing
351
+ // Push wrappers onto both cursors, then compare only the elements, not markers or spacing
202
352
  const savedCursor = this.cursor;
353
+ const savedTargetCursor = this.targetCursor;
203
354
  this.cursor = new Cursor(right, this.cursor);
355
+ this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor;
204
356
  try {
357
+ // Call visitProperty without propertyName to avoid pushing spurious 'element' path entries
358
+ // The property context should be provided through visitRightPaddedProperty() if needed
205
359
  await this.visitProperty(right.element, otherElement);
206
360
  } finally {
207
361
  this.cursor = savedCursor;
362
+ this.targetCursor = savedTargetCursor;
208
363
  }
209
364
 
210
365
  return right;
@@ -214,24 +369,28 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
214
369
  * Override visitLeftPadded to compare only the elements, not markers or spacing.
215
370
  * The context parameter p contains the corresponding element from the other tree.
216
371
  * Pushes the wrapper onto the cursor stack so captures can access it.
372
+ * Also updates targetCursor in parallel.
217
373
  */
218
374
  public async visitLeftPadded<T extends J | J.Space | number | string | boolean>(left: J.LeftPadded<T>, p: J): Promise<J.LeftPadded<T>> {
219
- if (!this.match) {
220
- return left;
221
- }
375
+ if (!this.match) return left;
222
376
 
223
377
  // Extract the other element if it's also a LeftPadded
224
- const otherElement = (p as any).kind === J.Kind.LeftPadded
225
- ? ((p as unknown) as J.LeftPadded<T>).element
226
- : p;
378
+ const isLeftPadded = (p as any).kind === J.Kind.LeftPadded;
379
+ const otherWrapper = isLeftPadded ? (p as unknown) as J.LeftPadded<T> : undefined;
380
+ const otherElement = isLeftPadded ? otherWrapper!.element : p;
227
381
 
228
- // Push wrapper onto cursor, then compare only the elements, not markers or spacing
382
+ // Push wrappers onto both cursors, then compare only the elements, not markers or spacing
229
383
  const savedCursor = this.cursor;
384
+ const savedTargetCursor = this.targetCursor;
230
385
  this.cursor = new Cursor(left, this.cursor);
386
+ this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor;
231
387
  try {
388
+ // Call visitProperty without propertyName to avoid pushing spurious 'element' path entries
389
+ // The property context should be provided through visitLeftPaddedProperty() if needed
232
390
  await this.visitProperty(left.element, otherElement);
233
391
  } finally {
234
392
  this.cursor = savedCursor;
393
+ this.targetCursor = savedTargetCursor;
235
394
  }
236
395
 
237
396
  return left;
@@ -241,34 +400,34 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
241
400
  * Override visitContainer to compare only the elements, not markers or spacing.
242
401
  * The context parameter p contains the corresponding element from the other tree.
243
402
  * Pushes the wrapper onto the cursor stack so captures can access it.
403
+ * Also updates targetCursor in parallel.
244
404
  */
245
405
  public async visitContainer<T extends J>(container: J.Container<T>, p: J): Promise<J.Container<T>> {
246
- if (!this.match) {
247
- return container;
248
- }
406
+ if (!this.match) return container;
249
407
 
250
408
  // Extract the other elements if it's also a Container
251
- const otherElements: J.RightPadded<T>[] = (p as any).kind === J.Kind.Container
252
- ? ((p as unknown) as J.Container<T>).elements
253
- : (p as any);
409
+ const isContainer = (p as any).kind === J.Kind.Container;
410
+ const otherContainer = isContainer ? (p as unknown) as J.Container<T> : undefined;
411
+ const otherElements: J.RightPadded<T>[] = isContainer ? otherContainer!.elements : (p as any);
254
412
 
255
413
  // Compare elements array length
256
414
  if (container.elements.length !== otherElements.length) {
257
- return this.abort(container);
415
+ return this.arrayLengthMismatch('elements');
258
416
  }
259
417
 
260
- // Push wrapper onto cursor, then compare each element
418
+ // Push wrappers onto both cursors, then compare each element
261
419
  const savedCursor = this.cursor;
420
+ const savedTargetCursor = this.targetCursor;
262
421
  this.cursor = new Cursor(container, this.cursor);
422
+ this.targetCursor = otherContainer ? new Cursor(otherContainer, this.targetCursor) : this.targetCursor;
263
423
  try {
264
424
  for (let i = 0; i < container.elements.length; i++) {
265
425
  await this.visitProperty(container.elements[i], otherElements[i]);
266
- if (!this.match) {
267
- return this.abort(container);
268
- }
426
+ if (!this.match) return container;
269
427
  }
270
428
  } finally {
271
429
  this.cursor = savedCursor;
430
+ this.targetCursor = savedTargetCursor;
272
431
  }
273
432
 
274
433
  return container;
@@ -1805,12 +1964,271 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1805
1964
  this.lenientTypeMatching = lenientTypeMatching;
1806
1965
  }
1807
1966
 
1967
+ /**
1968
+ * Unwraps parentheses from a tree node recursively.
1969
+ * This allows comparing expressions with and without redundant parentheses.
1970
+ *
1971
+ * @param tree The tree to unwrap
1972
+ * @returns The unwrapped tree
1973
+ */
1974
+ protected unwrap(tree: Tree | undefined): Tree | undefined {
1975
+ if (!tree) {
1976
+ return tree;
1977
+ }
1978
+
1979
+ // Unwrap J.Parentheses nodes recursively
1980
+ if ((tree as any).kind === J.Kind.Parentheses) {
1981
+ const parens = tree as J.Parentheses<any>;
1982
+ return this.unwrap(parens.tree.element as Tree);
1983
+ }
1984
+
1985
+ // Unwrap J.ControlParentheses nodes recursively
1986
+ if ((tree as any).kind === J.Kind.ControlParentheses) {
1987
+ const controlParens = tree as J.ControlParentheses<any>;
1988
+ return this.unwrap(controlParens.tree.element as Tree);
1989
+ }
1990
+
1991
+ return tree;
1992
+ }
1993
+
1994
+ override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
1995
+ // If we've already found a mismatch, abort further processing
1996
+ if (!this.match) return j as R;
1997
+
1998
+ // Unwrap parentheses from both trees before comparing
1999
+ const unwrappedJ = this.unwrap(j) || j;
2000
+ const unwrappedP = this.unwrap(p) || p;
2001
+
2002
+ // Skip the kind check that the base class does - semantic matching allows different kinds
2003
+ // (e.g., undefined identifier matching void expression)
2004
+ // Update targetCursor to track the target node in parallel with the pattern cursor
2005
+ const savedTargetCursor = this.targetCursor;
2006
+ this.targetCursor = new Cursor(unwrappedP, this.targetCursor);
2007
+ try {
2008
+ // Call the grandparent's visit to do actual visitation without the kind check
2009
+ return await JavaScriptVisitor.prototype.visit.call(this, unwrappedJ, unwrappedP) as R | undefined;
2010
+ } finally {
2011
+ this.targetCursor = savedTargetCursor;
2012
+ }
2013
+ }
2014
+
2015
+ /**
2016
+ * Override visitArrowFunction to allow semantic equivalence between expression body
2017
+ * and block with single return statement forms.
2018
+ *
2019
+ * Examples:
2020
+ * - `x => x + 1` matches `x => { return x + 1; }`
2021
+ * - `(x, y) => x + y` matches `(x, y) => { return x + y; }`
2022
+ */
2023
+ override async visitArrowFunction(arrowFunction: JS.ArrowFunction, other: J): Promise<J | undefined> {
2024
+ if (!this.match) return arrowFunction;
2025
+
2026
+ if (other.kind !== JS.Kind.ArrowFunction) {
2027
+ return this.kindMismatch();
2028
+ }
2029
+
2030
+ const otherArrow = other as JS.ArrowFunction;
2031
+
2032
+ // Compare all properties reflectively except lambda (handled specially below)
2033
+ for (const key of Object.keys(arrowFunction)) {
2034
+ if (key.startsWith('_') || key === 'id' || key === 'markers' || key === 'lambda') {
2035
+ continue;
2036
+ }
2037
+
2038
+ const jValue = (arrowFunction as any)[key];
2039
+ const otherValue = (otherArrow as any)[key];
2040
+
2041
+ // Handle arrays
2042
+ if (Array.isArray(jValue)) {
2043
+ if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
2044
+ return this.arrayLengthMismatch(key);
2045
+ }
2046
+ for (let i = 0; i < jValue.length; i++) {
2047
+ await this.visitProperty(jValue[i], otherValue[i]);
2048
+ if (!this.match) return arrowFunction;
2049
+ }
2050
+ } else {
2051
+ await this.visitProperty(jValue, otherValue);
2052
+ if (!this.match) return arrowFunction;
2053
+ }
2054
+ }
2055
+
2056
+ // Compare lambda parameters
2057
+ const params1 = arrowFunction.lambda.parameters.parameters;
2058
+ const params2 = otherArrow.lambda.parameters.parameters;
2059
+ if (params1.length !== params2.length) {
2060
+ return this.arrayLengthMismatch('lambda.parameters.parameters');
2061
+ }
2062
+ for (let i = 0; i < params1.length; i++) {
2063
+ await this.visitProperty(params1[i], params2[i]);
2064
+ if (!this.match) return arrowFunction;
2065
+ }
2066
+
2067
+ // Handle semantic equivalence for lambda bodies
2068
+ const body1 = arrowFunction.lambda.body;
2069
+ const body2 = otherArrow.lambda.body;
2070
+
2071
+ // Try to extract the expression from each body
2072
+ const expr1 = this.extractExpression(body1);
2073
+ const expr2 = this.extractExpression(body2);
2074
+
2075
+ if (expr1 && expr2) {
2076
+ // Both have extractable expressions - compare them
2077
+ await this.visit(expr1, expr2);
2078
+ } else {
2079
+ // At least one is not a simple expression or block-with-return
2080
+ // Fall back to exact comparison
2081
+ await this.visit(body1, body2);
2082
+ }
2083
+
2084
+ return arrowFunction;
2085
+ }
2086
+
2087
+ /**
2088
+ * Override visitLambdaParameters to allow semantic equivalence between
2089
+ * arrow functions with and without parentheses around single parameters.
2090
+ *
2091
+ * Examples:
2092
+ * - `x => x + 1` matches `(x) => x + 1`
2093
+ */
2094
+ override async visitLambdaParameters(parameters: J.Lambda.Parameters, other: J): Promise<J | undefined> {
2095
+ if (!this.match) return parameters;
2096
+
2097
+ if (other.kind !== J.Kind.LambdaParameters) {
2098
+ return this.kindMismatch();
2099
+ }
2100
+
2101
+ const otherParams = other as J.Lambda.Parameters;
2102
+
2103
+ // Compare all properties except 'parenthesized' using reflection
2104
+ for (const key of Object.keys(parameters)) {
2105
+ if (key.startsWith('_') || key === 'id' || key === 'markers' || key === 'parenthesized') {
2106
+ continue;
2107
+ }
2108
+
2109
+ const jValue = (parameters as any)[key];
2110
+ const otherValue = (otherParams as any)[key];
2111
+
2112
+ // Handle arrays
2113
+ if (Array.isArray(jValue)) {
2114
+ if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
2115
+ return this.arrayLengthMismatch(key);
2116
+ }
2117
+ for (let i = 0; i < jValue.length; i++) {
2118
+ await this.visitProperty(jValue[i], otherValue[i]);
2119
+ if (!this.match) return parameters;
2120
+ }
2121
+ } else {
2122
+ await this.visitProperty(jValue, otherValue);
2123
+ if (!this.match) return parameters;
2124
+ }
2125
+ }
2126
+
2127
+ return parameters;
2128
+ }
2129
+
2130
+ /**
2131
+ * Override visitPropertyAssignment to allow semantic equivalence between
2132
+ * object property shorthand and longhand forms.
2133
+ *
2134
+ * Examples:
2135
+ * - `{ x }` matches `{ x: x }`
2136
+ * - `{ x: x, y: y }` matches `{ x, y }`
2137
+ */
2138
+ override async visitPropertyAssignment(propertyAssignment: JS.PropertyAssignment, other: J): Promise<J | undefined> {
2139
+ if (!this.match) return propertyAssignment;
2140
+
2141
+ if (other.kind !== JS.Kind.PropertyAssignment) {
2142
+ return this.kindMismatch();
2143
+ }
2144
+
2145
+ const otherProp = other as JS.PropertyAssignment;
2146
+
2147
+ // Extract property names for semantic comparison
2148
+ const propName = this.getPropertyName(propertyAssignment);
2149
+ const otherPropName = this.getPropertyName(otherProp);
2150
+
2151
+ // Names must match
2152
+ if (!propName || !otherPropName || propName !== otherPropName) {
2153
+ // Can't do semantic comparison without identifiers, fall back to exact comparison
2154
+ return await super.visitPropertyAssignment(propertyAssignment, other);
2155
+ }
2156
+
2157
+ // Detect shorthand (no initializer) vs longhand (has initializer)
2158
+ const isShorthand1 = !propertyAssignment.initializer;
2159
+ const isShorthand2 = !otherProp.initializer;
2160
+
2161
+ if (isShorthand1 === isShorthand2) {
2162
+ // Both shorthand or both longhand - use base comparison
2163
+ return await super.visitPropertyAssignment(propertyAssignment, other);
2164
+ }
2165
+
2166
+ // One is shorthand, one is longhand - check semantic equivalence
2167
+ const longhandProp = isShorthand1 ? otherProp : propertyAssignment;
2168
+
2169
+ // Check if the longhand's initializer is an identifier with the same name as the property
2170
+ if (this.isIdentifierWithName(longhandProp.initializer, propName)) {
2171
+ // Semantically equivalent!
2172
+ return propertyAssignment;
2173
+ } else {
2174
+ // Not equivalent (e.g., { x: y })
2175
+ return this.structuralMismatch('initializer');
2176
+ }
2177
+ }
2178
+
2179
+ /**
2180
+ * Extracts the property name from a PropertyAssignment.
2181
+ * Returns the simple name if the property is an identifier, undefined otherwise.
2182
+ */
2183
+ private getPropertyName(prop: JS.PropertyAssignment): string | undefined {
2184
+ const nameExpr = prop.name.element;
2185
+ return isIdentifier(nameExpr) ? nameExpr.simpleName : undefined;
2186
+ }
2187
+
2188
+ /**
2189
+ * Checks if an expression is an identifier with the given name.
2190
+ */
2191
+ private isIdentifierWithName(expr: Expression | undefined, name: string): boolean | undefined {
2192
+ return expr && isIdentifier(expr) && expr.simpleName === name;
2193
+ }
2194
+
2195
+ /**
2196
+ * Extracts the expression from an arrow function body.
2197
+ * Returns the expression if:
2198
+ * - body is already an Expression, OR
2199
+ * - body is a Block with exactly one Return statement
2200
+ * Otherwise returns undefined.
2201
+ */
2202
+ private extractExpression(body: Statement | Expression): Expression | undefined {
2203
+ // If it's already an expression, return it
2204
+ if ((body as any).kind !== J.Kind.Block) {
2205
+ return body as Expression;
2206
+ }
2207
+
2208
+ // It's a block - check if it contains exactly one return statement
2209
+ const block = body as J.Block;
2210
+ if (block.statements.length !== 1) {
2211
+ return undefined;
2212
+ }
2213
+
2214
+ // Unwrap the RightPadded wrapper from the statement
2215
+ const stmtWrapper = block.statements[0];
2216
+ const stmt = stmtWrapper.element;
2217
+
2218
+ if ((stmt as any).kind !== J.Kind.Return) {
2219
+ return undefined;
2220
+ }
2221
+
2222
+ const returnStmt = stmt as J.Return;
2223
+ return returnStmt.expression;
2224
+ }
2225
+
1808
2226
  /**
1809
2227
  * Override visitProperty to allow lenient type matching.
1810
2228
  * When lenientTypeMatching is enabled, null vs Type comparisons are allowed
1811
2229
  * (where one value is null/undefined and the other is a Type object).
1812
2230
  */
1813
- protected override async visitProperty(j: any, other: any): Promise<any> {
2231
+ protected override async visitProperty(j: any, other: any, propertyName?: string): Promise<any> {
1814
2232
  // Handle null/undefined with lenient type matching
1815
2233
  if (this.lenientTypeMatching && (j == null || other == null)) {
1816
2234
  if (j !== other) {
@@ -1822,7 +2240,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1822
2240
  (otherKind && typeof otherKind === 'string' && otherKind.startsWith('org.openrewrite.java.tree.JavaType$'));
1823
2241
 
1824
2242
  if (!isTypeComparison) {
1825
- this.abort(j);
2243
+ this.structuralMismatch(propertyName!);
1826
2244
  }
1827
2245
  }
1828
2246
  return j;
@@ -1843,8 +2261,10 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1843
2261
  return this.lenientTypeMatching ? true : target === source;
1844
2262
  }
1845
2263
 
1846
- if (target.kind !== source.kind) {
1847
- return false;
2264
+ if (target.kind !== source.kind && (target.kind == Type.Kind.Unknown || source.kind == Type.Kind.Unknown)) {
2265
+ // In lenient mode, allow kind mismatches (e.g., Unknown vs proper type)
2266
+ // This handles cases where pattern has unresolved types
2267
+ return this.lenientTypeMatching;
1848
2268
  }
1849
2269
 
1850
2270
  // For method types, check declaring type
@@ -1917,119 +2337,150 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1917
2337
  */
1918
2338
  override async visitMethodInvocation(method: J.MethodInvocation, other: J): Promise<J | undefined> {
1919
2339
  if (other.kind !== J.Kind.MethodInvocation) {
1920
- return this.abort(method);
2340
+ return this.kindMismatch();
1921
2341
  }
1922
2342
 
1923
2343
  const otherMethod = other as J.MethodInvocation;
1924
2344
 
1925
- // Check basic structural equality first
1926
- if (method.name.simpleName !== otherMethod.name.simpleName ||
1927
- method.arguments.elements.length !== otherMethod.arguments.elements.length) {
1928
- return this.abort(method);
2345
+ // Check if we can skip name checking based on type attribution
2346
+ // We can only skip the name check if both have method types AND they represent the SAME method
2347
+ // (not just type-compatible methods, but the actual same function with same FQN)
2348
+ let canSkipNameCheck = false;
2349
+ if (method.methodType && otherMethod.methodType) {
2350
+ // Check if both method types have fully qualified declaring types with the same FQN
2351
+ // This indicates they're the same method from the same module (possibly aliased)
2352
+ const methodDeclaringType = method.methodType.declaringType;
2353
+ const otherDeclaringType = otherMethod.methodType.declaringType;
2354
+
2355
+ if (methodDeclaringType && otherDeclaringType &&
2356
+ Type.isFullyQualified(methodDeclaringType) && Type.isFullyQualified(otherDeclaringType)) {
2357
+
2358
+ const methodFQN = Type.FullyQualified.getFullyQualifiedName(methodDeclaringType as Type.FullyQualified);
2359
+ const otherFQN = Type.FullyQualified.getFullyQualifiedName(otherDeclaringType as Type.FullyQualified);
2360
+
2361
+ // Same module/class AND same method name in the type = same method (can be aliased)
2362
+ if (methodFQN === otherFQN && method.methodType.name === otherMethod.methodType.name) {
2363
+ canSkipNameCheck = true;
2364
+ }
2365
+ // If FQNs or method names don't match, we can't skip name check - fall through to name checking
2366
+ }
2367
+ // If one or both don't have fully qualified types, we can't safely skip name checking
2368
+ // Fall through to normal name comparison below
1929
2369
  }
1930
2370
 
1931
- // Check type attribution
1932
- // Both must have method types for semantic equality
1933
- if (!method.methodType || !otherMethod.methodType) {
1934
- // Lenient mode: if either has no type, allow structural matching
1935
- if (this.lenientTypeMatching) {
1936
- return super.visitMethodInvocation(method, other);
2371
+ // Check names unless we determined we can skip based on type FQN matching
2372
+ if (!canSkipNameCheck) {
2373
+ if (method.name.simpleName !== otherMethod.name.simpleName) {
2374
+ return this.valueMismatch('name.simpleName', method.name.simpleName, otherMethod.name.simpleName);
1937
2375
  }
1938
- // Strict mode: if one has type but the other doesn't, they don't match
1939
- if (method.methodType || otherMethod.methodType) {
1940
- return this.abort(method);
2376
+
2377
+ // In strict mode, check type attribution requirements
2378
+ if (!this.lenientTypeMatching) {
2379
+ // Strict mode: if one has type but the other doesn't, they don't match
2380
+ if ((method.methodType && !otherMethod.methodType) ||
2381
+ (!method.methodType && otherMethod.methodType)) {
2382
+ return this.typeMismatch('methodType');
2383
+ }
1941
2384
  }
1942
- // If neither has type, fall through to structural comparison
1943
- return super.visitMethodInvocation(method, other);
1944
- }
1945
2385
 
1946
- // Both have types - check they match semantically
1947
- const typesMatch = this.isOfType(method.methodType, otherMethod.methodType);
1948
- if (!typesMatch) {
1949
- // Types don't match - abort comparison
1950
- return this.abort(method);
1951
- }
2386
+ // If neither has type, use structural comparison
2387
+ if (!method.methodType && !otherMethod.methodType) {
2388
+ return super.visitMethodInvocation(method, other);
2389
+ }
1952
2390
 
1953
- // Types match! Now check if we can ignore receiver differences.
1954
- // We can only ignore receiver differences when one or both receivers are identifiers
1955
- // that represent module/namespace imports (e.g., `util` in `util.isDate()`).
1956
- // For other receivers (e.g., variables, expressions), we must compare them.
2391
+ // If both have types with FQ declaring types, verify they're compatible
2392
+ // (This prevents matching completely different methods like util.isArray vs util.isBoolean)
2393
+ if (method.methodType && otherMethod.methodType) {
2394
+ const methodDeclaringType = method.methodType.declaringType;
2395
+ const otherDeclaringType = otherMethod.methodType.declaringType;
1957
2396
 
1958
- const canIgnoreReceiverDifference =
1959
- // Case 1: One has no select (direct call like `forwardRef()`), other has select (namespace like `React.forwardRef()`)
1960
- (!method.select && otherMethod.select) ||
1961
- (method.select && !otherMethod.select);
2397
+ if (methodDeclaringType && otherDeclaringType &&
2398
+ Type.isFullyQualified(methodDeclaringType) && Type.isFullyQualified(otherDeclaringType)) {
1962
2399
 
1963
- if (!canIgnoreReceiverDifference) {
1964
- // Both have selects or both don't - must compare them structurally
2400
+ const methodFQN = Type.FullyQualified.getFullyQualifiedName(methodDeclaringType as Type.FullyQualified);
2401
+ const otherFQN = Type.FullyQualified.getFullyQualifiedName(otherDeclaringType as Type.FullyQualified);
2402
+
2403
+ // Different declaring types = different methods, even with same name
2404
+ if (methodFQN !== otherFQN) {
2405
+ return this.valueMismatch('methodType.declaringType');
2406
+ }
2407
+ }
2408
+ }
2409
+ }
2410
+
2411
+ // When types match (canSkipNameCheck = true), we can skip select comparison entirely.
2412
+ // This allows matching forwardRef() vs React.forwardRef() where types indicate same method.
2413
+ if (!canSkipNameCheck) {
2414
+ // Types didn't provide a match - must compare receivers structurally
1965
2415
  if ((method.select === undefined) !== (otherMethod.select === undefined)) {
1966
- return this.abort(method);
2416
+ return this.structuralMismatch('select');
1967
2417
  }
1968
2418
 
1969
2419
  if (method.select && otherMethod.select) {
1970
- await this.visit(method.select.element, otherMethod.select.element);
2420
+ await this.visitRightPaddedProperty('select', method.select, otherMethod.select as any);
2421
+ if (!this.match) return method;
1971
2422
  }
1972
2423
  }
2424
+ // else: types matched, skip select comparison (allows namespace vs named imports)
1973
2425
 
1974
2426
  // Compare type parameters
1975
2427
  if ((method.typeParameters === undefined) !== (otherMethod.typeParameters === undefined)) {
1976
- return this.abort(method);
2428
+ return this.structuralMismatch('typeParameters');
1977
2429
  }
1978
2430
 
1979
2431
  if (method.typeParameters && otherMethod.typeParameters) {
1980
- if (method.typeParameters.elements.length !== otherMethod.typeParameters.elements.length) {
1981
- return this.abort(method);
1982
- }
1983
- for (let i = 0; i < method.typeParameters.elements.length; i++) {
1984
- await this.visit(method.typeParameters.elements[i].element, otherMethod.typeParameters.elements[i].element);
1985
- if (!this.match) {
1986
- return this.abort(method);
1987
- }
1988
- }
2432
+ await this.visitContainerProperty('typeParameters', method.typeParameters, otherMethod.typeParameters);
2433
+ if (!this.match) return method;
1989
2434
  }
1990
2435
 
1991
- // Compare name (already checked simpleName above, but visit for markers/prefix)
1992
- await this.visit(method.name, otherMethod.name);
1993
- if (!this.match) {
1994
- return this.abort(method);
2436
+ // Compare name
2437
+ // If we determined we can skip name check (same FQN method, possibly aliased), skip it
2438
+ // This allows matching aliased imports where names differ but types are the same
2439
+ if (!canSkipNameCheck) {
2440
+ await this.visit(method.name, otherMethod.name);
2441
+ if (!this.match) return method;
1995
2442
  }
1996
2443
 
1997
2444
  // Compare arguments
1998
- for (let i = 0; i < method.arguments.elements.length; i++) {
1999
- await this.visit(method.arguments.elements[i].element, otherMethod.arguments.elements[i].element);
2000
- if (!this.match) {
2001
- return this.abort(method);
2002
- }
2003
- }
2445
+ await this.visitContainerProperty('arguments', method.arguments, otherMethod.arguments);
2446
+ if (!this.match) return method;
2004
2447
 
2005
2448
  return method;
2006
2449
  }
2007
2450
 
2008
2451
  /**
2009
- * Override identifier comparison to include type checking for field access.
2452
+ * Override identifier comparison to include:
2453
+ * 1. Type checking for field access
2454
+ * 2. Semantic equivalence between `undefined` identifier and void expressions
2010
2455
  */
2011
2456
  override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
2457
+ // Check if this identifier is "undefined" and the other is a void expression
2458
+ if (identifier.simpleName === 'undefined' && (other as any).kind === JS.Kind.Void) {
2459
+ // Both evaluate to undefined, so they match
2460
+ return identifier;
2461
+ }
2462
+
2012
2463
  if (other.kind !== J.Kind.Identifier) {
2013
- return this.abort(identifier);
2464
+ return this.kindMismatch();
2014
2465
  }
2015
2466
 
2016
2467
  const otherIdentifier = other as J.Identifier;
2017
2468
 
2018
2469
  // Check name matches
2019
2470
  if (identifier.simpleName !== otherIdentifier.simpleName) {
2020
- return this.abort(identifier);
2471
+ return this.valueMismatch('simpleName');
2021
2472
  }
2022
2473
 
2023
2474
  // For identifiers with field types, check type attribution
2024
2475
  if (identifier.fieldType && otherIdentifier.fieldType) {
2025
2476
  if (!this.isOfType(identifier.fieldType, otherIdentifier.fieldType)) {
2026
- return this.abort(identifier);
2477
+ return this.typeMismatch('fieldType');
2027
2478
  }
2028
2479
  } else if (identifier.fieldType || otherIdentifier.fieldType) {
2029
2480
  // Lenient mode: if either has no type, allow structural matching
2030
2481
  if (!this.lenientTypeMatching) {
2031
2482
  // Strict mode: if only one has a type, they don't match
2032
- return this.abort(identifier);
2483
+ return this.typeMismatch('fieldType');
2033
2484
  }
2034
2485
  }
2035
2486
 
@@ -2045,29 +2496,29 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2045
2496
  const otherVariableDeclarations = other as J.VariableDeclarations;
2046
2497
 
2047
2498
  // Visit leading annotations
2048
- if (variableDeclarations.leadingAnnotations.length !== otherVariableDeclarations.leadingAnnotations.length) {
2049
- return this.abort(variableDeclarations);
2050
- }
2051
-
2052
- for (let i = 0; i < variableDeclarations.leadingAnnotations.length; i++) {
2053
- await this.visit(variableDeclarations.leadingAnnotations[i], otherVariableDeclarations.leadingAnnotations[i]);
2054
- if (!this.match) return variableDeclarations;
2055
- }
2499
+ await this.visitArrayProperty(
2500
+ variableDeclarations,
2501
+ 'leadingAnnotations',
2502
+ variableDeclarations.leadingAnnotations,
2503
+ otherVariableDeclarations.leadingAnnotations,
2504
+ async (ann1, ann2) => { await this.visit(ann1, ann2); }
2505
+ );
2506
+ if (!this.match) return variableDeclarations;
2056
2507
 
2057
2508
  // Visit modifiers
2058
- if (variableDeclarations.modifiers.length !== otherVariableDeclarations.modifiers.length) {
2059
- return this.abort(variableDeclarations);
2060
- }
2061
-
2062
- for (let i = 0; i < variableDeclarations.modifiers.length; i++) {
2063
- await this.visit(variableDeclarations.modifiers[i], otherVariableDeclarations.modifiers[i]);
2064
- if (!this.match) return variableDeclarations;
2065
- }
2509
+ await this.visitArrayProperty(
2510
+ variableDeclarations,
2511
+ 'modifiers',
2512
+ variableDeclarations.modifiers,
2513
+ otherVariableDeclarations.modifiers,
2514
+ async (mod1, mod2) => { await this.visit(mod1, mod2); }
2515
+ );
2516
+ if (!this.match) return variableDeclarations;
2066
2517
 
2067
2518
  // Compare typeExpression - lenient matching allows one to be undefined
2068
2519
  if ((variableDeclarations.typeExpression === undefined) !== (otherVariableDeclarations.typeExpression === undefined)) {
2069
2520
  if (!this.lenientTypeMatching) {
2070
- return this.abort(variableDeclarations);
2521
+ return this.structuralMismatch('typeExpression');
2071
2522
  }
2072
2523
  // In lenient mode, skip type comparison and continue
2073
2524
  } else if (variableDeclarations.typeExpression && otherVariableDeclarations.typeExpression) {
@@ -2078,19 +2529,18 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2078
2529
 
2079
2530
  // Compare varargs
2080
2531
  if ((variableDeclarations.varargs === undefined) !== (otherVariableDeclarations.varargs === undefined)) {
2081
- return this.abort(variableDeclarations);
2532
+ return this.structuralMismatch('varargs');
2082
2533
  }
2083
2534
 
2084
2535
  // Compare variables
2085
- if (variableDeclarations.variables.length !== otherVariableDeclarations.variables.length) {
2086
- return this.abort(variableDeclarations);
2087
- }
2088
-
2089
- // Visit each variable in lock step
2090
- for (let i = 0; i < variableDeclarations.variables.length; i++) {
2091
- await this.visit(variableDeclarations.variables[i].element, otherVariableDeclarations.variables[i].element);
2092
- if (!this.match) return variableDeclarations;
2093
- }
2536
+ await this.visitArrayProperty(
2537
+ variableDeclarations,
2538
+ 'variables',
2539
+ variableDeclarations.variables,
2540
+ otherVariableDeclarations.variables,
2541
+ async (var1, var2) => { await this.visitRightPadded(var1, var2 as any); }
2542
+ );
2543
+ if (!this.match) return variableDeclarations;
2094
2544
 
2095
2545
  return variableDeclarations;
2096
2546
  }
@@ -2104,28 +2554,28 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2104
2554
  const otherMethodDeclaration = other as J.MethodDeclaration;
2105
2555
 
2106
2556
  // Visit leading annotations
2107
- if (methodDeclaration.leadingAnnotations.length !== otherMethodDeclaration.leadingAnnotations.length) {
2108
- return this.abort(methodDeclaration);
2109
- }
2110
-
2111
- for (let i = 0; i < methodDeclaration.leadingAnnotations.length; i++) {
2112
- await this.visit(methodDeclaration.leadingAnnotations[i], otherMethodDeclaration.leadingAnnotations[i]);
2113
- if (!this.match) return methodDeclaration;
2114
- }
2557
+ await this.visitArrayProperty(
2558
+ methodDeclaration,
2559
+ 'leadingAnnotations',
2560
+ methodDeclaration.leadingAnnotations,
2561
+ otherMethodDeclaration.leadingAnnotations,
2562
+ async (ann1, ann2) => { await this.visit(ann1, ann2); }
2563
+ );
2564
+ if (!this.match) return methodDeclaration;
2115
2565
 
2116
2566
  // Visit modifiers
2117
- if (methodDeclaration.modifiers.length !== otherMethodDeclaration.modifiers.length) {
2118
- return this.abort(methodDeclaration);
2119
- }
2120
-
2121
- for (let i = 0; i < methodDeclaration.modifiers.length; i++) {
2122
- await this.visit(methodDeclaration.modifiers[i], otherMethodDeclaration.modifiers[i]);
2123
- if (!this.match) return methodDeclaration;
2124
- }
2567
+ await this.visitArrayProperty(
2568
+ methodDeclaration,
2569
+ 'modifiers',
2570
+ methodDeclaration.modifiers,
2571
+ otherMethodDeclaration.modifiers,
2572
+ async (mod1, mod2) => { await this.visit(mod1, mod2); }
2573
+ );
2574
+ if (!this.match) return methodDeclaration;
2125
2575
 
2126
2576
  // Visit type parameters if present
2127
2577
  if (!!methodDeclaration.typeParameters !== !!otherMethodDeclaration.typeParameters) {
2128
- return this.abort(methodDeclaration);
2578
+ return this.structuralMismatch('typeParameters');
2129
2579
  }
2130
2580
 
2131
2581
  if (methodDeclaration.typeParameters && otherMethodDeclaration.typeParameters) {
@@ -2136,7 +2586,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2136
2586
  // Compare returnTypeExpression - lenient matching allows one to be undefined
2137
2587
  if ((methodDeclaration.returnTypeExpression === undefined) !== (otherMethodDeclaration.returnTypeExpression === undefined)) {
2138
2588
  if (!this.lenientTypeMatching) {
2139
- return this.abort(methodDeclaration);
2589
+ return this.typeMismatch('returnTypeExpression');
2140
2590
  }
2141
2591
  // In lenient mode, skip type comparison and continue
2142
2592
  } else if (methodDeclaration.returnTypeExpression && otherMethodDeclaration.returnTypeExpression) {
@@ -2150,36 +2600,22 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2150
2600
  if (!this.match) return methodDeclaration;
2151
2601
 
2152
2602
  // Compare parameters
2153
- if (methodDeclaration.parameters.elements.length !== otherMethodDeclaration.parameters.elements.length) {
2154
- return this.abort(methodDeclaration);
2155
- }
2156
-
2157
- // Visit each parameter in lock step
2158
- for (let i = 0; i < methodDeclaration.parameters.elements.length; i++) {
2159
- await this.visit(methodDeclaration.parameters.elements[i].element, otherMethodDeclaration.parameters.elements[i].element);
2160
- if (!this.match) return methodDeclaration;
2161
- }
2603
+ await this.visitContainer(methodDeclaration.parameters, otherMethodDeclaration.parameters as any);
2604
+ if (!this.match) return methodDeclaration;
2162
2605
 
2163
2606
  // Visit throws if present
2164
2607
  if (!!methodDeclaration.throws !== !!otherMethodDeclaration.throws) {
2165
- return this.abort(methodDeclaration);
2608
+ return this.structuralMismatch('throws');
2166
2609
  }
2167
2610
 
2168
2611
  if (methodDeclaration.throws && otherMethodDeclaration.throws) {
2169
- // Visit each throws expression in lock step
2170
- if (methodDeclaration.throws.elements.length !== otherMethodDeclaration.throws.elements.length) {
2171
- return this.abort(methodDeclaration);
2172
- }
2173
-
2174
- for (let i = 0; i < methodDeclaration.throws.elements.length; i++) {
2175
- await this.visit(methodDeclaration.throws.elements[i].element, otherMethodDeclaration.throws.elements[i].element);
2176
- if (!this.match) return methodDeclaration;
2177
- }
2612
+ await this.visitContainer(methodDeclaration.throws, otherMethodDeclaration.throws as any);
2613
+ if (!this.match) return methodDeclaration;
2178
2614
  }
2179
2615
 
2180
2616
  // Visit body if present
2181
2617
  if (!!methodDeclaration.body !== !!otherMethodDeclaration.body) {
2182
- return this.abort(methodDeclaration);
2618
+ return this.structuralMismatch('body');
2183
2619
  }
2184
2620
 
2185
2621
  if (methodDeclaration.body && otherMethodDeclaration.body) {
@@ -2189,4 +2625,59 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2189
2625
 
2190
2626
  return methodDeclaration;
2191
2627
  }
2628
+
2629
+ /**
2630
+ * Override visitVoid to allow semantic equivalence with undefined identifier.
2631
+ * This handles the reverse case where the pattern is a void expression
2632
+ * and the source is the undefined identifier.
2633
+ *
2634
+ * Examples:
2635
+ * - `void 0` matches `undefined`
2636
+ * - `void(0)` matches `undefined`
2637
+ * - `void 1` matches `undefined`
2638
+ */
2639
+ override async visitVoid(voidExpr: JS.Void, other: J): Promise<J | undefined> {
2640
+ if (!this.match) return voidExpr;
2641
+
2642
+ // Check if the other is an undefined identifier
2643
+ if ((other as any).kind === J.Kind.Identifier) {
2644
+ const identifier = other as J.Identifier;
2645
+ if (identifier.simpleName === 'undefined') {
2646
+ // Both evaluate to undefined, so they match
2647
+ return voidExpr;
2648
+ }
2649
+ }
2650
+
2651
+ // Otherwise delegate to parent
2652
+ return super.visitVoid(voidExpr, other as any);
2653
+ }
2654
+
2655
+ /**
2656
+ * Override visitLiteral to allow semantic equivalence between
2657
+ * different numeric literal formats.
2658
+ *
2659
+ * Examples:
2660
+ * - `255` matches `0xFF`
2661
+ * - `255` matches `0o377`
2662
+ * - `255` matches `0b11111111`
2663
+ * - `1000` matches `1e3`
2664
+ */
2665
+ override async visitLiteral(literal: J.Literal, other: J): Promise<J | undefined> {
2666
+ if (!this.match) return literal;
2667
+
2668
+ if ((other as any).kind !== J.Kind.Literal) {
2669
+ return await super.visitLiteral(literal, other);
2670
+ }
2671
+
2672
+ const otherLiteral = other as J.Literal;
2673
+
2674
+ // Only compare value and type, ignoring valueSource (text representation) and unicodeEscapes
2675
+ await this.visitProperty(literal.value, otherLiteral.value, 'value');
2676
+ if (!this.match) return literal;
2677
+
2678
+ await this.visitProperty(literal.type, otherLiteral.type, 'type');
2679
+ if (!this.match) return literal;
2680
+
2681
+ return literal;
2682
+ }
2192
2683
  }