@openrewrite/rewrite 8.67.0-20251105-154016 → 8.67.0-20251105-201900

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/java/tree.d.ts +8 -1
  2. package/dist/java/tree.d.ts.map +1 -1
  3. package/dist/java/tree.js +17 -5
  4. package/dist/java/tree.js.map +1 -1
  5. package/dist/javascript/comparator.d.ts +13 -3
  6. package/dist/javascript/comparator.d.ts.map +1 -1
  7. package/dist/javascript/comparator.js +41 -11
  8. package/dist/javascript/comparator.js.map +1 -1
  9. package/dist/javascript/templating/capture.d.ts +4 -3
  10. package/dist/javascript/templating/capture.d.ts.map +1 -1
  11. package/dist/javascript/templating/capture.js +3 -3
  12. package/dist/javascript/templating/capture.js.map +1 -1
  13. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  14. package/dist/javascript/templating/comparator.js +69 -10
  15. package/dist/javascript/templating/comparator.js.map +1 -1
  16. package/dist/javascript/templating/pattern.d.ts +6 -2
  17. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  18. package/dist/javascript/templating/pattern.js +32 -35
  19. package/dist/javascript/templating/pattern.js.map +1 -1
  20. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  21. package/dist/javascript/templating/rewrite.js +21 -4
  22. package/dist/javascript/templating/rewrite.js.map +1 -1
  23. package/dist/javascript/templating/types.d.ts +82 -2
  24. package/dist/javascript/templating/types.d.ts.map +1 -1
  25. package/dist/javascript/templating/utils.d.ts +3 -1
  26. package/dist/javascript/templating/utils.d.ts.map +1 -1
  27. package/dist/javascript/templating/utils.js +2 -1
  28. package/dist/javascript/templating/utils.js.map +1 -1
  29. package/dist/version.txt +1 -1
  30. package/package.json +1 -1
  31. package/src/java/tree.ts +8 -3
  32. package/src/javascript/comparator.ts +47 -12
  33. package/src/javascript/templating/capture.ts +7 -6
  34. package/src/javascript/templating/comparator.ts +55 -10
  35. package/src/javascript/templating/pattern.ts +30 -34
  36. package/src/javascript/templating/rewrite.ts +28 -4
  37. package/src/javascript/templating/types.ts +86 -4
  38. 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
- // Continue with normal visitation, passing the other node as context
184
- return await super.visit(j, p);
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 otherElement = isRightPadded ? ((p as unknown) as J.RightPadded<T>).element : p;
220
+ const otherWrapper = isRightPadded ? (p as unknown) as J.RightPadded<T> : undefined;
221
+ const otherElement = isRightPadded ? otherWrapper!.element : p;
200
222
 
201
- // Push wrapper onto cursor, then compare only the elements, not markers or spacing
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 otherElement = isLeftPadded ? ((p as unknown) as J.LeftPadded<T>).element : p;
251
+ const otherWrapper = isLeftPadded ? (p as unknown) as J.LeftPadded<T> : undefined;
252
+ const otherElement = isLeftPadded ? otherWrapper!.element : p;
226
253
 
227
- // Push wrapper onto cursor, then compare only the elements, not markers or spacing
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 otherElements: J.RightPadded<T>[] = isContainer ? ((p as unknown) as J.Container<T>).elements : (p as any);
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 wrapper onto cursor, then compare each element
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
- if (PlaceholderUtils.isCapture(j as J)) {
45
- const success = this.matcher.handleCapture(PlaceholderUtils.getCaptureMarker(j)!, p, undefined);
46
- if (!success) {
47
- return this.abort(j) as R;
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
- // Handle the capture with the wrapper - use the element for pattern matching
83
- const success = this.matcher.handleCapture(captureMarker, targetElement as J, targetWrapper as J.RightPadded<J> | undefined);
84
- if (!success) {
85
- return this.abort(right);
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
- * @returns A Matcher object
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
- // Check if pattern is a capture placeholder
379
- if (PlaceholderUtils.isCapture(pattern)) {
380
- return this.handleCapture(PlaceholderUtils.getCaptureMarker(pattern)!, target);
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
- return await comparator.compare(pattern, target);
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 constraint and capturing flag
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 constraint and capturing flag
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
- const match = await pattern.match(node);
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(Array.isArray(config.before) ? config.before : [config.before], config.after);
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
- constraint?: (node: T) => boolean;
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
  }