@openrewrite/rewrite 8.67.0-20251112-160335 → 8.67.0-20251113-160321

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.
@@ -67,25 +67,158 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
67
67
 
68
68
  /**
69
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
70
76
  */
71
- protected abort<T>(t: T): T {
77
+ protected abort<T>(t: T, reason?: string, propertyName?: string, expected?: any, actual?: any): T {
72
78
  this.match = false;
73
79
  return t;
74
80
  }
75
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
+
76
208
  /**
77
209
  * Generic method to visit a property value using the appropriate visitor method.
78
210
  * This ensures wrappers (RightPadded, LeftPadded, Container) are properly tracked on the cursor.
79
211
  *
80
212
  * @param j The property value from the first tree
81
213
  * @param other The corresponding property value from the second tree
214
+ * @param propertyName Optional property name for error reporting
82
215
  * @returns The visited property value from the first tree
83
216
  */
84
- protected async visitProperty(j: any, other: any): Promise<any> {
217
+ protected async visitProperty(j: any, other: any, propertyName?: string): Promise<any> {
85
218
  // Handle null/undefined (but not other falsy values like 0, false, '')
86
219
  if (j == null || other == null) {
87
220
  if (j !== other) {
88
- this.abort(j);
221
+ return this.structuralMismatch(propertyName);
89
222
  }
90
223
  return j;
91
224
  }
@@ -94,14 +227,20 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
94
227
 
95
228
  // Check wrappers by kind
96
229
  if (kind === J.Kind.RightPadded) {
97
- return await this.visitRightPadded(j, other);
230
+ return propertyName ? await this.visitRightPaddedProperty(propertyName, j, other) :
231
+ await this.visitRightPadded(j, other);
98
232
  }
99
233
 
100
234
  if (kind === J.Kind.LeftPadded) {
101
- return await this.visitLeftPadded(j, other);
235
+ return propertyName ? await this.visitLeftPaddedProperty(propertyName, j, other) :
236
+ await this.visitLeftPadded(j, other);
102
237
  }
103
238
 
104
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
+ }
105
244
  return await this.visitContainer(j, other);
106
245
  }
107
246
 
@@ -121,7 +260,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
121
260
 
122
261
  // For primitive values, compare directly
123
262
  if (j !== other) {
124
- this.abort(j);
263
+ return this.valueMismatch(propertyName, j, other);
125
264
  }
126
265
  return j;
127
266
  }
@@ -136,19 +275,17 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
136
275
  * @returns The visited element from the first tree
137
276
  */
138
277
  protected async visitElement<T extends J>(j: T, other: T): Promise<T> {
139
- if (!this.match) {
140
- return j;
141
- }
278
+ if (!this.match) return j;
142
279
 
143
280
  // Check if kinds match
144
281
  if (j.kind !== other.kind) {
145
- return this.abort(j);
282
+ return this.kindMismatch();
146
283
  }
147
284
 
148
285
  // Iterate over all properties
149
286
  for (const key of Object.keys(j)) {
150
287
  // Skip internal/private properties, id property, and markers property
151
- if (key.startsWith('_') || key === 'id' || key === 'markers') {
288
+ if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers') {
152
289
  continue;
153
290
  }
154
291
 
@@ -158,22 +295,18 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
158
295
  // Handle arrays - compare element by element
159
296
  if (Array.isArray(jValue)) {
160
297
  if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
161
- return this.abort(j);
298
+ return this.arrayLengthMismatch(key);
162
299
  }
163
300
 
164
301
  for (let i = 0; i < jValue.length; i++) {
165
- await this.visitProperty(jValue[i], otherValue[i]);
166
- if (!this.match) {
167
- return j;
168
- }
302
+ await this.visitProperty(jValue[i], otherValue[i], `${key}[${i}]`);
303
+ if (!this.match) return j;
169
304
  }
170
305
  } else {
171
306
  // Visit the property (which will handle wrappers, trees, primitives, etc.)
172
- await this.visitProperty(jValue, otherValue);
307
+ await this.visitProperty(jValue, otherValue, key);
173
308
 
174
- if (!this.match) {
175
- return j;
176
- }
309
+ if (!this.match) return j;
177
310
  }
178
311
  }
