@openrewrite/rewrite 8.67.0-20251105-160319 → 8.67.0-20251105-204403
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 +8 -1
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js +17 -5
- package/dist/java/tree.js.map +1 -1
- package/dist/javascript/comparator.d.ts +13 -3
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +41 -11
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/templating/capture.d.ts +4 -3
- package/dist/javascript/templating/capture.d.ts.map +1 -1
- package/dist/javascript/templating/capture.js +3 -3
- package/dist/javascript/templating/capture.js.map +1 -1
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +69 -10
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts +6 -2
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +32 -35
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +21 -4
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +82 -2
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +3 -1
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +2 -1
- package/dist/javascript/templating/utils.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/java/tree.ts +8 -3
- package/src/javascript/comparator.ts +47 -12
- package/src/javascript/templating/capture.ts +7 -6
- package/src/javascript/templating/comparator.ts +55 -10
- package/src/javascript/templating/pattern.ts +30 -34
- package/src/javascript/templating/rewrite.ts +28 -4
- package/src/javascript/templating/types.ts +86 -4
- package/src/javascript/templating/utils.ts +3 -1
|
@@ -29,15 +29,27 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
29
29
|
*/
|
|
30
30
|
protected match: boolean = true;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Cursor tracking the current position in the target tree.
|
|
34
|
+
* Maintained in parallel with the pattern tree cursor (this.cursor).
|
|
35
|
+
*/
|
|
36
|
+
protected targetCursor?: Cursor;
|
|
37
|
+
|
|
32
38
|
/**
|
|
33
39
|
* Compares two AST trees.
|
|
34
|
-
*
|
|
35
|
-
* @param tree1 The first tree to compare
|
|
36
|
-
* @param tree2 The second tree to compare
|
|
40
|
+
*
|
|
41
|
+
* @param tree1 The first tree to compare (pattern tree)
|
|
42
|
+
* @param tree2 The second tree to compare (target tree)
|
|
43
|
+
* @param parentCursor1 Optional parent cursor for the pattern tree (for navigating to root)
|
|
44
|
+
* @param parentCursor2 Optional parent cursor for the target tree (for navigating to root)
|
|
37
45
|
* @returns true if the trees match, false otherwise
|
|
38
46
|
*/
|
|
39
|
-
async compare(tree1: J, tree2: J): Promise<boolean> {
|
|
47
|
+
async compare(tree1: J, tree2: J, parentCursor1?: Cursor, parentCursor2?: Cursor): Promise<boolean> {
|
|
40
48
|
this.match = true;
|
|
49
|
+
// Initialize targetCursor with parent if provided, otherwise undefined (will be set by visit())
|
|
50
|
+
this.targetCursor = parentCursor2;
|
|
51
|
+
// Initialize this.cursor (pattern cursor) with parent if provided
|
|
52
|
+
this.cursor = parentCursor1 || new Cursor(undefined, undefined);
|
|
41
53
|
await this.visit(tree1, tree2);
|
|
42
54
|
return this.match;
|
|
43
55
|
}
|
|
@@ -180,14 +192,23 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
180
192
|
return this.abort(j) as R;
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
//
|
|
184
|
-
|
|
195
|
+
// Update targetCursor to track the target node in parallel with the pattern cursor
|
|
196
|
+
// (Can be overridden by subclasses if they need cursor access before calling super)
|
|
197
|
+
const savedTargetCursor = this.targetCursor;
|
|
198
|
+
this.targetCursor = new Cursor(p, this.targetCursor);
|
|
199
|
+
try {
|
|
200
|
+
// Continue with normal visitation, passing the other node as context
|
|
201
|
+
return await super.visit(j, p);
|
|
202
|
+
} finally {
|
|
203
|
+
this.targetCursor = savedTargetCursor;
|
|
204
|
+
}
|
|
185
205
|
}
|
|
186
206
|
|
|
187
207
|
/**
|
|
188
208
|
* Override visitRightPadded to compare only the elements, not markers or spacing.
|
|
189
209
|
* The context parameter p contains the corresponding element from the other tree.
|
|
190
210
|
* Pushes the wrapper onto the cursor stack so captures can access it.
|
|
211
|
+
* Also updates targetCursor in parallel.
|
|
191
212
|
*/
|
|
192
213
|
public async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: J): Promise<J.RightPadded<T>> {
|
|
193
214
|
if (!this.match) {
|
|
@@ -196,15 +217,19 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
196
217
|
|
|
197
218
|
// Extract the other element if it's also a RightPadded
|
|
198
219
|
const isRightPadded = (p as any).kind === J.Kind.RightPadded;
|
|
199
|
-
const
|
|
220
|
+
const otherWrapper = isRightPadded ? (p as unknown) as J.RightPadded<T> : undefined;
|
|
221
|
+
const otherElement = isRightPadded ? otherWrapper!.element : p;
|
|
200
222
|
|
|
201
|
-
// Push
|
|
223
|
+
// Push wrappers onto both cursors, then compare only the elements, not markers or spacing
|
|
202
224
|
const savedCursor = this.cursor;
|
|
225
|
+
const savedTargetCursor = this.targetCursor;
|
|
203
226
|
this.cursor = new Cursor(right, this.cursor);
|
|
227
|
+
this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor;
|
|
204
228
|
try {
|
|
205
229
|
await this.visitProperty(right.element, otherElement);
|
|
206
230
|
} finally {
|
|
207
231
|
this.cursor = savedCursor;
|
|
232
|
+
this.targetCursor = savedTargetCursor;
|
|
208
233
|
}
|
|
209
234
|
|
|
210
235
|
return right;
|
|
@@ -214,6 +239,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
214
239
|
* Override visitLeftPadded to compare only the elements, not markers or spacing.
|
|
215
240
|
* The context parameter p contains the corresponding element from the other tree.
|
|
216
241
|
* Pushes the wrapper onto the cursor stack so captures can access it.
|
|
242
|
+
* Also updates targetCursor in parallel.
|
|
217
243
|
*/
|
|
218
244
|
public async visitLeftPadded<T extends J | J.Space | number | string | boolean>(left: J.LeftPadded<T>, p: J): Promise<J.LeftPadded<T>> {
|
|
219
245
|
if (!this.match) {
|
|
@@ -222,15 +248,19 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
222
248
|
|
|
223
249
|
// Extract the other element if it's also a LeftPadded
|
|
224
250
|
const isLeftPadded = (p as any).kind === J.Kind.LeftPadded;
|
|
225
|
-
const
|
|
251
|
+
const otherWrapper = isLeftPadded ? (p as unknown) as J.LeftPadded<T> : undefined;
|
|
252
|
+
const otherElement = isLeftPadded ? otherWrapper!.element : p;
|
|
226
253
|
|
|
227
|
-
// Push
|
|
254
|
+
// Push wrappers onto both cursors, then compare only the elements, not markers or spacing
|
|
228
255
|
const savedCursor = this.cursor;
|
|
256
|
+
const savedTargetCursor = this.targetCursor;
|
|
229
257
|
this.cursor = new Cursor(left, this.cursor);
|
|
258
|
+
this.targetCursor = otherWrapper ? new Cursor(otherWrapper, this.targetCursor) : this.targetCursor;
|
|
230
259
|
try {
|
|
231
260
|
await this.visitProperty(left.element, otherElement);
|
|
232
261
|
} finally {
|
|
233
262
|
this.cursor = savedCursor;
|
|
263
|
+
this.targetCursor = savedTargetCursor;
|
|
234
264
|
}
|
|
235
265
|
|
|
236
266
|
return left;
|
|
@@ -240,6 +270,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
240
270
|
* Override visitContainer to compare only the elements, not markers or spacing.
|
|
241
271
|
* The context parameter p contains the corresponding element from the other tree.
|
|
242
272
|
* Pushes the wrapper onto the cursor stack so captures can access it.
|
|
273
|
+
* Also updates targetCursor in parallel.
|
|
243
274
|
*/
|
|
244
275
|
public async visitContainer<T extends J>(container: J.Container<T>, p: J): Promise<J.Container<T>> {
|
|
245
276
|
if (!this.match) {
|
|
@@ -248,16 +279,19 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
248
279
|
|
|
249
280
|
// Extract the other elements if it's also a Container
|
|
250
281
|
const isContainer = (p as any).kind === J.Kind.Container;
|
|
251
|
-
const
|
|
282
|
+
const otherContainer = isContainer ? (p as unknown) as J.Container<T> : undefined;
|
|
283
|
+
const otherElements: J.RightPadded<T>[] = isContainer ? otherContainer!.elements : (p as any);
|
|
252
284
|
|
|
253
285
|
// Compare elements array length
|
|
254
286
|
if (container.elements.length !== otherElements.length) {
|
|
255
287
|
return this.abort(container);
|
|
256
288
|
}
|
|
257
289
|
|
|
258
|
-
// Push
|
|
290
|
+
// Push wrappers onto both cursors, then compare each element
|
|
259
291
|
const savedCursor = this.cursor;
|
|
292
|
+
const savedTargetCursor = this.targetCursor;
|
|
260
293
|
this.cursor = new Cursor(container, this.cursor);
|
|
294
|
+
this.targetCursor = otherContainer ? new Cursor(otherContainer, this.targetCursor) : this.targetCursor;
|
|
261
295
|
try {
|
|
262
296
|
for (let i = 0; i < container.elements.length; i++) {
|
|
263
297
|
await this.visitProperty(container.elements[i], otherElements[i]);
|
|
@@ -267,6 +301,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
|
|
|
267
301
|
}
|
|
268
302
|
} finally {
|
|
269
303
|
this.cursor = savedCursor;
|
|
304
|
+
this.targetCursor = savedTargetCursor;
|
|
270
305
|
}
|
|
271
306
|
|
|
272
307
|
return container;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import {Cursor} from '../..';
|
|
16
17
|
import {J, Type} from '../../java';
|
|
17
18
|
import {Any, Capture, CaptureOptions, TemplateParam, VariadicOptions} from './types';
|
|
18
19
|
|
|
@@ -29,8 +30,8 @@ import {Any, Capture, CaptureOptions, TemplateParam, VariadicOptions} from './ty
|
|
|
29
30
|
* )
|
|
30
31
|
* });
|
|
31
32
|
*/
|
|
32
|
-
export function and<T>(...constraints: ((node: T) => boolean)[]): (node: T) => boolean {
|
|
33
|
-
return (node: T) => constraints.every(c => c(node));
|
|
33
|
+
export function and<T>(...constraints: ((node: T, cursor?: Cursor) => boolean)[]): (node: T, cursor?: Cursor) => boolean {
|
|
34
|
+
return (node: T, cursor?: Cursor) => constraints.every(c => c(node, cursor));
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -45,8 +46,8 @@ export function and<T>(...constraints: ((node: T) => boolean)[]): (node: T) => b
|
|
|
45
46
|
* )
|
|
46
47
|
* });
|
|
47
48
|
*/
|
|
48
|
-
export function or<T>(...constraints: ((node: T) => boolean)[]): (node: T) => boolean {
|
|
49
|
-
return (node: T) => constraints.some(c => c(node));
|
|
49
|
+
export function or<T>(...constraints: ((node: T, cursor?: Cursor) => boolean)[]): (node: T, cursor?: Cursor) => boolean {
|
|
50
|
+
return (node: T, cursor?: Cursor) => constraints.some(c => c(node, cursor));
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
@@ -58,8 +59,8 @@ export function or<T>(...constraints: ((node: T) => boolean)[]): (node: T) => bo
|
|
|
58
59
|
* constraint: not((node) => typeof node.value === 'string')
|
|
59
60
|
* });
|
|
60
61
|
*/
|
|
61
|
-
export function not<T>(constraint: (node: T) => boolean): (node: T) => boolean {
|
|
62
|
-
return (node: T) => !constraint(node);
|
|
62
|
+
export function not<T>(constraint: (node: T, cursor?: Cursor) => boolean): (node: T, cursor?: Cursor) => boolean {
|
|
63
|
+
return (node: T, cursor?: Cursor) => !constraint(node, cursor);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// Symbol to access the internal capture name without triggering Proxy
|
|
@@ -41,18 +41,38 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
41
41
|
override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
|
|
42
42
|
// Check if the pattern node is a capture - this handles unwrapped captures
|
|
43
43
|
// (Wrapped captures in J.RightPadded are handled by visitRightPadded override)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// Note: targetCursor will be pushed by parent's visit() method after this check
|
|
45
|
+
const captureMarker = PlaceholderUtils.getCaptureMarker(j)!;
|
|
46
|
+
if (captureMarker) {
|
|
47
|
+
|
|
48
|
+
// Push targetCursor to position it at the captured node for constraint evaluation
|
|
49
|
+
// Only create cursor if targetCursor was initialized (meaning user provided one)
|
|
50
|
+
const savedTargetCursor = this.targetCursor;
|
|
51
|
+
const cursorAtCapturedNode = this.targetCursor !== undefined
|
|
52
|
+
? new Cursor(p, this.targetCursor)
|
|
53
|
+
: undefined;
|
|
54
|
+
this.targetCursor = cursorAtCapturedNode;
|
|
55
|
+
try {
|
|
56
|
+
// Evaluate constraint with cursor at the captured node
|
|
57
|
+
if (captureMarker.constraint && !captureMarker.constraint(p, cursorAtCapturedNode)) {
|
|
58
|
+
return this.abort(j) as R;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const success = this.matcher.handleCapture(captureMarker, p, undefined);
|
|
62
|
+
if (!success) {
|
|
63
|
+
return this.abort(j) as R;
|
|
64
|
+
}
|
|
65
|
+
return j as R;
|
|
66
|
+
} finally {
|
|
67
|
+
this.targetCursor = savedTargetCursor;
|
|
48
68
|
}
|
|
49
|
-
return j as R;
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
if (!this.match) {
|
|
53
72
|
return j as R;
|
|
54
73
|
}
|
|
55
74
|
|
|
75
|
+
// Continue with parent's visit which will push targetCursor and traverse
|
|
56
76
|
return await super.visit(j, p, parent);
|
|
57
77
|
}
|
|
58
78
|
|
|
@@ -79,12 +99,28 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
79
99
|
const targetWrapper = isRightPadded ? (p as unknown) as J.RightPadded<T> : undefined;
|
|
80
100
|
const targetElement = isRightPadded ? targetWrapper!.element : p;
|
|
81
101
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
// Push targetCursor to position it at the captured element for constraint evaluation
|
|
103
|
+
// Only create cursor if targetCursor was initialized (meaning user provided one)
|
|
104
|
+
const savedTargetCursor = this.targetCursor;
|
|
105
|
+
const cursorAtCapturedNode = this.targetCursor !== undefined
|
|
106
|
+
? (targetWrapper ? new Cursor(targetWrapper, this.targetCursor) : new Cursor(targetElement, this.targetCursor))
|
|
107
|
+
: undefined;
|
|
108
|
+
this.targetCursor = cursorAtCapturedNode;
|
|
109
|
+
try {
|
|
110
|
+
// Evaluate constraint with cursor at the captured node
|
|
111
|
+
if (captureMarker.constraint && !captureMarker.constraint(targetElement as J, cursorAtCapturedNode)) {
|
|
112
|
+
return this.abort(right);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle the capture with the wrapper - use the element for pattern matching
|
|
116
|
+
const success = this.matcher.handleCapture(captureMarker, targetElement as J, targetWrapper as J.RightPadded<J> | undefined);
|
|
117
|
+
if (!success) {
|
|
118
|
+
return this.abort(right);
|
|
119
|
+
}
|
|
120
|
+
return right;
|
|
121
|
+
} finally {
|
|
122
|
+
this.targetCursor = savedTargetCursor;
|
|
86
123
|
}
|
|
87
|
-
return right;
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
// Not a capture wrapper - use parent implementation
|
|
@@ -352,6 +388,15 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
|
|
|
352
388
|
continue; // Try next consumption amount
|
|
353
389
|
}
|
|
354
390
|
|
|
391
|
+
// Evaluate constraint for variadic capture
|
|
392
|
+
// For variadic captures, constraint receives the entire array of captured elements
|
|
393
|
+
// The targetCursor is positioned in the target tree (parent context)
|
|
394
|
+
if (captureMarker.constraint) {
|
|
395
|
+
if (!captureMarker.constraint(capturedElements as any, this.targetCursor)) {
|
|
396
|
+
continue; // Try next consumption amount
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
355
400
|
// Save current state for backtracking
|
|
356
401
|
const savedState = this.matcher.saveState();
|
|
357
402
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import {Cursor} from '../..';
|
|
16
17
|
import {J, Type} from '../../java';
|
|
17
18
|
import {JS} from '../index';
|
|
18
19
|
import {JavaScriptVisitor} from '../visitor';
|
|
@@ -181,10 +182,13 @@ export class Pattern {
|
|
|
181
182
|
* Creates a matcher for this pattern against a specific AST node.
|
|
182
183
|
*
|
|
183
184
|
* @param ast The AST node to match against
|
|
184
|
-
* @
|
|
185
|
+
* @param cursor Optional cursor at the node's position in a larger tree. Used for context-aware
|
|
186
|
+
* capture constraints to navigate to parent nodes. If omitted, a cursor will be
|
|
187
|
+
* created at the ast root, allowing constraints to navigate within the matched subtree.
|
|
188
|
+
* @returns A MatchResult if the pattern matches, undefined otherwise
|
|
185
189
|
*/
|
|
186
|
-
async match(ast: J): Promise<MatchResult | undefined> {
|
|
187
|
-
const matcher = new Matcher(this, ast);
|
|
190
|
+
async match(ast: J, cursor?: Cursor): Promise<MatchResult | undefined> {
|
|
191
|
+
const matcher = new Matcher(this, ast, cursor);
|
|
188
192
|
const success = await matcher.matches();
|
|
189
193
|
if (!success) {
|
|
190
194
|
return undefined;
|
|
@@ -301,13 +305,19 @@ class Matcher {
|
|
|
301
305
|
*
|
|
302
306
|
* @param pattern The pattern to match
|
|
303
307
|
* @param ast The AST node to match against
|
|
308
|
+
* @param cursor Optional cursor at the AST node's position
|
|
304
309
|
*/
|
|
305
310
|
constructor(
|
|
306
311
|
private readonly pattern: Pattern,
|
|
307
|
-
private readonly ast: J
|
|
312
|
+
private readonly ast: J,
|
|
313
|
+
cursor?: Cursor
|
|
308
314
|
) {
|
|
315
|
+
// If no cursor provided, create one at the ast root so constraints can navigate up
|
|
316
|
+
this.cursor = cursor ?? new Cursor(ast, undefined);
|
|
309
317
|
}
|
|
310
318
|
|
|
319
|
+
private readonly cursor: Cursor;
|
|
320
|
+
|
|
311
321
|
/**
|
|
312
322
|
* Checks if the pattern matches the AST node.
|
|
313
323
|
*
|
|
@@ -375,18 +385,11 @@ class Matcher {
|
|
|
375
385
|
* @returns true if the pattern matches the target, false otherwise
|
|
376
386
|
*/
|
|
377
387
|
private async matchNode(pattern: J, target: J): Promise<boolean> {
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// Check if nodes have the same kind
|
|
384
|
-
if (pattern.kind !== target.kind) {
|
|
385
|
-
return false;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Use the pattern matching comparator with configured lenient type matching
|
|
389
|
-
// Default to true for backward compatibility with existing patterns
|
|
388
|
+
// Always delegate to the comparator visitor, which handles:
|
|
389
|
+
// - Capture detection and constraint evaluation
|
|
390
|
+
// - Kind checking
|
|
391
|
+
// - Deep structural comparison
|
|
392
|
+
// This centralizes all matching logic in one place
|
|
390
393
|
const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
|
|
391
394
|
const comparator = new PatternMatchingComparator({
|
|
392
395
|
handleCapture: (capture, t, w) => this.handleCapture(capture, t, w),
|
|
@@ -394,7 +397,9 @@ class Matcher {
|
|
|
394
397
|
saveState: () => this.saveState(),
|
|
395
398
|
restoreState: (state) => this.restoreState(state)
|
|
396
399
|
}, lenientTypeMatching);
|
|
397
|
-
|
|
400
|
+
// Pass cursors to allow constraints to navigate to root
|
|
401
|
+
// Pattern cursor is undefined (pattern is the root), target cursor is provided by user
|
|
402
|
+
return await comparator.compare(pattern, target, undefined, this.cursor);
|
|
398
403
|
}
|
|
399
404
|
|
|
400
405
|
/**
|
|
@@ -431,14 +436,9 @@ class Matcher {
|
|
|
431
436
|
return false;
|
|
432
437
|
}
|
|
433
438
|
|
|
434
|
-
// Find the original capture object to get
|
|
439
|
+
// Find the original capture object to get capturing flag
|
|
440
|
+
// Note: Constraints are now evaluated in PatternMatchingComparator where cursor is correctly positioned
|
|
435
441
|
const captureObj = this.pattern.captures.find(c => c.getName() === captureName);
|
|
436
|
-
const constraint = captureObj?.getConstraint?.();
|
|
437
|
-
|
|
438
|
-
// Apply constraint if present
|
|
439
|
-
if (constraint && !constraint(target as any)) {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
442
|
|
|
443
443
|
// Only store the binding if this is a capturing placeholder
|
|
444
444
|
const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
|
|
@@ -465,14 +465,9 @@ class Matcher {
|
|
|
465
465
|
return false;
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
// Find the original capture object to get
|
|
468
|
+
// Find the original capture object to get capturing flag
|
|
469
|
+
// Note: Constraints are now evaluated in PatternMatchingComparator where cursor is correctly positioned
|
|
469
470
|
const captureObj = this.pattern.captures.find(c => c.getName() === captureName);
|
|
470
|
-
const constraint = captureObj?.getConstraint?.();
|
|
471
|
-
|
|
472
|
-
// Apply constraint if present - for variadic captures, constraint receives the array of elements
|
|
473
|
-
if (constraint && !constraint(targets as any)) {
|
|
474
|
-
return false;
|
|
475
|
-
}
|
|
476
471
|
|
|
477
472
|
// Only store the binding if this is a capturing placeholder
|
|
478
473
|
const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
|
|
@@ -514,12 +509,13 @@ class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
|
|
|
514
509
|
if (ident.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
|
|
515
510
|
const captureInfo = PlaceholderUtils.parseCapture(ident.simpleName);
|
|
516
511
|
if (captureInfo) {
|
|
517
|
-
// Find the original capture object to get variadic options
|
|
512
|
+
// Find the original capture object to get variadic options and constraint
|
|
518
513
|
const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
|
|
519
514
|
const variadicOptions = captureObj?.getVariadicOptions();
|
|
515
|
+
const constraint = captureObj?.getConstraint?.();
|
|
520
516
|
|
|
521
|
-
// Add CaptureMarker to the Identifier
|
|
522
|
-
const marker = new CaptureMarker(captureInfo.name, variadicOptions);
|
|
517
|
+
// Add CaptureMarker to the Identifier with constraint
|
|
518
|
+
const marker = new CaptureMarker(captureInfo.name, variadicOptions, constraint);
|
|
523
519
|
return updateIfChanged(ident, {
|
|
524
520
|
markers: {
|
|
525
521
|
...ident.markers,
|
|
@@ -25,14 +25,33 @@ import {Template} from './template';
|
|
|
25
25
|
class RewriteRuleImpl implements RewriteRule {
|
|
26
26
|
constructor(
|
|
27
27
|
private readonly before: Pattern[],
|
|
28
|
-
private readonly after: Template | ((match: MatchResult) => Template)
|
|
28
|
+
private readonly after: Template | ((match: MatchResult) => Template),
|
|
29
|
+
private readonly where?: (node: J, cursor: Cursor) => boolean | Promise<boolean>,
|
|
30
|
+
private readonly whereNot?: (node: J, cursor: Cursor) => boolean | Promise<boolean>
|
|
29
31
|
) {
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
|
|
33
35
|
for (const pattern of this.before) {
|
|
34
|
-
|
|
36
|
+
// Pass cursor to pattern.match() for context-aware capture constraints
|
|
37
|
+
const match = await pattern.match(node, cursor);
|
|
35
38
|
if (match) {
|
|
39
|
+
// Evaluate context predicates after structural match
|
|
40
|
+
if (this.where) {
|
|
41
|
+
const whereResult = await this.where(node, cursor);
|
|
42
|
+
if (!whereResult) {
|
|
43
|
+
continue; // Pattern matched but context doesn't, try next pattern
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (this.whereNot) {
|
|
48
|
+
const whereNotResult = await this.whereNot(node, cursor);
|
|
49
|
+
if (whereNotResult) {
|
|
50
|
+
continue; // Pattern matched but context is excluded, try next pattern
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Apply transformation
|
|
36
55
|
let result: J | undefined;
|
|
37
56
|
|
|
38
57
|
if (typeof this.after === 'function') {
|
|
@@ -50,7 +69,7 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
|
|
53
|
-
// Return undefined if no patterns match
|
|
72
|
+
// Return undefined if no patterns match or all context checks failed
|
|
54
73
|
return undefined;
|
|
55
74
|
}
|
|
56
75
|
|
|
@@ -140,7 +159,12 @@ export function rewrite(
|
|
|
140
159
|
throw new Error('Builder function must return an object with before and after properties');
|
|
141
160
|
}
|
|
142
161
|
|
|
143
|
-
return new RewriteRuleImpl(
|
|
162
|
+
return new RewriteRuleImpl(
|
|
163
|
+
Array.isArray(config.before) ? config.before : [config.before],
|
|
164
|
+
config.after,
|
|
165
|
+
config.where,
|
|
166
|
+
config.whereNot
|
|
167
|
+
);
|
|
144
168
|
}
|
|
145
169
|
|
|
146
170
|
/**
|
|
@@ -40,11 +40,41 @@ export interface VariadicOptions {
|
|
|
40
40
|
* the capture is variadic:
|
|
41
41
|
* - For regular captures: constraint receives a single node of type T
|
|
42
42
|
* - For variadic captures: constraint receives an array of nodes of type T[]
|
|
43
|
+
*
|
|
44
|
+
* The constraint function can optionally receive a cursor parameter to perform
|
|
45
|
+
* context-aware validation during pattern matching.
|
|
43
46
|
*/
|
|
44
47
|
export interface CaptureOptions<T = any> {
|
|
45
48
|
name?: string;
|
|
46
49
|
variadic?: boolean | VariadicOptions;
|
|
47
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Optional constraint function that validates whether a captured node should be accepted.
|
|
52
|
+
* The function receives:
|
|
53
|
+
* - node: The captured node (or array of nodes for variadic captures)
|
|
54
|
+
* - cursor: Optional cursor providing access to the node's context in the AST
|
|
55
|
+
*
|
|
56
|
+
* @param node The captured node to validate
|
|
57
|
+
* @param cursor Optional cursor at the captured node's position
|
|
58
|
+
* @returns true if the capture should be accepted, false otherwise
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // Simple node validation
|
|
63
|
+
* capture<J.Literal>('size', {
|
|
64
|
+
* constraint: (node) => typeof node.value === 'number' && node.value > 100
|
|
65
|
+
* })
|
|
66
|
+
*
|
|
67
|
+
* // Context-aware validation
|
|
68
|
+
* capture<J.MethodInvocation>('method', {
|
|
69
|
+
* constraint: (node, cursor) => {
|
|
70
|
+
* if (!node.name.simpleName.startsWith('get')) return false;
|
|
71
|
+
* const cls = cursor?.firstEnclosing(isClassDeclaration);
|
|
72
|
+
* return cls?.name.simpleName === 'ApiController';
|
|
73
|
+
* }
|
|
74
|
+
* })
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
constraint?: (node: T, cursor?: Cursor) => boolean;
|
|
48
78
|
/**
|
|
49
79
|
* Type annotation for this capture. When provided, the template engine will generate
|
|
50
80
|
* a preamble declaring the capture identifier with this type annotation, allowing
|
|
@@ -101,8 +131,9 @@ export interface Capture<T = any> {
|
|
|
101
131
|
* Gets the constraint function if this capture has one.
|
|
102
132
|
* For regular captures (T = Expression), constraint receives a single node.
|
|
103
133
|
* For variadic captures (T = Expression[]), constraint receives an array of nodes.
|
|
134
|
+
* The constraint function can optionally receive a cursor for context-aware validation.
|
|
104
135
|
*/
|
|
105
|
-
getConstraint?(): ((node: T) => boolean) | undefined;
|
|
136
|
+
getConstraint?(): ((node: T, cursor?: Cursor) => boolean) | undefined;
|
|
106
137
|
}
|
|
107
138
|
|
|
108
139
|
/**
|
|
@@ -367,6 +398,57 @@ export interface RewriteRule {
|
|
|
367
398
|
* Configuration for a replacement rule.
|
|
368
399
|
*/
|
|
369
400
|
export interface RewriteConfig {
|
|
370
|
-
before: Pattern | Pattern[]
|
|
371
|
-
after: Template | ((match: MatchResult) => Template)
|
|
401
|
+
before: Pattern | Pattern[];
|
|
402
|
+
after: Template | ((match: MatchResult) => Template);
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Optional context predicate that must evaluate to true for the transformation to be applied.
|
|
406
|
+
* Evaluated after the pattern matches structurally but before applying the template.
|
|
407
|
+
* Provides access to both the matched node and the cursor for context inspection.
|
|
408
|
+
*
|
|
409
|
+
* @param node The matched AST node
|
|
410
|
+
* @param cursor The cursor at the matched node, providing access to ancestors and context
|
|
411
|
+
* @returns true if the transformation should be applied, false otherwise
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```typescript
|
|
415
|
+
* rewrite(() => ({
|
|
416
|
+
* before: pattern`await ${_('promise')}`,
|
|
417
|
+
* after: template`await ${_('promise')}.catch(handleError)`,
|
|
418
|
+
* where: (node, cursor) => {
|
|
419
|
+
* // Only apply inside async functions
|
|
420
|
+
* const method = cursor.firstEnclosing((n: any): n is J.MethodDeclaration =>
|
|
421
|
+
* n.kind === J.Kind.MethodDeclaration
|
|
422
|
+
* );
|
|
423
|
+
* return method?.modifiers.some(m => m.type === 'async') || false;
|
|
424
|
+
* }
|
|
425
|
+
* }));
|
|
426
|
+
* ```
|
|
427
|
+
*/
|
|
428
|
+
where?: (node: J, cursor: Cursor) => boolean | Promise<boolean>;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Optional context predicate that must evaluate to false for the transformation to be applied.
|
|
432
|
+
* Evaluated after the pattern matches structurally but before applying the template.
|
|
433
|
+
* Provides access to both the matched node and the cursor for context inspection.
|
|
434
|
+
*
|
|
435
|
+
* @param node The matched AST node
|
|
436
|
+
* @param cursor The cursor at the matched node, providing access to ancestors and context
|
|
437
|
+
* @returns true if the transformation should NOT be applied, false if it should proceed
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```typescript
|
|
441
|
+
* rewrite(() => ({
|
|
442
|
+
* before: pattern`await ${_('promise')}`,
|
|
443
|
+
* after: template`await ${_('promise')}.catch(handleError)`,
|
|
444
|
+
* whereNot: (node, cursor) => {
|
|
445
|
+
* // Don't apply inside try-catch blocks
|
|
446
|
+
* return cursor.firstEnclosing((n: any): n is J.Try =>
|
|
447
|
+
* n.kind === J.Kind.Try
|
|
448
|
+
* ) !== undefined;
|
|
449
|
+
* }
|
|
450
|
+
* }));
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
whereNot?: (node: J, cursor: Cursor) => boolean | Promise<boolean>;
|
|
372
454
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import {Cursor} from '../..';
|
|
16
17
|
import {J} from '../../java';
|
|
17
18
|
import {JS} from '../index';
|
|
18
19
|
import {JavaScriptParser} from '../parser';
|
|
@@ -125,7 +126,8 @@ export class CaptureMarker implements Marker {
|
|
|
125
126
|
|
|
126
127
|
constructor(
|
|
127
128
|
public readonly captureName: string,
|
|
128
|
-
public readonly variadicOptions?: VariadicOptions
|
|
129
|
+
public readonly variadicOptions?: VariadicOptions,
|
|
130
|
+
public readonly constraint?: (node: any, cursor?: Cursor) => boolean
|
|
129
131
|
) {
|
|
130
132
|
}
|
|
131
133
|
}
|