@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
@@ -17,7 +17,50 @@ import {Cursor, Tree} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {JS} from '../index';
19
19
  import {JavaScriptSemanticComparatorVisitor} from '../comparator';
20
- import {PlaceholderUtils, CaptureStorageValue} from './utils';
20
+ import {CaptureMarker, CaptureStorageValue, PlaceholderUtils} from './utils';
21
+ import {DebugLogEntry, MatchExplanation} from './types';
22
+
23
+ /**
24
+ * Debug callbacks for pattern matching.
25
+ * These are always used together - either all present or all absent.
26
+ * Part of Layer 1 (Core Instrumentation).
27
+ */
28
+ export interface DebugCallbacks {
29
+ log: (level: DebugLogEntry['level'], scope: DebugLogEntry['scope'], message: string, data?: any) => void;
30
+ setExplanation: (reason: MatchExplanation['reason'], expected: string, actual: string, details?: string) => void;
31
+ getExplanation: () => MatchExplanation | undefined;
32
+ restoreExplanation: (explanation: MatchExplanation) => void;
33
+ clearExplanation: () => void;
34
+ pushPath: (name: string) => void;
35
+ popPath: () => void;
36
+ }
37
+
38
+ /**
39
+ * Snapshot of matcher state for backtracking.
40
+ * Includes both capture storage and debug state.
41
+ */
42
+ export interface MatcherState {
43
+ storage: Map<string, CaptureStorageValue>;
44
+ debugState?: {
45
+ explanation?: MatchExplanation;
46
+ logLength: number;
47
+ path: string[];
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Callbacks for the matcher (debug and capture handling).
53
+ * Part of Layer 1 (Core Instrumentation).
54
+ */
55
+ export interface MatcherCallbacks {
56
+ handleCapture: (capture: CaptureMarker, target: J, wrapper?: J.RightPadded<J>) => boolean;
57
+ handleVariadicCapture: (capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded<J>[]) => boolean;
58
+ saveState: () => MatcherState;
59
+ restoreState: (state: MatcherState) => void;
60
+
61
+ // Debug callbacks - either all present (when debugging enabled) or absent
62
+ debug?: DebugCallbacks;
63
+ }
21
64
 
22
65
  /**
23
66
  * A comparator for pattern matching that is lenient about optional properties.
@@ -26,69 +69,197 @@ import {PlaceholderUtils, CaptureStorageValue} from './utils';
26
69
  */
27
70
  export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisitor {
28
71
  constructor(
29
- private readonly matcher: {
30
- handleCapture: (pattern: J, target: J, wrapper?: J.RightPadded<J>) => boolean;
31
- handleVariadicCapture: (pattern: J, targets: J[], wrappers?: J.RightPadded<J>[]) => boolean;
32
- saveState: () => Map<string, CaptureStorageValue>;
33
- restoreState: (state: Map<string, CaptureStorageValue>) => void;
34
- },
72
+ protected readonly matcher: MatcherCallbacks,
35
73
  lenientTypeMatching: boolean = true
36
74
  ) {
37
75
  // Enable lenient type matching based on pattern configuration (default: true for backward compatibility)
38
76
  super(lenientTypeMatching);
39
77
  }
40
78
 
41
- /**
42
- * Extracts the wrapper from the cursor if the parent is a RightPadded.
43
- */
44
- private getWrapperFromCursor(cursor: Cursor): J.RightPadded<J> | undefined {
45
- if (!cursor.parent) {
46
- return undefined;
47
- }
48
- const parent = cursor.parent.value;
49
- // Check if parent is a RightPadded by checking its kind
50
- if ((parent as any).kind === J.Kind.RightPadded) {
51
- return parent as J.RightPadded<J>;
52
- }
53
- return undefined;
54
- }
55
-
56
79
  override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
57
- // Check if the pattern node is a capture - this handles captures anywhere in the tree
58
- if (PlaceholderUtils.isCapture(j as J)) {
59
- const wrapper = this.getWrapperFromCursor(this.cursor);
60
- const success = this.matcher.handleCapture(j as J, p, wrapper);
61
- if (!success) {
62
- return this.abort(j) as R;
80
+ // Check if the pattern node is a capture - this handles unwrapped captures
81
+ // (Wrapped captures in J.RightPadded are handled by visitRightPadded override)
82
+ // Note: targetCursor will be pushed by parent's visit() method after this check
83
+ const captureMarker = PlaceholderUtils.getCaptureMarker(j)!;
84
+ if (captureMarker) {
85
+
86
+ // Push targetCursor to position it at the captured node for constraint evaluation
87
+ // Only create cursor if targetCursor was initialized (meaning user provided one)
88
+ const savedTargetCursor = this.targetCursor;
89
+ const cursorAtCapturedNode = this.targetCursor !== undefined
90
+ ? new Cursor(p, this.targetCursor)
91
+ : new Cursor(p);
92
+ this.targetCursor = cursorAtCapturedNode;
93
+ try {
94
+ // Evaluate constraint with cursor at the captured node (always defined)
95
+ // Skip constraint for variadic captures - they're evaluated in matchSequence with the full array
96
+ if (captureMarker.constraint && !captureMarker.variadicOptions && !captureMarker.constraint(p, cursorAtCapturedNode)) {
97
+ const captureName = captureMarker.captureName || 'unnamed';
98
+ const targetKind = (p as any).kind || 'unknown';
99
+ return this.constraintFailed(captureName, targetKind) as R;
100
+ }
101
+
102
+ const success = this.matcher.handleCapture(captureMarker, p, undefined);
103
+ if (!success) {
104
+ const captureName = captureMarker.captureName || 'unnamed';
105
+ return this.captureConflict(captureName) as R;
106
+ }
107
+ return j as R;
108
+ } finally {
109
+ this.targetCursor = savedTargetCursor;
63
110
  }
64
- return j as R;
65
111
  }
66
112
 
67
113
  if (!this.match) {
68
114
  return j as R;
69
115
  }
70
116
 
71
- return super.visit(j, p, parent);
117
+ // Continue with parent's visit which will push targetCursor and traverse
118
+ return await super.visit(j, p, parent);
72
119
  }
73
120
 
74
121
  protected hasSameKind(j: J, other: J): boolean {
75
122
  return super.hasSameKind(j, other) ||
76
- (j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
123
+ (j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
124
+ }
125
+
126
+ /**
127
+ * Additional specialized abort methods for pattern matching scenarios.
128
+ */
129
+
130
+ protected constraintFailed(captureName: string, targetKind: string) {
131
+ const pattern = this.cursor?.value as any;
132
+ return this.abort(pattern, 'constraint-failed', `capture[${captureName}]`, 'constraint satisfied', `constraint failed for ${targetKind}`);
133
+ }
134
+
135
+ protected captureConflict(captureName: string) {
136
+ const pattern = this.cursor?.value as any;
137
+ return this.abort(pattern, 'capture-conflict', `capture[${captureName}]`, 'compatible binding', 'conflicting binding');
77
138
  }
78
139
 
79
- override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
80
- if (PlaceholderUtils.isCapture(identifier)) {
81
- const wrapper = this.getWrapperFromCursor(this.cursor);
82
- const success = this.matcher.handleCapture(identifier, other, wrapper);
83
- return success ? identifier : this.abort(identifier);
140
+ /**
141
+ * Override visitRightPadded to check if this wrapper has a CaptureMarker.
142
+ * If so, capture the entire wrapper (to preserve markers like semicolons).
143
+ */
144
+ override async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: J): Promise<J.RightPadded<T>> {
145
+ if (!this.match) {
146
+ return right;
147
+ }
148
+
149
+ // Check if this RightPadded has a CaptureMarker (attached during pattern construction)
150
+ // Note: Markers are now only at the wrapper level, not at the element level
151
+ const captureMarker = PlaceholderUtils.getCaptureMarker(right);
152
+ if (captureMarker) {
153
+ // Extract the target wrapper if it's also a RightPadded
154
+ const isRightPadded = (p as any).kind === J.Kind.RightPadded;
155
+ const targetWrapper = isRightPadded ? (p as unknown) as J.RightPadded<T> : undefined;
156
+ const targetElement = isRightPadded ? targetWrapper!.element : p;
157
+
158
+ // Push targetCursor to position it at the captured element for constraint evaluation
159
+ const savedTargetCursor = this.targetCursor;
160
+ const cursorAtCapturedNode = this.targetCursor !== undefined
161
+ ? (targetWrapper ? new Cursor(targetWrapper, this.targetCursor) : new Cursor(targetElement, this.targetCursor))
162
+ : (targetWrapper ? new Cursor(targetWrapper) : new Cursor(targetElement));
163
+ this.targetCursor = cursorAtCapturedNode;
164
+ try {
165
+ // Evaluate constraint with cursor at the captured node (always defined)
166
+ // Skip constraint for variadic captures - they're evaluated in matchSequence with the full array
167
+ if (captureMarker.constraint && !captureMarker.variadicOptions && !captureMarker.constraint(targetElement as J, cursorAtCapturedNode)) {
168
+ const captureName = captureMarker.captureName || 'unnamed';
169
+ const targetKind = (targetElement as any).kind || 'unknown';
170
+ return this.constraintFailed(captureName, targetKind);
171
+ }
172
+
173
+ // Handle the capture with the wrapper - use the element for pattern matching
174
+ const success = this.matcher.handleCapture(captureMarker, targetElement as J, targetWrapper as J.RightPadded<J> | undefined);
175
+ if (!success) {
176
+ const captureName = captureMarker.captureName || 'unnamed';
177
+ return this.captureConflict(captureName);
178
+ }
179
+ return right;
180
+ } finally {
181
+ this.targetCursor = savedTargetCursor;
182
+ }
183
+ }
184
+
185
+ // Not a capture wrapper - use parent implementation
186
+ return super.visitRightPadded(right, p);
187
+ }
188
+
189
+ override async visitContainer<T extends J>(container: J.Container<T>, p: J): Promise<J.Container<T>> {
190
+ // Check if any elements are variadic captures
191
+ const hasVariadicCapture = container.elements.some(elem =>
192
+ PlaceholderUtils.isVariadicCapture(elem)
193
+ );
194
+
195
+ // If no variadic captures, use parent implementation
196
+ if (!hasVariadicCapture) {
197
+ return super.visitContainer(container, p);
198
+ }
199
+
200
+ // Otherwise, handle variadic captures ourselves
201
+ if (!this.match) {
202
+ return container;
203
+ }
204
+
205
+ // Extract the other container
206
+ const isContainer = (p as any).kind === J.Kind.Container;
207
+ if (!isContainer) {
208
+ // Set up cursors temporarily for kindMismatch to use
209
+ const savedCursor = this.cursor;
210
+ const savedTargetCursor = this.targetCursor;
211
+ this.cursor = new Cursor(container, this.cursor);
212
+ this.targetCursor = new Cursor(p, this.targetCursor);
213
+ try {
214
+ return this.kindMismatch();
215
+ } finally {
216
+ this.cursor = savedCursor;
217
+ this.targetCursor = savedTargetCursor;
218
+ }
84
219
  }
85
- return super.visitIdentifier(identifier, other);
220
+ const otherContainer = p as unknown as J.Container<T>;
221
+
222
+ // Push wrappers onto both cursors
223
+ const savedCursor = this.cursor;
224
+ const savedTargetCursor = this.targetCursor;
225
+ this.cursor = new Cursor(container, this.cursor);
226
+ this.targetCursor = new Cursor(otherContainer, this.targetCursor);
227
+ try {
228
+ // Use matchSequence for variadic matching
229
+ // filterEmpty=true to skip J.Empty elements (they represent missing elements in destructuring)
230
+ if (!await this.matchSequence(container.elements as J.RightPadded<J>[], otherContainer.elements as J.RightPadded<J>[], true)) {
231
+ return this.structuralMismatch('elements');
232
+ }
233
+ } finally {
234
+ this.cursor = savedCursor;
235
+ this.targetCursor = savedTargetCursor;
236
+ }
237
+
238
+ return container;
239
+ }
240
+
241
+ /**
242
+ * Visit a single element in a container (for non-variadic matching).
243
+ * Extracted to allow debug subclass to add path tracking.
244
+ *
245
+ * @param element The pattern element
246
+ * @param otherElement The target element
247
+ * @param index The index in the container
248
+ * @returns true if matching should continue, false if it failed
249
+ */
250
+ protected async visitContainerElement<T extends J>(
251
+ element: J.RightPadded<T>,
252
+ otherElement: J.RightPadded<T>,
253
+ index: number
254
+ ): Promise<boolean> {
255
+ await this.visitRightPadded(element as any, otherElement as any);
256
+ return this.match;
86
257
  }
87
258
 
88
259
  override async visitMethodInvocation(methodInvocation: J.MethodInvocation, other: J): Promise<J | undefined> {
89
260
  // Check if any arguments are variadic captures
90
261
  const hasVariadicCapture = methodInvocation.arguments.elements.some(arg =>
91
- PlaceholderUtils.isVariadicCapture(arg.element)
262
+ PlaceholderUtils.isVariadicCapture(arg)
92
263
  );
93
264
 
94
265
  // If no variadic captures, use parent implementation (which includes semantic/type-aware matching)
@@ -97,57 +268,83 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
97
268
  }
98
269
 
99
270
  // Otherwise, handle variadic captures ourselves
100
- if (!this.match || other.kind !== J.Kind.MethodInvocation) {
271
+ if (!this.match) {
101
272
  return this.abort(methodInvocation);
102
273
  }
103
274
 
275
+ if (other.kind !== J.Kind.MethodInvocation) {
276
+ // Set up cursors for kindMismatch
277
+ const savedCursor = this.cursor;
278
+ const savedTargetCursor = this.targetCursor;
279
+ this.cursor = new Cursor(methodInvocation, this.cursor);
280
+ this.targetCursor = new Cursor(other, this.targetCursor);
281
+ try {
282
+ return this.kindMismatch();
283
+ } finally {
284
+ this.cursor = savedCursor;
285
+ this.targetCursor = savedTargetCursor;
286
+ }
287
+ }
288
+
104
289
  const otherMethodInvocation = other as J.MethodInvocation;
105
290
 
106
- // Compare select
107
- if ((methodInvocation.select === undefined) !== (otherMethodInvocation.select === undefined)) {
108
- return this.abort(methodInvocation);
109
- }
291
+ // Set up cursors for the entire method
292
+ const savedCursor = this.cursor;
293
+ const savedTargetCursor = this.targetCursor;
294
+ this.cursor = new Cursor(methodInvocation, this.cursor);
295
+ this.targetCursor = new Cursor(otherMethodInvocation, this.targetCursor);
296
+ try {
297
+ // Compare select
298
+ if ((methodInvocation.select === undefined) !== (otherMethodInvocation.select === undefined)) {
299
+ return this.structuralMismatch('select');
300
+ }
110
301
 
111
- // Visit select if present
112
- if (methodInvocation.select && otherMethodInvocation.select) {
113
- await this.visit(methodInvocation.select.element, otherMethodInvocation.select.element);
114
- if (!this.match) return methodInvocation;
115
- }
302
+ // Visit select if present
303
+ if (methodInvocation.select && otherMethodInvocation.select) {
304
+ await this.visit(methodInvocation.select.element, otherMethodInvocation.select.element);
305
+ if (!this.match) return methodInvocation;
306
+ }
116
307
 
117
- // Compare typeParameters
118
- if ((methodInvocation.typeParameters === undefined) !== (otherMethodInvocation.typeParameters === undefined)) {
119
- return this.abort(methodInvocation);
120
- }
308
+ // Compare typeParameters
309
+ if ((methodInvocation.typeParameters === undefined) !== (otherMethodInvocation.typeParameters === undefined)) {
310
+ return this.structuralMismatch('typeParameters');
311
+ }
312
+
313
+ // Visit typeParameters if present
314
+ if (methodInvocation.typeParameters && otherMethodInvocation.typeParameters) {
315
+ if (methodInvocation.typeParameters.elements.length !== otherMethodInvocation.typeParameters.elements.length) {
316
+ return this.arrayLengthMismatch('typeParameters.elements');
317
+ }
121
318
 
122
- // Visit typeParameters if present
123
- if (methodInvocation.typeParameters && otherMethodInvocation.typeParameters) {
124
- if (methodInvocation.typeParameters.elements.length !== otherMethodInvocation.typeParameters.elements.length) {
125
- return this.abort(methodInvocation);
319
+ // Visit each type parameter in lock step (visit RightPadded to check for markers)
320
+ for (let i = 0; i < methodInvocation.typeParameters.elements.length; i++) {
321
+ await this.visitRightPadded(methodInvocation.typeParameters.elements[i], otherMethodInvocation.typeParameters.elements[i] as any);
322
+ if (!this.match) return methodInvocation;
323
+ }
126
324
  }
127
325
 
128
- // Visit each type parameter in lock step
129
- for (let i = 0; i < methodInvocation.typeParameters.elements.length; i++) {
130
- await this.visit(methodInvocation.typeParameters.elements[i].element, otherMethodInvocation.typeParameters.elements[i].element);
131
- if (!this.match) return methodInvocation;
326
+ // Visit name
327
+ await this.visit(methodInvocation.name, otherMethodInvocation.name);
328
+ if (!this.match) {
329
+ return methodInvocation;
132
330
  }
133
- }
134
331
 
135
- // Visit name
136
- await this.visit(methodInvocation.name, otherMethodInvocation.name);
137
- if (!this.match) return methodInvocation;
332
+ // Special handling for variadic captures in arguments
333
+ if (!await this.matchArguments(methodInvocation.arguments.elements, otherMethodInvocation.arguments.elements)) {
334
+ return this.structuralMismatch('arguments');
335
+ }
138
336
 
139
- // Special handling for variadic captures in arguments
140
- if (!await this.matchArguments(methodInvocation.arguments.elements, otherMethodInvocation.arguments.elements)) {
141
- return this.abort(methodInvocation);
337
+ return methodInvocation;
338
+ } finally {
339
+ this.cursor = savedCursor;
340
+ this.targetCursor = savedTargetCursor;
142
341
  }
143
-
144
- return methodInvocation;
145
342
  }
146
343
 
147
344
  override async visitBlock(block: J.Block, other: J): Promise<J | undefined> {
148
345
  // Check if any statements have CaptureMarker indicating they're variadic
149
346
  const hasVariadicCapture = block.statements.some(stmt => {
150
- const captureMarker = PlaceholderUtils.getCaptureMarker(stmt.element);
347
+ const captureMarker = PlaceholderUtils.getCaptureMarker(stmt);
151
348
  return captureMarker?.variadicOptions !== undefined;
152
349
  });
153
350
 
@@ -157,25 +354,48 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
157
354
  }
158
355
 
159
356
  // Otherwise, handle variadic captures ourselves
160
- if (!this.match || other.kind !== J.Kind.Block) {
357
+ if (!this.match) {
161
358
  return this.abort(block);
162
359
  }
163
360
 
361
+ if (other.kind !== J.Kind.Block) {
362
+ // Set up cursors for kindMismatch
363
+ const savedCursor = this.cursor;
364
+ const savedTargetCursor = this.targetCursor;
365
+ this.cursor = new Cursor(block, this.cursor);
366
+ this.targetCursor = new Cursor(other, this.targetCursor);
367
+ try {
368
+ return this.kindMismatch();
369
+ } finally {
370
+ this.cursor = savedCursor;
371
+ this.targetCursor = savedTargetCursor;
372
+ }
373
+ }
374
+
164
375
  const otherBlock = other as J.Block;
165
376
 
166
- // Special handling for variadic captures in statements
167
- if (!await this.matchSequence(block.statements, otherBlock.statements, false)) {
168
- return this.abort(block);
169
- }
377
+ // Set up cursors for structural comparison
378
+ const savedCursor = this.cursor;
379
+ const savedTargetCursor = this.targetCursor;
380
+ this.cursor = new Cursor(block, this.cursor);
381
+ this.targetCursor = new Cursor(otherBlock, this.targetCursor);
382
+ try {
383
+ // Special handling for variadic captures in statements
384
+ if (!await this.matchSequence(block.statements, otherBlock.statements, false)) {
385
+ return this.structuralMismatch('statements');
386
+ }
170
387
 
171
- return block;
388
+ return block;
389
+ } finally {
390
+ this.cursor = savedCursor;
391
+ this.targetCursor = savedTargetCursor;
392
+ }
172
393
  }
173
394
 
174
395
  override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, other: J): Promise<J | undefined> {
175
- // Check if any statements are variadic captures (unwrap ExpressionStatement wrappers first)
396
+ // Check if any statements are variadic captures
176
397
  const hasVariadicCapture = compilationUnit.statements.some(stmt => {
177
- const unwrapped = PlaceholderUtils.unwrapStatementCapture(stmt.element);
178
- return PlaceholderUtils.isVariadicCapture(unwrapped);
398
+ return PlaceholderUtils.isVariadicCapture(stmt);
179
399
  });
180
400
 
181
401
  // If no variadic captures, use parent implementation
@@ -184,18 +404,42 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
184
404
  }
185
405
 
186
406
  // Otherwise, handle variadic captures ourselves
187
- if (!this.match || other.kind !== JS.Kind.CompilationUnit) {
407
+ if (!this.match) {
188
408
  return this.abort(compilationUnit);
189
409
  }
190
410
 
411
+ if (other.kind !== JS.Kind.CompilationUnit) {
412
+ // Set up cursors for kindMismatch
413
+ const savedCursor = this.cursor;
414
+ const savedTargetCursor = this.targetCursor;
415
+ this.cursor = new Cursor(compilationUnit, this.cursor);
416
+ this.targetCursor = new Cursor(other, this.targetCursor);
417
+ try {
418
+ return this.kindMismatch();
419
+ } finally {
420
+ this.cursor = savedCursor;
421
+ this.targetCursor = savedTargetCursor;
422
+ }
423
+ }
424
+
191
425
  const otherCompilationUnit = other as JS.CompilationUnit;
192
426
 
193
- // Special handling for variadic captures in top-level statements
194
- if (!await this.matchSequence(compilationUnit.statements, otherCompilationUnit.statements, false)) {
195
- return this.abort(compilationUnit);
196
- }
427
+ // Set up cursors for structural comparison
428
+ const savedCursor = this.cursor;
429
+ const savedTargetCursor = this.targetCursor;
430
+ this.cursor = new Cursor(compilationUnit, this.cursor);
431
+ this.targetCursor = new Cursor(otherCompilationUnit, this.targetCursor);
432
+ try {
433
+ // Special handling for variadic captures in top-level statements
434
+ if (!await this.matchSequence(compilationUnit.statements, otherCompilationUnit.statements, false)) {
435
+ return this.structuralMismatch('statements');
436
+ }
197
437
 
198
- return compilationUnit;
438
+ return compilationUnit;
439
+ } finally {
440
+ this.cursor = savedCursor;
441
+ this.targetCursor = savedTargetCursor;
442
+ }
199
443
  }
200
444
 
201
445
  /**
@@ -203,7 +447,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
203
447
  * A variadic capture can match zero or more consecutive arguments.
204
448
  */
205
449
  private async matchArguments(patternArgs: J.RightPadded<J>[], targetArgs: J.RightPadded<J>[]): Promise<boolean> {
206
- return this.matchSequence(patternArgs, targetArgs, true);
450
+ return await this.matchSequence(patternArgs, targetArgs, true);
207
451
  }
208
452
 
209
453
  /**
@@ -218,7 +462,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
218
462
  * @param filterEmpty Whether to filter out J.Empty elements when capturing (true for arguments, false for statements)
219
463
  * @returns true if the sequence matches, false otherwise
220
464
  */
221
- private async matchSequence(patternElements: J.RightPadded<J>[], targetElements: J.RightPadded<J>[], filterEmpty: boolean): Promise<boolean> {
465
+ protected async matchSequence(patternElements: J.RightPadded<J>[], targetElements: J.RightPadded<J>[], filterEmpty: boolean): Promise<boolean> {
222
466
  return await this.matchSequenceOptimized(patternElements, targetElements, 0, 0, filterEmpty);
223
467
  }
224
468
 
@@ -234,7 +478,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
234
478
  * @param filterEmpty Whether to filter out J.Empty elements when capturing
235
479
  * @returns true if the remaining sequence matches, false otherwise
236
480
  */
237
- private async matchSequenceOptimized(
481
+ protected async matchSequenceOptimized(
238
482
  patternElements: J.RightPadded<J>[],
239
483
  targetElements: J.RightPadded<J>[],
240
484
  patternIdx: number,
@@ -246,8 +490,9 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
246
490
  return targetIdx >= targetElements.length; // Success if all targets consumed
247
491
  }
248
492
 
249
- const patternElement = patternElements[patternIdx].element;
250
- const captureMarker = PlaceholderUtils.getCaptureMarker(patternElement);
493
+ // Check for markers at wrapper level only (markers are now only at the outermost level)
494
+ const patternWrapper = patternElements[patternIdx];
495
+ const captureMarker = PlaceholderUtils.getCaptureMarker(patternWrapper);
251
496
  const isVariadic = captureMarker?.variadicOptions !== undefined;
252
497
 
253
498
  if (isVariadic) {
@@ -256,15 +501,20 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
256
501
  const min = variadicOptions?.min ?? 0;
257
502
  const max = variadicOptions?.max ?? Infinity;
258
503
 
259
- // Calculate maximum possible consumption
504
+ // Calculate maximum possible consumption and check if remaining patterns are deterministic
260
505
  let nonVariadicRemainingPatterns = 0;
506
+ let allRemainingPatternsAreDeterministic = true;
261
507
  for (let i = patternIdx + 1; i < patternElements.length; i++) {
262
- const nextPatternElement = patternElements[i].element;
263
- const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(nextPatternElement);
508
+ const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(patternElements[i]);
264
509
  const nextIsVariadic = nextCaptureMarker?.variadicOptions !== undefined;
265
510
  if (!nextIsVariadic) {
266
511
  nonVariadicRemainingPatterns++;
267
512
  }
513
+ // A pattern is deterministic if it's not a capture at all (i.e., a literal/fixed structure)
514
+ // Variadic captures and non-variadic captures are both non-deterministic
515
+ if (nextCaptureMarker) {
516
+ allRemainingPatternsAreDeterministic = false;
517
+ }
268
518
  }
269
519
  const remainingTargetElements = targetElements.length - targetIdx;
270
520
  const maxPossible = Math.min(remainingTargetElements - nonVariadicRemainingPatterns, max);
@@ -274,17 +524,21 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
274
524
  let pivotDetected = false;
275
525
  let pivotAt = -1;
276
526
 
277
- if (patternIdx + 1 < patternElements.length && min <= maxPossible) {
278
- const nextPattern = patternElements[patternIdx + 1].element;
527
+ // Skip pivot detection if we're using deterministic optimization
528
+ // (when all remaining patterns are literals, there's only ONE valid consumption amount)
529
+ const useDeterministicOptimization = allRemainingPatternsAreDeterministic && maxPossible >= min && maxPossible <= max;
530
+
531
+ if (!useDeterministicOptimization && patternIdx + 1 < patternElements.length && min <= maxPossible) {
532
+ const nextPattern = patternElements[patternIdx + 1];
279
533
 
280
534
  // Scan through possible consumption amounts starting from min
281
535
  for (let tryConsume = min; tryConsume <= maxPossible; tryConsume++) {
282
536
  // Check if element after our consumption would match next pattern
283
537
  if (targetIdx + tryConsume < targetElements.length) {
284
- const candidateElement = targetElements[targetIdx + tryConsume].element;
538
+ const candidateElement = targetElements[targetIdx + tryConsume];
285
539
 
286
540
  // Skip J.Empty for arguments
287
- if (filterEmpty && candidateElement.kind === J.Kind.Empty) {
541
+ if (filterEmpty && candidateElement.element.kind === J.Kind.Empty) {
288
542
  continue;
289
543
  }
290
544
 
@@ -292,7 +546,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
292
546
  const savedMatch = this.match;
293
547
  const savedState = this.matcher.saveState();
294
548
 
295
- await this.visit(nextPattern, candidateElement);
549
+ await this.visitRightPadded(nextPattern, candidateElement as any);
296
550
  const matchesNext = this.match;
297
551
 
298
552
  this.match = savedMatch;
@@ -308,10 +562,15 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
308
562
  }
309
563
  }
310
564
 
311
- // Try different consumption amounts
312
- // If pivot detected, try that first; otherwise use greedy approach (max to min)
565
+ // Determine consumption order
313
566
  const consumptionOrder: number[] = [];
314
- if (pivotDetected && pivotAt >= 0) {
567
+
568
+ // OPTIMIZATION: If all remaining patterns are deterministic (literals, not captures),
569
+ // there's only ONE mathematically valid consumption amount. Skip backtracking entirely.
570
+ // Example: foo(${args}, 999) matching foo(1,2,42) -> args MUST be [1,2], only try consume=2
571
+ if (useDeterministicOptimization) {
572
+ consumptionOrder.push(maxPossible);
573
+ } else if (pivotDetected && pivotAt >= 0) {
315
574
  // Try pivot first, then others as fallback
316
575
  consumptionOrder.push(pivotAt);
317
576
  for (let c = maxPossible; c >= min; c--) {
@@ -328,30 +587,33 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
328
587
 
329
588
  for (const consume of consumptionOrder) {
330
589
  // Capture elements for this consumption amount
331
- const capturedWrappers: J.RightPadded<J>[] = [];
332
- for (let i = 0; i < consume; i++) {
333
- const wrapped = targetElements[targetIdx + i];
334
- const element = wrapped.element;
335
- // For arguments, filter out J.Empty as it represents an empty argument list
336
- // For statements, include all elements
337
- if (!filterEmpty || element.kind !== J.Kind.Empty) {
338
- capturedWrappers.push(wrapped);
339
- }
340
- }
341
-
342
- // Extract just the elements for the constraint check
590
+ // For empty argument lists, there will be a single J.Empty element that we need to filter out
591
+ const rawWrappers = targetElements.slice(targetIdx, targetIdx + consume);
592
+ const capturedWrappers = filterEmpty
593
+ ? rawWrappers.filter(w => w.element.kind !== J.Kind.Empty)
594
+ : rawWrappers;
343
595
  const capturedElements: J[] = capturedWrappers.map(w => w.element);
344
596
 
345
- // Re-check min/max constraints against actual captured elements (after filtering if applicable)
597
+ // Check min/max constraints against filtered elements
346
598
  if (capturedElements.length < min || capturedElements.length > max) {
347
- continue; // Try next consumption amount
599
+ continue;
600
+ }
601
+
602
+ // Evaluate constraint for variadic capture
603
+ // For variadic captures, constraint receives the entire array of captured elements
604
+ // The targetCursor points to the parent container (always defined in container matching)
605
+ if (captureMarker.constraint) {
606
+ const cursor = this.targetCursor || new Cursor(targetElements[0]);
607
+ if (!captureMarker.constraint(capturedElements as any, cursor)) {
608
+ continue; // Try next consumption amount
609
+ }
348
610
  }
349
611
 
350
612
  // Save current state for backtracking
351
613
  const savedState = this.matcher.saveState();
352
614
 
353
615
  // Handle the variadic capture
354
- const success = this.matcher.handleVariadicCapture(patternElement, capturedElements, capturedWrappers);
616
+ const success = this.matcher.handleVariadicCapture(captureMarker, capturedElements, capturedWrappers);
355
617
  if (!success) {
356
618
  // Restore state and try next amount
357
619
  this.matcher.restoreState(savedState);
@@ -390,41 +652,732 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
390
652
  return false;
391
653
  }
392
654
 
393
- // Save current state for backtracking (both match state and capture bindings)
394
- const savedMatch = this.match;
395
- const savedState = this.matcher.saveState();
655
+ if (!await this.visitSequenceElement(patternWrapper, targetWrapper, targetIdx)) {
656
+ return false;
657
+ }
396
658
 
397
- // Push wrapper onto cursor so captures can access it
398
- const savedCursor = this.cursor;
399
- this.cursor = new Cursor(targetWrapper, this.cursor);
659
+ // Continue matching the rest
660
+ return await this.matchSequenceOptimized(
661
+ patternElements,
662
+ targetElements,
663
+ patternIdx + 1,
664
+ targetIdx + 1,
665
+ filterEmpty
666
+ );
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Visit a single element in a sequence during non-variadic matching.
672
+ * Extracted to allow debug subclass to add path tracking.
673
+ *
674
+ * @param patternWrapper The pattern element
675
+ * @param targetWrapper The target element
676
+ * @param targetIdx The index in the target sequence
677
+ * @returns true if matching succeeded, false otherwise
678
+ */
679
+ protected async visitSequenceElement(
680
+ patternWrapper: J.RightPadded<J>,
681
+ targetWrapper: J.RightPadded<J>,
682
+ targetIdx: number
683
+ ): Promise<boolean> {
684
+ // Save current state for backtracking (both match state and capture bindings)
685
+ const savedMatch = this.match;
686
+ const savedState = this.matcher.saveState();
687
+
688
+ await this.visitRightPadded(patternWrapper, targetWrapper as any);
689
+
690
+ if (!this.match) {
691
+ // Restore state on match failure
692
+ this.match = savedMatch;
693
+ this.matcher.restoreState(savedState);
694
+ return false;
695
+ }
696
+
697
+ return true;
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Debug-instrumented version of PatternMatchingComparator.
703
+ * Overrides methods to add path tracking, logging, and explanation capture.
704
+ * Zero cost when not instantiated - production code uses the base class.
705
+ */
706
+ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
707
+ private get debug(): DebugCallbacks {
708
+ return this.matcher.debug!;
709
+ }
710
+
711
+ /**
712
+ * Extracts the last segment of a kind string (after the last dot).
713
+ * For example: "org.openrewrite.java.tree.J.MethodInvocation" -> "MethodInvocation"
714
+ */
715
+ private formatKind(kind: string): string {
716
+ return kind.substring(kind.lastIndexOf('.') + 1);
717
+ }
718
+
719
+ /**
720
+ * Formats a value for display in error messages.
721
+ */
722
+ private formatValue(value: any): string {
723
+ if (value === null) return 'null';
724
+ if (value === undefined) return 'undefined';
725
+ if (typeof value === 'string') return `"${value}"`;
726
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
727
+
728
+ // For objects with a kind property (LST nodes)
729
+ if (value && typeof value === 'object' && value.kind) {
730
+ const kind = this.formatKind(value.kind);
731
+
732
+ // Show key identifying properties for common node types
733
+ if (value.simpleName) return `${kind}("${value.simpleName}")`;
734
+ if (value.value !== undefined) return `${kind}(${this.formatValue(value.value)})`;
735
+
736
+ return kind;
737
+ }
738
+
739
+ return String(value);
740
+ }
741
+
742
+ /**
743
+ * Override abort to capture explanation when debug is enabled.
744
+ * Only sets explanation on the first abort call (when this.match is still true).
745
+ * This preserves the most specific explanation closest to the actual mismatch.
746
+ */
747
+ protected override abort<T>(t: T, reason?: string, propertyName?: string, expected?: any, actual?: any): T {
748
+ // If already aborted, don't overwrite the explanation
749
+ // The first abort is typically the most specific
750
+ if (!this.match) {
751
+ return t;
752
+ }
753
+
754
+ // If we have context about the mismatch, capture it
755
+ if (reason && this.debug && (expected !== undefined || actual !== undefined)) {
756
+ const expectedStr = this.formatValue(expected);
757
+ const actualStr = this.formatValue(actual);
758
+
759
+ this.debug.setExplanation(
760
+ reason as any,
761
+ expectedStr,
762
+ actualStr,
763
+ 'Property values do not match'
764
+ );
765
+ }
766
+
767
+ // Set `this.match = false`
768
+ return super.abort(t, reason, propertyName, expected, actual);
769
+ }
770
+
771
+ /**
772
+ * Override helper methods to extract detailed context from cursors.
773
+ */
774
+
775
+ protected override kindMismatch() {
776
+ const pattern = this.cursor?.value as any;
777
+ const target = this.targetCursor?.value as any;
778
+ // Pass the full kind strings - formatValue() will detect and format them
779
+ return this.abort(pattern, 'kind-mismatch', 'kind', this.formatKind(pattern?.kind), this.formatKind(target?.kind));
780
+ }
781
+
782
+ protected override structuralMismatch(propertyName: string) {
783
+ const pattern = this.cursor?.value as any;
784
+ const target = this.targetCursor?.value as any;
785
+ const expectedValue = pattern?.[propertyName];
786
+ const actualValue = target?.[propertyName];
787
+ return this.abort(pattern, 'structural-mismatch', propertyName, expectedValue, actualValue);
788
+ }
789
+
790
+ protected override arrayLengthMismatch(propertyName: string) {
791
+ const pattern = this.cursor?.value as any;
792
+ const target = this.targetCursor?.value as any;
793
+ const expectedArray = pattern?.[propertyName];
794
+ const actualArray = target?.[propertyName];
795
+ const expectedLen = Array.isArray(expectedArray) ? expectedArray.length : 'not an array';
796
+ const actualLen = Array.isArray(actualArray) ? actualArray.length : 'not an array';
797
+ return this.abort(pattern, 'array-length-mismatch', propertyName, expectedLen, actualLen);
798
+ }
799
+
800
+ protected override valueMismatch(propertyName?: string, expected?: any, actual?: any) {
801
+ const pattern = this.cursor?.value as any;
802
+ const target = this.targetCursor?.value as any;
803
+
804
+ // Track number of paths pushed for cleanup
805
+ let pathsPushed = 0;
806
+
807
+ // Handle path tracking only if propertyName is provided
808
+ if (propertyName) {
809
+ // Split dotted property paths (e.g., "name.simpleName" → ["name", "simpleName"])
810
+ const pathParts = propertyName.split('.');
811
+ pathsPushed = pathParts.length;
812
+
813
+ // Add each property to path with kind information for nested objects
814
+ const kindStr = this.formatKind(pattern?.kind);
815
+ this.debug.pushPath(`${kindStr}#${pathParts[0]}`);
816
+
817
+ // For nested properties, try to get the kind of intermediate objects
818
+ let currentObj = pattern?.[pathParts[0]];
819
+ for (let i = 1; i < pathParts.length; i++) {
820
+ if (currentObj && typeof currentObj === 'object' && currentObj.kind) {
821
+ // Include the kind of the nested object
822
+ const nestedKind = this.formatKind(currentObj.kind);
823
+ this.debug.pushPath(`${nestedKind}#${pathParts[i]}`);
824
+ } else {
825
+ // Fallback to just the property name if no kind available
826
+ this.debug.pushPath(pathParts[i]);
827
+ }
828
+ currentObj = currentObj?.[pathParts[i]];
829
+ }
830
+ }
831
+
832
+ try {
833
+ // If expected/actual provided, use them directly
834
+ if (expected !== undefined || actual !== undefined) {
835
+ return this.abort(pattern, 'value-mismatch', propertyName, expected, actual);
836
+ }
837
+
838
+ // Otherwise, try to extract from cursors (fallback for older code)
839
+ if (propertyName) {
840
+ // Navigate dotted property paths
841
+ const getNestedValue = (obj: any, path: string) => {
842
+ return path.split('.').reduce((current, prop) => current?.[prop], obj);
843
+ };
844
+
845
+ const expectedValue = getNestedValue(pattern, propertyName);
846
+ const actualValue = getNestedValue(target, propertyName);
847
+ return this.abort(pattern, 'value-mismatch', propertyName, expectedValue, actualValue);
848
+ } else {
849
+ // No property name - compare whole objects
850
+ return this.abort(pattern, 'value-mismatch', propertyName, pattern, target);
851
+ }
852
+ } finally {
853
+ // Pop all the path components we pushed
854
+ for (let i = 0; i < pathsPushed; i++) {
855
+ this.debug.popPath();
856
+ }
857
+ }
858
+ }
859
+
860
+ override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
861
+ const captureMarker = PlaceholderUtils.getCaptureMarker(j)!;
862
+ if (captureMarker) {
863
+ const savedTargetCursor = this.targetCursor;
864
+ const cursorAtCapturedNode = this.targetCursor !== undefined
865
+ ? new Cursor(p, this.targetCursor)
866
+ : new Cursor(p);
867
+ this.targetCursor = cursorAtCapturedNode;
400
868
  try {
401
- await this.visit(patternElement, targetElement);
869
+ if (captureMarker.constraint && !captureMarker.variadicOptions) {
870
+ this.debug.log('debug', 'constraint', `Evaluating constraint for capture: ${captureMarker.captureName}`);
871
+ const constraintResult = captureMarker.constraint(p, cursorAtCapturedNode);
872
+ if (!constraintResult) {
873
+ this.debug.log('info', 'constraint', `Constraint failed for capture: ${captureMarker.captureName}`);
874
+ this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(p as any).kind}`, `Constraint evaluation returned false`);
875
+ return this.abort(j) as R;
876
+ }
877
+ this.debug.log('debug', 'constraint', `Constraint passed for capture: ${captureMarker.captureName}`);
878
+ }
879
+
880
+ const success = this.matcher.handleCapture(captureMarker, p, undefined);
881
+ if (!success) {
882
+ return this.abort(j) as R;
883
+ }
884
+ return j as R;
402
885
  } finally {
403
- this.cursor = savedCursor;
886
+ this.targetCursor = savedTargetCursor;
887
+ }
888
+ }
889
+
890
+ return await super.visit(j, p, parent);
891
+ }
892
+
893
+ protected override async visitElement<T extends J>(j: T, other: T): Promise<T> {
894
+ if (!this.match) {
895
+ return j;
896
+ }
897
+
898
+ const kindStr = this.formatKind(j.kind);
899
+ if (j.kind !== other.kind) {
900
+ return this.abort(j, 'kind-mismatch', 'kind', kindStr, this.formatKind(other.kind));
901
+ }
902
+
903
+ for (const key of Object.keys(j)) {
904
+ if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers' || key === 'prefix') {
905
+ continue;
906
+ }
907
+
908
+ const jValue = (j as any)[key];
909
+ const otherValue = (other as any)[key];
910
+
911
+ if (Array.isArray(jValue)) {
912
+ if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) {
913
+ this.debug.pushPath(`${kindStr}#${key}`);
914
+ const result = this.abort(j, 'array-length-mismatch', key, jValue.length,
915
+ Array.isArray(otherValue) ? otherValue.length : otherValue);
916
+ this.debug.popPath();
917
+ return result;
918
+ }
919
+
920
+ for (let i = 0; i < jValue.length; i++) {
921
+ this.debug.pushPath(`${kindStr}#${key}`);
922
+ this.debug.pushPath(i.toString());
923
+ try {
924
+ await this.visitProperty(jValue[i], otherValue[i]);
925
+ if (!this.match) {
926
+ return j;
927
+ }
928
+ } finally {
929
+ this.debug.popPath();
930
+ this.debug.popPath();
931
+ }
932
+ }
933
+ } else {
934
+ this.debug.pushPath(`${kindStr}#${key}`);
935
+ try {
936
+ await this.visitProperty(jValue, otherValue);
937
+ if (!this.match) {
938
+ return j;
939
+ }
940
+ } finally {
941
+ this.debug.popPath();
942
+ }
943
+ }
944
+ }
945
+
946
+ return j;
947
+ }
948
+
949
+ override async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: J): Promise<J.RightPadded<T>> {
950
+ if (!this.match) {
951
+ return right;
952
+ }
953
+
954
+ const captureMarker = PlaceholderUtils.getCaptureMarker(right);
955
+ if (captureMarker) {
956
+ const isRightPadded = (p as any).kind === J.Kind.RightPadded;
957
+ const targetWrapper = isRightPadded ? (p as unknown) as J.RightPadded<T> : undefined;
958
+ const targetElement = isRightPadded ? targetWrapper!.element : p;
959
+
960
+ const savedTargetCursor = this.targetCursor;
961
+ const cursorAtCapturedNode = this.targetCursor !== undefined
962
+ ? (targetWrapper ? new Cursor(targetWrapper, this.targetCursor) : new Cursor(targetElement, this.targetCursor))
963
+ : (targetWrapper ? new Cursor(targetWrapper) : new Cursor(targetElement));
964
+ this.targetCursor = cursorAtCapturedNode;
965
+ try {
966
+ if (captureMarker.constraint && !captureMarker.variadicOptions) {
967
+ this.debug.log('debug', 'constraint', `Evaluating constraint for wrapped capture: ${captureMarker.captureName}`);
968
+ const constraintResult = captureMarker.constraint(targetElement as J, cursorAtCapturedNode);
969
+ if (!constraintResult) {
970
+ this.debug.log('info', 'constraint', `Constraint failed for wrapped capture: ${captureMarker.captureName}`);
971
+ this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(targetElement as any).kind}`, `Constraint evaluation returned false`);
972
+ return this.abort(right);
973
+ }
974
+ this.debug.log('debug', 'constraint', `Constraint passed for wrapped capture: ${captureMarker.captureName}`);
975
+ }
976
+
977
+ const success = this.matcher.handleCapture(captureMarker, targetElement as J, targetWrapper as J.RightPadded<J> | undefined);
978
+ if (!success) {
979
+ return this.abort(right);
980
+ }
981
+ return right;
982
+ } finally {
983
+ this.targetCursor = savedTargetCursor;
984
+ }
985
+ }
986
+
987
+ return await super.visitRightPadded(right, p);
988
+ }
989
+
990
+ override async visitContainer<T extends J>(container: J.Container<T>, p: J): Promise<J.Container<T>> {
991
+ if (!this.match) {
992
+ return container;
993
+ }
994
+
995
+ const isContainer = (p as any).kind === J.Kind.Container;
996
+ if (!isContainer) {
997
+ return this.abort(container);
998
+ }
999
+ const otherContainer = p as unknown as J.Container<T>;
1000
+
1001
+ const hasVariadicCapture = container.elements.some(elem =>
1002
+ PlaceholderUtils.isVariadicCapture(elem)
1003
+ );
1004
+
1005
+ const savedCursor = this.cursor;
1006
+ const savedTargetCursor = this.targetCursor;
1007
+ this.cursor = new Cursor(container, this.cursor);
1008
+ this.targetCursor = new Cursor(otherContainer, this.targetCursor);
1009
+ try {
1010
+ if (hasVariadicCapture) {
1011
+ if (!await this.matchSequence(container.elements as J.RightPadded<J>[], otherContainer.elements as J.RightPadded<J>[], true)) {
1012
+ return this.arrayLengthMismatch('elements');
1013
+ }
1014
+ } else {
1015
+ // Non-variadic path - track indices
1016
+ if (container.elements.length !== otherContainer.elements.length) {
1017
+ return this.arrayLengthMismatch('elements');
1018
+ }
1019
+
1020
+ for (let i = 0; i < container.elements.length; i++) {
1021
+ this.debug.pushPath(i.toString());
1022
+ try {
1023
+ if (!await this.visitContainerElement(container.elements[i], otherContainer.elements[i], i)) {
1024
+ return container;
1025
+ }
1026
+ } finally {
1027
+ this.debug.popPath();
1028
+ }
1029
+ }
1030
+ }
1031
+ } finally {
1032
+ this.cursor = savedCursor;
1033
+ this.targetCursor = savedTargetCursor;
1034
+ }
1035
+
1036
+ return container;
1037
+ }
1038
+
1039
+ /**
1040
+ * Override visitContainerProperty to add path tracking with property context.
1041
+ */
1042
+ protected override async visitContainerProperty<T extends J>(
1043
+ propertyName: string,
1044
+ container: J.Container<T>,
1045
+ otherContainer: J.Container<T>
1046
+ ): Promise<J.Container<T>> {
1047
+ // Get parent from cursor
1048
+ const parent = this.cursor.value as J;
1049
+
1050
+ // Push path for the property
1051
+ const kindStr = this.formatKind((parent as any).kind);
1052
+ this.debug.pushPath(`${kindStr}#${propertyName}`);
1053
+
1054
+ try {
1055
+ await this.visitContainer(container, otherContainer as any);
1056
+ return container;
1057
+ } finally {
1058
+ this.debug.popPath();
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * Override visitRightPaddedProperty to add path tracking with property context.
1064
+ */
1065
+ protected override async visitRightPaddedProperty<T extends J | boolean>(
1066
+ propertyName: string,
1067
+ rightPadded: J.RightPadded<T>,
1068
+ otherRightPadded: J.RightPadded<T>
1069
+ ): Promise<J.RightPadded<T>> {
1070
+ // Get parent from cursor
1071
+ const parent = this.cursor.value as J;
1072
+
1073
+ // Push path for the property
1074
+ const kindStr = this.formatKind((parent as any).kind);
1075
+ this.debug.pushPath(`${kindStr}#${propertyName}`);
1076
+
1077
+ try {
1078
+ return await this.visitRightPadded(rightPadded, otherRightPadded as any);
1079
+ } finally {
1080
+ this.debug.popPath();
1081
+ }
1082
+ }
1083
+
1084
+ /**
1085
+ * Override visitLeftPaddedProperty to add path tracking with property context.
1086
+ */
1087
+ protected override async visitLeftPaddedProperty<T extends J | J.Space | number | string | boolean>(
1088
+ propertyName: string,
1089
+ leftPadded: J.LeftPadded<T>,
1090
+ otherLeftPadded: J.LeftPadded<T>
1091
+ ): Promise<J.LeftPadded<T>> {
1092
+ // Get parent from cursor
1093
+ const parent = this.cursor.value as J;
1094
+
1095
+ // Push path for the property
1096
+ const kindStr = this.formatKind((parent as any).kind);
1097
+ this.debug.pushPath(`${kindStr}#${propertyName}`);
1098
+
1099
+ try {
1100
+ return await this.visitLeftPadded(leftPadded, otherLeftPadded as any);
1101
+ } finally {
1102
+ this.debug.popPath();
1103
+ }
1104
+ }
1105
+
1106
+
1107
+ protected override async visitContainerElement<T extends J>(
1108
+ element: J.RightPadded<T>,
1109
+ otherElement: J.RightPadded<T>,
1110
+ index: number
1111
+ ): Promise<boolean> {
1112
+ // Don't push index here - it should be handled by the caller with proper context
1113
+ return await super.visitContainerElement(element, otherElement, index);
1114
+ }
1115
+
1116
+ protected override async visitArrayProperty<T>(
1117
+ parent: J,
1118
+ propertyName: string,
1119
+ array1: T[],
1120
+ array2: T[],
1121
+ visitor: (item1: T, item2: T, index: number) => Promise<void>
1122
+ ): Promise<void> {
1123
+ // Push path for the property
1124
+ const kindStr = this.formatKind((parent as any).kind);
1125
+ this.debug.pushPath(`${kindStr}#${propertyName}`);
1126
+
1127
+ try {
1128
+ // Check length mismatch (will have path context)
1129
+ if (array1.length !== array2.length) {
1130
+ this.arrayLengthMismatch(propertyName);
1131
+ return;
1132
+ }
1133
+
1134
+ // Visit each element with index tracking
1135
+ for (let i = 0; i < array1.length; i++) {
1136
+ this.debug.pushPath(i.toString());
1137
+ try {
1138
+ await visitor(array1[i], array2[i], i);
1139
+ if (!this.match) {
1140
+ return;
1141
+ }
1142
+ } finally {
1143
+ this.debug.popPath();
1144
+ }
1145
+ }
1146
+ } finally {
1147
+ this.debug.popPath();
1148
+ }
1149
+ }
1150
+
1151
+ protected override async matchSequence(
1152
+ patternElements: J.RightPadded<J>[],
1153
+ targetElements: J.RightPadded<J>[],
1154
+ filterEmpty: boolean
1155
+ ): Promise<boolean> {
1156
+ // Push path component for the container
1157
+ // Extract kind from cursors if available
1158
+ const pattern = this.cursor?.value as any;
1159
+ if (pattern && pattern.kind) {
1160
+ const kindStr = this.formatKind(pattern.kind);
1161
+ // Determine property name based on the kind
1162
+ let propertyName = 'elements';
1163
+ if (pattern.kind.includes('MethodInvocation')) {
1164
+ propertyName = 'arguments';
1165
+ } else if (pattern.kind.includes('Block')) {
1166
+ propertyName = 'statements';
404
1167
  }
1168
+ this.debug.pushPath(`${kindStr}#${propertyName}`);
1169
+ }
1170
+
1171
+ try {
1172
+ return await super.matchSequence(patternElements, targetElements, filterEmpty);
1173
+ } finally {
1174
+ if (this.cursor?.value) {
1175
+ this.debug.popPath();
1176
+ }
1177
+ }
1178
+ }
1179
+
1180
+ protected override async visitSequenceElement(
1181
+ patternWrapper: J.RightPadded<J>,
1182
+ targetWrapper: J.RightPadded<J>,
1183
+ targetIdx: number
1184
+ ): Promise<boolean> {
1185
+ this.debug.pushPath(targetIdx.toString());
1186
+ try {
1187
+ // Save current state for backtracking (both match state and capture bindings)
1188
+ const savedMatch = this.match;
1189
+ const savedState = this.matcher.saveState();
1190
+
1191
+ await this.visitRightPadded(patternWrapper, targetWrapper as any);
1192
+
405
1193
  if (!this.match) {
1194
+ // Preserve explanation before restoring state
1195
+ const explanation = this.debug.getExplanation();
406
1196
  // Restore state on match failure
407
1197
  this.match = savedMatch;
408
1198
  this.matcher.restoreState(savedState);
1199
+ // Restore the explanation if one was set during matching
1200
+ if (explanation) {
1201
+ this.debug.restoreExplanation(explanation);
1202
+ }
409
1203
  return false;
410
1204
  }
411
1205
 
412
- // Continue matching the rest
413
- const restMatches = await this.matchSequenceOptimized(
1206
+ return true;
1207
+ } finally {
1208
+ this.debug.popPath();
1209
+ }
1210
+ }
1211
+
1212
+ protected override async matchSequenceOptimized(
1213
+ patternElements: J.RightPadded<J>[],
1214
+ targetElements: J.RightPadded<J>[],
1215
+ patternIdx: number,
1216
+ targetIdx: number,
1217
+ filterEmpty: boolean
1218
+ ): Promise<boolean> {
1219
+ if (patternIdx >= patternElements.length) {
1220
+ return targetIdx >= targetElements.length;
1221
+ }
1222
+
1223
+ const patternWrapper = patternElements[patternIdx];
1224
+ const captureMarker = PlaceholderUtils.getCaptureMarker(patternWrapper);
1225
+ const isVariadic = captureMarker?.variadicOptions !== undefined;
1226
+
1227
+ if (isVariadic) {
1228
+ const variadicOptions = captureMarker!.variadicOptions;
1229
+ const min = variadicOptions?.min ?? 0;
1230
+ const max = variadicOptions?.max ?? Infinity;
1231
+
1232
+ let nonVariadicRemainingPatterns = 0;
1233
+ let allRemainingPatternsAreDeterministic = true;
1234
+ for (let i = patternIdx + 1; i < patternElements.length; i++) {
1235
+ const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(patternElements[i]);
1236
+ const nextIsVariadic = nextCaptureMarker?.variadicOptions !== undefined;
1237
+ if (!nextIsVariadic) {
1238
+ nonVariadicRemainingPatterns++;
1239
+ }
1240
+ if (nextCaptureMarker) {
1241
+ allRemainingPatternsAreDeterministic = false;
1242
+ }
1243
+ }
1244
+ const remainingTargetElements = targetElements.length - targetIdx;
1245
+ const maxPossible = Math.min(remainingTargetElements - nonVariadicRemainingPatterns, max);
1246
+
1247
+ let pivotDetected = false;
1248
+ let pivotAt = -1;
1249
+
1250
+ // Skip pivot detection if we're using deterministic optimization
1251
+ // (when all remaining patterns are literals, there's only ONE valid consumption amount)
1252
+ const useDeterministicOptimization = allRemainingPatternsAreDeterministic && maxPossible >= min && maxPossible <= max;
1253
+
1254
+ if (!useDeterministicOptimization && patternIdx + 1 < patternElements.length && min <= maxPossible) {
1255
+ const nextPattern = patternElements[patternIdx + 1];
1256
+
1257
+ for (let tryConsume = min; tryConsume <= maxPossible; tryConsume++) {
1258
+ if (targetIdx + tryConsume < targetElements.length) {
1259
+ const candidateElement = targetElements[targetIdx + tryConsume];
1260
+
1261
+ if (filterEmpty && candidateElement.element.kind === J.Kind.Empty) {
1262
+ continue;
1263
+ }
1264
+
1265
+ const savedMatch = this.match;
1266
+ const savedState = this.matcher.saveState();
1267
+
1268
+ await this.visitRightPadded(nextPattern, candidateElement as any);
1269
+ const matchesNext = this.match;
1270
+
1271
+ this.match = savedMatch;
1272
+ this.matcher.restoreState(savedState);
1273
+
1274
+ if (matchesNext) {
1275
+ pivotDetected = true;
1276
+ pivotAt = tryConsume;
1277
+ break;
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+
1283
+ const consumptionOrder: number[] = [];
1284
+ // OPTIMIZATION: If all remaining patterns are deterministic (literals, not captures),
1285
+ // there's only ONE mathematically valid consumption amount. Skip backtracking entirely.
1286
+ // Example: foo(${args}, 999) matching foo(1,2,42) -> args MUST be [1,2], only try consume=2
1287
+ if (useDeterministicOptimization) {
1288
+ consumptionOrder.push(maxPossible);
1289
+ } else if (pivotDetected && pivotAt >= 0) {
1290
+ consumptionOrder.push(pivotAt);
1291
+ for (let c = maxPossible; c >= min; c--) {
1292
+ if (c !== pivotAt) {
1293
+ consumptionOrder.push(c);
1294
+ }
1295
+ }
1296
+ } else {
1297
+ for (let c = maxPossible; c >= min; c--) {
1298
+ consumptionOrder.push(c);
1299
+ }
1300
+ }
1301
+
1302
+ for (const consume of consumptionOrder) {
1303
+ // Capture elements for this consumption amount
1304
+ // For empty argument lists, there will be a single J.Empty element that we need to filter out
1305
+ const rawWrappers = targetElements.slice(targetIdx, targetIdx + consume);
1306
+ const capturedWrappers = filterEmpty
1307
+ ? rawWrappers.filter(w => w.element.kind !== J.Kind.Empty)
1308
+ : rawWrappers;
1309
+ const capturedElements: J[] = capturedWrappers.map(w => w.element);
1310
+
1311
+ // Check min/max constraints against filtered elements
1312
+ if (capturedElements.length < min || capturedElements.length > max) {
1313
+ continue;
1314
+ }
1315
+
1316
+ if (captureMarker.constraint) {
1317
+ this.debug.log('debug', 'constraint', `Evaluating variadic constraint for capture: ${captureMarker.captureName} (${capturedElements.length} elements)`);
1318
+ const cursor = this.targetCursor || new Cursor(targetElements[0]);
1319
+ const constraintResult = captureMarker.constraint(capturedElements as any, cursor);
1320
+ if (!constraintResult) {
1321
+ this.debug.log('info', 'constraint', `Variadic constraint failed for capture: ${captureMarker.captureName}`);
1322
+ continue;
1323
+ }
1324
+ this.debug.log('debug', 'constraint', `Variadic constraint passed for capture: ${captureMarker.captureName}`);
1325
+ }
1326
+
1327
+ const savedState = this.matcher.saveState();
1328
+
1329
+ const success = this.matcher.handleVariadicCapture(captureMarker, capturedElements, capturedWrappers);
1330
+ if (!success) {
1331
+ this.matcher.restoreState(savedState);
1332
+ continue;
1333
+ }
1334
+
1335
+ const restMatches = await this.matchSequenceOptimized(
1336
+ patternElements,
1337
+ targetElements,
1338
+ patternIdx + 1,
1339
+ targetIdx + consume,
1340
+ filterEmpty
1341
+ );
1342
+
1343
+ if (restMatches) {
1344
+ return true;
1345
+ }
1346
+
1347
+ // Preserve explanation from this failed attempt before restoring state
1348
+ // This is especially important when using deterministic optimization (only one attempt)
1349
+ const currentExplanation = this.debug.getExplanation();
1350
+ this.matcher.restoreState(savedState);
1351
+ // Restore the explanation if one was set during this attempt
1352
+ if (currentExplanation) {
1353
+ this.debug.restoreExplanation(currentExplanation);
1354
+ }
1355
+ }
1356
+
1357
+ return false;
1358
+ } else {
1359
+ if (targetIdx >= targetElements.length) {
1360
+ return false;
1361
+ }
1362
+
1363
+ const targetWrapper = targetElements[targetIdx];
1364
+ const targetElement = targetWrapper.element;
1365
+
1366
+ if (filterEmpty && targetElement.kind === J.Kind.Empty) {
1367
+ return false;
1368
+ }
1369
+
1370
+ if (!await this.visitSequenceElement(patternWrapper, targetWrapper, targetIdx)) {
1371
+ return false;
1372
+ }
1373
+
1374
+ return await this.matchSequenceOptimized(
414
1375
  patternElements,
415
1376
  targetElements,
416
1377
  patternIdx + 1,
417
1378
  targetIdx + 1,
418
1379
  filterEmpty
419
1380
  );
420
-
421
- if (!restMatches) {
422
- // Restore full state on backtracking failure
423
- this.match = savedMatch;
424
- this.matcher.restoreState(savedState);
425
- }
426
-
427
- return restMatches;
428
1381
  }
429
1382
  }
430
1383
  }