179
312
 
@@ -182,13 +315,11 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
182
315
 
183
316
  override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
184
317
  // If we've already found a mismatch, abort further processing
185
- if (!this.match) {
186
- return j as R;
187
- }
318
+ if (!this.match) return j as R;
188
319
 
189
320
  // Check if the nodes have the same kind
190
321
  if (!this.hasSameKind(j as J, p)) {
191
- return this.abort(j) as R;
322
+ return this.kindMismatch() as R;
192
323
  }
193
324
 
194
325
  // Update targetCursor to track the target node in parallel with the pattern cursor
@@ -210,9 +341,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
210
341
  * Also updates targetCursor in parallel.
211
342
  */
212
343
  public async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: J): Promise<J.RightPadded<T>> {
213
- if (!this.match) {
214
- return right;
215
- }
344
+ if (!this.match) return right;
216
345
 
217
346
  // Extract the other element if it's also a RightPadded
218
347
  const isRightPadded = (p as any).kind === J.Kind.RightPadded;
@@ -225,6 +354,8 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
225
354
  this.cursor = new Cursor(right, this.cursor);
226
355
  this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor;
227
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
228
359
  await this.visitProperty(right.element, otherElement);
229
360
  } finally {
230
361
  this.cursor = savedCursor;
@@ -241,9 +372,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
241
372
  * Also updates targetCursor in parallel.
242
373
  */
243
374
  public async visitLeftPadded<T extends J | J.Space | number | string | boolean>(left: J.LeftPadded<T>, p: J): Promise<J.LeftPadded<T>> {
244
- if (!this.match) {
245
- return left;
246
- }
375
+ if (!this.match) return left;
247
376
 
248
377
  // Extract the other element if it's also a LeftPadded
249
378
  const isLeftPadded = (p as any).kind === J.Kind.LeftPadded;
@@ -256,6 +385,8 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
256
385
  this.cursor = new Cursor(left, this.cursor);
257
386
  this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor;
258
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
259
390
  await this.visitProperty(left.element, otherElement);
260
391
  } finally {
261
392
  this.cursor = savedCursor;
@@ -272,9 +403,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
272
403
  * Also updates targetCursor in parallel.
273
404
  */
274
405
  public async visitContainer<T extends J>(container: J.Container<T>, p: J): Promise<J.Container<T>> {
275
- if (!this.match) {
276
- return container;
277
- }
406
+ if (!this.match) return container;
278
407
 
279
408
  // Extract the other elements if it's also a Container
280
409
  const isContainer = (p as any).kind === J.Kind.Container;
@@ -283,7 +412,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
283
412
 
284
413
  // Compare elements array length
285
414
  if (container.elements.length !== otherElements.length) {
286
- return this.abort(container);
415
+ return this.arrayLengthMismatch('elements');
287
416
  }
288
417
 
289
418
  // Push wrappers onto both cursors, then compare each element
@@ -294,9 +423,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
294
423
  try {
295
424
  for (let i = 0; i < container.elements.length; i++) {
296
425
  await this.visitProperty(container.elements[i], otherElements[i]);
297
- if (!this.match) {
298
- return this.abort(container);
299
- }
426
+ if (!this.match) return container;
300
427
  }
301
428
  } finally {
302
429
  this.cursor = savedCursor;
@@ -1866,9 +1993,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1866
1993
 
1867
1994
  override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
1868
1995
  // If we've already found a mismatch, abort further processing
1869
- if (!this.match) {
1870
- return j as R;
1871
- }
1996
+ if (!this.match) return j as R;
1872
1997
 
1873
1998
  // Unwrap parentheses from both trees before comparing
1874
1999
  const unwrappedJ = this.unwrap(j) || j;
@@ -1896,12 +2021,10 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1896
2021
  * - `(x, y) => x + y` matches `(x, y) => { return x + y; }`
1897
2022
  */
1898
2023
  override async visitArrowFunction(arrowFunction: JS.ArrowFunction, other: J): Promise<J | undefined> {
1899
- if (!this.match) {
1900
- return arrowFunction;
1901
- }
2024
+ if (!this.match) return arrowFunction;
1902
2025
 
1903
2026
  if (other.kind !== JS.Kind.ArrowFunction) {
1904
- return this.abort(arrowFunction);
2027
+ return this.kindMismatch();
1905
2028
  }
1906
2029
 
1907
2030
  const otherArrow = other as JS.ArrowFunction;
@@ -1918,7 +2041,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1918
2041
  // Handle arrays
1919
2042
  if (Array.isArray(jValue)) {
1920
2043
  if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
1921
- return this.abort(arrowFunction);
2044
+ return this.arrayLengthMismatch(key);
1922
2045
  }
1923
2046
  for (let i = 0; i < jValue.length; i++) {
1924
2047
  await this.visitProperty(jValue[i], otherValue[i]);
@@ -1934,7 +2057,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1934
2057
  const params1 = arrowFunction.lambda.parameters.parameters;
1935
2058
  const params2 = otherArrow.lambda.parameters.parameters;
1936
2059
  if (params1.length !== params2.length) {
1937
- return this.abort(arrowFunction);
2060
+ return this.arrayLengthMismatch('lambda.parameters.parameters');
1938
2061
  }
1939
2062
  for (let i = 0; i < params1.length; i++) {
1940
2063
  await this.visitProperty(params1[i], params2[i]);
@@ -1969,12 +2092,10 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1969
2092
  * - `x => x + 1` matches `(x) => x + 1`
1970
2093
  */
1971
2094
  override async visitLambdaParameters(parameters: J.Lambda.Parameters, other: J): Promise<J | undefined> {
1972
- if (!this.match) {
1973
- return parameters;
1974
- }
2095
+ if (!this.match) return parameters;
1975
2096
 
1976
2097
  if (other.kind !== J.Kind.LambdaParameters) {
1977
- return this.abort(parameters);
2098
+ return this.kindMismatch();
1978
2099
  }
1979
2100
 
1980
2101
  const otherParams = other as J.Lambda.Parameters;
@@ -1991,7 +2112,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
1991
2112
  // Handle arrays
1992
2113
  if (Array.isArray(jValue)) {
1993
2114
  if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
1994
- return this.abort(parameters);
2115
+ return this.arrayLengthMismatch(key);
1995
2116
  }
1996
2117
  for (let i = 0; i < jValue.length; i++) {
1997
2118
  await this.visitProperty(jValue[i], otherValue[i]);
@@ -2015,12 +2136,10 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2015
2136
  * - `{ x: x, y: y }` matches `{ x, y }`
2016
2137
  */
2017
2138
  override async visitPropertyAssignment(propertyAssignment: JS.PropertyAssignment, other: J): Promise<J | undefined> {
2018
- if (!this.match) {
2019
- return propertyAssignment;
2020
- }
2139
+ if (!this.match) return propertyAssignment;
2021
2140
 
2022
2141
  if (other.kind !== JS.Kind.PropertyAssignment) {
2023
- return this.abort(propertyAssignment);
2142
+ return this.kindMismatch();
2024
2143
  }
2025
2144
 
2026
2145
  const otherProp = other as JS.PropertyAssignment;
@@ -2053,7 +2172,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2053
2172
  return propertyAssignment;
2054
2173
  } else {
2055
2174
  // Not equivalent (e.g., { x: y })
2056
- return this.abort(propertyAssignment);
2175
+ return this.structuralMismatch('initializer');
2057
2176
  }
2058
2177
  }
2059
2178
 
@@ -2109,7 +2228,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2109
2228
  * When lenientTypeMatching is enabled, null vs Type comparisons are allowed
2110
2229
  * (where one value is null/undefined and the other is a Type object).
2111
2230
  */
2112
- protected override async visitProperty(j: any, other: any): Promise<any> {
2231
+ protected override async visitProperty(j: any, other: any, propertyName?: string): Promise<any> {
2113
2232
  // Handle null/undefined with lenient type matching
2114
2233
  if (this.lenientTypeMatching && (j == null || other == null)) {
2115
2234
  if (j !== other) {
@@ -2121,7 +2240,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2121
2240
  (otherKind && typeof otherKind === 'string' && otherKind.startsWith('org.openrewrite.java.tree.JavaType$'));
2122
2241
 
2123
2242
  if (!isTypeComparison) {
2124
- this.abort(j);
2243
+ this.structuralMismatch(propertyName!);
2125
2244
  }
2126
2245
  }
2127
2246
  return j;
@@ -2218,16 +2337,11 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2218
2337
  */
2219
2338
  override async visitMethodInvocation(method: J.MethodInvocation, other: J): Promise<J | undefined> {
2220
2339
  if (other.kind !== J.Kind.MethodInvocation) {
2221
- return this.abort(method);
2340
+ return this.kindMismatch();
2222
2341
  }
2223
2342
 
2224
2343
  const otherMethod = other as J.MethodInvocation;
2225
2344
 
2226
- // Check argument length (always required)
2227
- if (method.arguments.elements.length !== otherMethod.arguments.elements.length) {
2228
- return this.abort(method);
2229
- }
2230
-
2231
2345
  // Check if we can skip name checking based on type attribution
2232
2346
  // We can only skip the name check if both have method types AND they represent the SAME method
2233
2347
  // (not just type-compatible methods, but the actual same function with same FQN)
@@ -2257,7 +2371,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2257
2371
  // Check names unless we determined we can skip based on type FQN matching
2258
2372
  if (!canSkipNameCheck) {
2259
2373
  if (method.name.simpleName !== otherMethod.name.simpleName) {
2260
- return this.abort(method);
2374
+ return this.valueMismatch('name.simpleName', method.name.simpleName, otherMethod.name.simpleName);
2261
2375
  }
2262
2376
 
2263
2377
  // In strict mode, check type attribution requirements
@@ -2265,7 +2379,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2265
2379
  // Strict mode: if one has type but the other doesn't, they don't match
2266
2380
  if ((method.methodType && !otherMethod.methodType) ||
2267
2381
  (!method.methodType && otherMethod.methodType)) {
2268
- return this.abort(method);
2382
+ return this.typeMismatch('methodType');
2269
2383
  }
2270
2384
  }
2271
2385
 
@@ -2288,7 +2402,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2288
2402
 
2289
2403
  // Different declaring types = different methods, even with same name
2290
2404
  if (methodFQN !== otherFQN) {
2291
- return this.abort(method);
2405
+ return this.valueMismatch('methodType.declaringType');
2292
2406
  }
2293
2407
  }
2294
2408
  }
@@ -2299,33 +2413,24 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2299
2413
  if (!canSkipNameCheck) {
2300
2414
  // Types didn't provide a match - must compare receivers structurally
2301
2415
  if ((method.select === undefined) !== (otherMethod.select === undefined)) {
2302
- return this.abort(method);
2416
+ return this.structuralMismatch('select');
2303
2417
  }
2304
2418
 
2305
2419
  if (method.select && otherMethod.select) {
2306
- await this.visitRightPadded(method.select, otherMethod.select as any);
2307
- if (!this.match) {
2308
- return this.abort(method);
2309
- }
2420
+ await this.visitRightPaddedProperty('select', method.select, otherMethod.select as any);
2421
+ if (!this.match) return method;
2310
2422
  }
2311
2423
  }
2312
2424
  // else: types matched, skip select comparison (allows namespace vs named imports)
2313
2425
 
2314
2426
  // Compare type parameters
2315
2427
  if ((method.typeParameters === undefined) !== (otherMethod.typeParameters === undefined)) {
2316
- return this.abort(method);
2428
+ return this.structuralMismatch('typeParameters');
2317
2429
  }
2318
2430
 
2319
2431
  if (method.typeParameters && otherMethod.typeParameters) {
2320
- if (method.typeParameters.elements.length !== otherMethod.typeParameters.elements.length) {
2321
- return this.abort(method);
2322
- }
2323
- for (let i = 0; i < method.typeParameters.elements.length; i++) {
2324
- await this.visitRightPadded(method.typeParameters.elements[i], otherMethod.typeParameters.elements[i] as any);
2325
- if (!this.match) {
2326
- return this.abort(method);
2327
- }
2328
- }
2432
+ await this.visitContainerProperty('typeParameters', method.typeParameters, otherMethod.typeParameters);
2433
+ if (!this.match) return method;
2329
2434
  }
2330
2435
 
2331
2436
  // Compare name
@@ -2333,18 +2438,12 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2333
2438
  // This allows matching aliased imports where names differ but types are the same
2334
2439
  if (!canSkipNameCheck) {
2335
2440
  await this.visit(method.name, otherMethod.name);
2336
- if (!this.match) {
2337
- return this.abort(method);
2338
- }
2441
+ if (!this.match) return method;
2339
2442
  }
2340
2443
 
2341
- // Compare arguments (visit RightPadded to check for markers)
2342
- for (let i = 0; i < method.arguments.elements.length; i++) {
2343
- await this.visitRightPadded(method.arguments.elements[i], otherMethod.arguments.elements[i] as any);
2344
- if (!this.match) {
2345
- return this.abort(method);
2346
- }
2347
- }
2444
+ // Compare arguments
2445
+ await this.visitContainerProperty('arguments', method.arguments, otherMethod.arguments);
2446
+ if (!this.match) return method;
2348
2447
 
2349
2448
  return method;
2350
2449
  }
@@ -2362,26 +2461,26 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2362
2461
  }
2363
2462
 
2364
2463
  if (other.kind !== J.Kind.Identifier) {
2365
- return this.abort(identifier);
2464
+ return this.kindMismatch();
2366
2465
  }
2367
2466
 
2368
2467
  const otherIdentifier = other as J.Identifier;
2369
2468
 
2370
2469
  // Check name matches
2371
2470
  if (identifier.simpleName !== otherIdentifier.simpleName) {
2372
- return this.abort(identifier);
2471
+ return this.valueMismatch('simpleName');
2373
2472
  }
2374
2473
 
2375
2474
  // For identifiers with field types, check type attribution
2376
2475
  if (identifier.fieldType && otherIdentifier.fieldType) {
2377
2476
  if (!this.isOfType(identifier.fieldType, otherIdentifier.fieldType)) {
2378
- return this.abort(identifier);
2477
+ return this.typeMismatch('fieldType');
2379
2478
  }
2380
2479
  } else if (identifier.fieldType || otherIdentifier.fieldType) {
2381
2480
  // Lenient mode: if either has no type, allow structural matching
2382
2481
  if (!this.lenientTypeMatching) {
2383
2482
  // Strict mode: if only one has a type, they don't match
2384
- return this.abort(identifier);
2483
+ return this.typeMismatch('fieldType');
2385
2484
  }
2386
2485
  }
2387
2486
 
@@ -2397,29 +2496,29 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2397
2496
  const otherVariableDeclarations = other as J.VariableDeclarations;
2398
2497
 
2399
2498
  // Visit leading annotations
2400
- if (variableDeclarations.leadingAnnotations.length !== otherVariableDeclarations.leadingAnnotations.length) {
2401
- return this.abort(variableDeclarations);
2402
- }
2403
-
2404
- for (let i = 0; i < variableDeclarations.leadingAnnotations.length; i++) {
2405
- await this.visit(variableDeclarations.leadingAnnotations[i], otherVariableDeclarations.leadingAnnotations[i]);
2406
- if (!this.match) return variableDeclarations;
2407
- }
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;
2408
2507
 
2409
2508
  // Visit modifiers
2410
- if (variableDeclarations.modifiers.length !== otherVariableDeclarations.modifiers.length) {
2411
- return this.abort(variableDeclarations);
2412
- }
2413
-
2414
- for (let i = 0; i < variableDeclarations.modifiers.length; i++) {
2415
- await this.visit(variableDeclarations.modifiers[i], otherVariableDeclarations.modifiers[i]);
2416
- if (!this.match) return variableDeclarations;
2417
- }
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;
2418
2517
 
2419
2518
  // Compare typeExpression - lenient matching allows one to be undefined
2420
2519
  if ((variableDeclarations.typeExpression === undefined) !== (otherVariableDeclarations.typeExpression === undefined)) {
2421
2520
  if (!this.lenientTypeMatching) {
2422
- return this.abort(variableDeclarations);
2521
+ return this.structuralMismatch('typeExpression');
2423
2522
  }
2424
2523
  // In lenient mode, skip type comparison and continue
2425
2524
  } else if (variableDeclarations.typeExpression && otherVariableDeclarations.typeExpression) {
@@ -2430,19 +2529,18 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2430
2529
 
2431
2530
  // Compare varargs
2432
2531
  if ((variableDeclarations.varargs === undefined) !== (otherVariableDeclarations.varargs === undefined)) {
2433
- return this.abort(variableDeclarations);
2532
+ return this.structuralMismatch('varargs');
2434
2533
  }
2435
2534
 
2436
2535
  // Compare variables
2437
- if (variableDeclarations.variables.length !== otherVariableDeclarations.variables.length) {
2438
- return this.abort(variableDeclarations);
2439
- }
2440
-
2441
- // Visit each variable in lock step
2442
- for (let i = 0; i < variableDeclarations.variables.length; i++) {
2443
- await this.visitRightPadded(variableDeclarations.variables[i], otherVariableDeclarations.variables[i] as any);
2444
- if (!this.match) return variableDeclarations;
2445
- }
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;
2446
2544
 
2447
2545
  return variableDeclarations;
2448
2546
  }
@@ -2456,28 +2554,28 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2456
2554
  const otherMethodDeclaration = other as J.MethodDeclaration;
2457
2555
 
2458
2556
  // Visit leading annotations
2459
- if (methodDeclaration.leadingAnnotations.length !== otherMethodDeclaration.leadingAnnotations.length) {
2460
- return this.abort(methodDeclaration);
2461
- }
2462
-
2463
- for (let i = 0; i < methodDeclaration.leadingAnnotations.length; i++) {
2464
- await this.visit(methodDeclaration.leadingAnnotations[i], otherMethodDeclaration.leadingAnnotations[i]);
2465
- if (!this.match) return methodDeclaration;
2466
- }
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;
2467
2565
 
2468
2566
  // Visit modifiers
2469
- if (methodDeclaration.modifiers.length !== otherMethodDeclaration.modifiers.length) {
2470
- return this.abort(methodDeclaration);
2471
- }
2472
-
2473
- for (let i = 0; i < methodDeclaration.modifiers.length; i++) {
2474
- await this.visit(methodDeclaration.modifiers[i], otherMethodDeclaration.modifiers[i]);
2475
- if (!this.match) return methodDeclaration;
2476
- }
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;
2477
2575
 
2478
2576
  // Visit type parameters if present
2479
2577
  if (!!methodDeclaration.typeParameters !== !!otherMethodDeclaration.typeParameters) {
2480
- return this.abort(methodDeclaration);
2578
+ return this.structuralMismatch('typeParameters');
2481
2579
  }
2482
2580
 
2483
2581
  if (methodDeclaration.typeParameters && otherMethodDeclaration.typeParameters) {
@@ -2488,7 +2586,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2488
2586
  // Compare returnTypeExpression - lenient matching allows one to be undefined
2489
2587
  if ((methodDeclaration.returnTypeExpression === undefined) !== (otherMethodDeclaration.returnTypeExpression === undefined)) {
2490
2588
  if (!this.lenientTypeMatching) {
2491
- return this.abort(methodDeclaration);
2589
+ return this.typeMismatch('returnTypeExpression');
2492
2590
  }
2493
2591
  // In lenient mode, skip type comparison and continue
2494
2592
  } else if (methodDeclaration.returnTypeExpression && otherMethodDeclaration.returnTypeExpression) {
@@ -2502,36 +2600,22 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2502
2600
  if (!this.match) return methodDeclaration;
2503
2601
 
2504
2602
  // Compare parameters
2505
- if (methodDeclaration.parameters.elements.length !== otherMethodDeclaration.parameters.elements.length) {
2506
- return this.abort(methodDeclaration);
2507
- }
2508
-
2509
- // Visit each parameter in lock step
2510
- for (let i = 0; i < methodDeclaration.parameters.elements.length; i++) {
2511
- await this.visitRightPadded(methodDeclaration.parameters.elements[i], otherMethodDeclaration.parameters.elements[i] as any);
2512
- if (!this.match) return methodDeclaration;
2513
- }
2603
+ await this.visitContainer(methodDeclaration.parameters, otherMethodDeclaration.parameters as any);
2604
+ if (!this.match) return methodDeclaration;
2514
2605
 
2515
2606
  // Visit throws if present
2516
2607
  if (!!methodDeclaration.throws !== !!otherMethodDeclaration.throws) {
2517
- return this.abort(methodDeclaration);
2608
+ return this.structuralMismatch('throws');
2518
2609
  }
2519
2610
 
2520
2611
  if (methodDeclaration.throws && otherMethodDeclaration.throws) {
2521
- // Visit each throws expression in lock step
2522
- if (methodDeclaration.throws.elements.length !== otherMethodDeclaration.throws.elements.length) {
2523
- return this.abort(methodDeclaration);
2524
- }
2525
-
2526
- for (let i = 0; i < methodDeclaration.throws.elements.length; i++) {
2527
- await this.visitRightPadded(methodDeclaration.throws.elements[i], otherMethodDeclaration.throws.elements[i] as any);
2528
- if (!this.match) return methodDeclaration;
2529
- }
2612
+ await this.visitContainer(methodDeclaration.throws, otherMethodDeclaration.throws as any);
2613
+ if (!this.match) return methodDeclaration;
2530
2614
  }
2531
2615
 
2532
2616
  // Visit body if present
2533
2617
  if (!!methodDeclaration.body !== !!otherMethodDeclaration.body) {
2534
- return this.abort(methodDeclaration);
2618
+ return this.structuralMismatch('body');
2535
2619
  }
2536
2620
 
2537
2621
  if (methodDeclaration.body && otherMethodDeclaration.body) {
@@ -2553,9 +2637,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2553
2637
  * - `void 1` matches `undefined`
2554
2638
  */
2555
2639
  override async visitVoid(voidExpr: JS.Void, other: J): Promise<J | undefined> {
2556
- if (!this.match) {
2557
- return voidExpr;
2558
- }
2640
+ if (!this.match) return voidExpr;
2559
2641
 
2560
2642
  // Check if the other is an undefined identifier
2561
2643
  if ((other as any).kind === J.Kind.Identifier) {
@@ -2581,9 +2663,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2581
2663
  * - `1000` matches `1e3`
2582
2664
  */
2583
2665
  override async visitLiteral(literal: J.Literal, other: J): Promise<J | undefined> {
2584
- if (!this.match) {
2585
- return literal;
2586
- }
2666
+ if (!this.match) return literal;
2587
2667
 
2588
2668
  if ((other as any).kind !== J.Kind.Literal) {
2589
2669
  return await super.visitLiteral(literal, other);
@@ -2592,10 +2672,10 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2592
2672
  const otherLiteral = other as J.Literal;
2593
2673
 
2594
2674
  // Only compare value and type, ignoring valueSource (text representation) and unicodeEscapes
2595
- await this.visitProperty(literal.value, otherLiteral.value);
2675
+ await this.visitProperty(literal.value, otherLiteral.value, 'value');
2596
2676
  if (!this.match) return literal;
2597
2677
 
2598
- await this.visitProperty(literal.type, otherLiteral.type);
2678
+ await this.visitProperty(literal.type, otherLiteral.type, 'type');
2599
2679
  if (!this.match) return literal;
2600
2680
 
2601
2681
  return literal;