@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.
- package/dist/java/tree.d.ts +10 -1
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js +21 -5
- package/dist/java/tree.js.map +1 -1
- package/dist/java/type-visitor.d.ts +1 -1
- package/dist/java/type-visitor.d.ts.map +1 -1
- package/dist/java/visitor.d.ts +2 -2
- package/dist/java/visitor.d.ts.map +1 -1
- package/dist/java/visitor.js +8 -2
- package/dist/java/visitor.js.map +1 -1
- package/dist/javascript/assertions.d.ts +6 -0
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +14 -6
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/comparator.d.ts +154 -7
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +623 -180
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/format.d.ts +5 -3
- package/dist/javascript/format.d.ts.map +1 -1
- package/dist/javascript/format.js +85 -43
- package/dist/javascript/format.js.map +1 -1
- package/dist/javascript/index.d.ts +1 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +1 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/parser.d.ts +2 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +39 -30
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/templating/capture.d.ts +81 -14
- package/dist/javascript/templating/capture.d.ts.map +1 -1
- package/dist/javascript/templating/capture.js +98 -8
- package/dist/javascript/templating/capture.js.map +1 -1
- package/dist/javascript/templating/comparator.d.ts +125 -15
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +946 -118
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +58 -25
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +527 -94
- package/dist/javascript/templating/engine.js.map +1 -1
- package/dist/javascript/templating/index.d.ts +3 -3
- package/dist/javascript/templating/index.d.ts.map +1 -1
- package/dist/javascript/templating/index.js +3 -1
- package/dist/javascript/templating/index.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts +121 -16
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +528 -257
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.d.ts +30 -5
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +183 -81
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts +56 -11
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +143 -16
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +31 -5
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +89 -15
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +359 -12
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +52 -35
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +107 -109
- package/dist/javascript/templating/utils.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +21 -11
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/json/rpc.js +2 -2
- package/dist/json/rpc.js.map +1 -1
- package/dist/recipe/order-imports.js.map +1 -1
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +10 -6
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/version.txt +1 -1
- package/dist/visitor.d.ts +4 -4
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +8 -3
- package/dist/visitor.js.map +1 -1
- package/package.json +4 -2
- package/src/java/tree.ts +10 -3
- package/src/java/type-visitor.ts +1 -1
- package/src/java/visitor.ts +11 -5
- package/src/javascript/assertions.ts +9 -3
- package/src/javascript/comparator.ts +676 -185
- package/src/javascript/format.ts +72 -34
- package/src/javascript/index.ts +1 -0
- package/src/javascript/parser.ts +51 -31
- package/src/javascript/templating/capture.ts +107 -15
- package/src/javascript/templating/comparator.ts +1087 -134
- package/src/javascript/templating/engine.ts +601 -103
- package/src/javascript/templating/index.ts +9 -2
- package/src/javascript/templating/pattern.ts +655 -281
- package/src/javascript/templating/placeholder-replacement.ts +183 -80
- package/src/javascript/templating/rewrite.ts +152 -18
- package/src/javascript/templating/template.ts +110 -22
- package/src/javascript/templating/types.ts +386 -12
- package/src/javascript/templating/utils.ts +116 -102
- package/src/javascript/type-mapping.ts +20 -11
- package/src/json/rpc.ts +2 -2
- package/src/recipe/order-imports.ts +1 -1
- package/src/test/rewrite-test.ts +12 -7
- 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 {
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
326
|
+
// Visit name
|
|
327
|
+
await this.visit(methodInvocation.name, otherMethodInvocation.name);
|
|
328
|
+
if (!this.match) {
|
|
329
|
+
return methodInvocation;
|
|
132
330
|
}
|
|
133
|
-
}
|
|
134
331
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
|
396
|
+
// Check if any statements are variadic captures
|
|
176
397
|
const hasVariadicCapture = compilationUnit.statements.some(stmt => {
|
|
177
|
-
|
|
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
|
|
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
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
const
|
|
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
|
|
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
|
-
|
|
278
|
-
|
|
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]
|
|
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.
|
|
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
|
-
//
|
|
312
|
-
// If pivot detected, try that first; otherwise use greedy approach (max to min)
|
|
565
|
+
// Determine consumption order
|
|
313
566
|
const consumptionOrder: number[] = [];
|
|
314
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
//
|
|
597
|
+
// Check min/max constraints against filtered elements
|
|
346
598
|
if (capturedElements.length < min || capturedElements.length > max) {
|
|
347
|
-
continue;
|
|
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(
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
655
|
+
if (!await this.visitSequenceElement(patternWrapper, targetWrapper, targetIdx)) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
396
658
|
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
413
|
-
|
|
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
|
}
|