@openrewrite/rewrite 8.67.0-20251119-160338 → 8.67.0-20251120-075051

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/javascript/add-import.d.ts +21 -10
  2. package/dist/javascript/add-import.d.ts.map +1 -1
  3. package/dist/javascript/add-import.js +55 -21
  4. package/dist/javascript/add-import.js.map +1 -1
  5. package/dist/javascript/remove-import.d.ts +18 -14
  6. package/dist/javascript/remove-import.d.ts.map +1 -1
  7. package/dist/javascript/remove-import.js +37 -47
  8. package/dist/javascript/remove-import.js.map +1 -1
  9. package/dist/javascript/templating/capture.d.ts +3 -4
  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 +7 -1
  14. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  15. package/dist/javascript/templating/comparator.js +45 -10
  16. package/dist/javascript/templating/comparator.js.map +1 -1
  17. package/dist/javascript/templating/pattern.d.ts +10 -11
  18. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  19. package/dist/javascript/templating/pattern.js +18 -36
  20. package/dist/javascript/templating/pattern.js.map +1 -1
  21. package/dist/javascript/templating/rewrite.js +2 -2
  22. package/dist/javascript/templating/rewrite.js.map +1 -1
  23. package/dist/javascript/templating/template.d.ts +27 -13
  24. package/dist/javascript/templating/template.d.ts.map +1 -1
  25. package/dist/javascript/templating/template.js +31 -14
  26. package/dist/javascript/templating/template.js.map +1 -1
  27. package/dist/javascript/templating/types.d.ts +111 -15
  28. package/dist/javascript/templating/types.d.ts.map +1 -1
  29. package/dist/version.txt +1 -1
  30. package/package.json +1 -1
  31. package/src/javascript/add-import.ts +70 -27
  32. package/src/javascript/remove-import.ts +37 -46
  33. package/src/javascript/templating/capture.ts +7 -7
  34. package/src/javascript/templating/comparator.ts +50 -11
  35. package/src/javascript/templating/pattern.ts +32 -24
  36. package/src/javascript/templating/rewrite.ts +2 -2
  37. package/src/javascript/templating/template.ts +36 -18
  38. package/src/javascript/templating/types.ts +127 -16
@@ -6,16 +6,16 @@ import {ElementRemovalFormatter} from "../java/formatting-utils";
6
6
 
7
7
  /**
8
8
  * @param visitor The visitor to add the import removal to
9
- * @param target Either the module name (e.g., 'fs') to remove specific members from,
10
- * or the name of the import to remove entirely
9
+ * @param module The module name (e.g., 'fs', 'react') to remove imports from
11
10
  * @param member Optionally, the specific member to remove from the import.
12
- * If not specified, removes the import matching `target`.
11
+ * If not specified, removes all unused imports from the module.
13
12
  * Special values:
14
- * - 'default': Removes the default import from the target module if unused,
13
+ * - 'default': Removes the default import from the module if unused,
15
14
  * regardless of its local name (e.g., `import React from 'react'`)
15
+ * - '*': Removes the namespace import if unused (e.g., `import * as fs from 'fs'`)
16
16
  *
17
17
  * @example
18
- * // Remove a named import if unused
18
+ * // Remove a specific named import if unused
19
19
  * maybeRemoveImport(visitor, 'fs', 'readFile');
20
20
  *
21
21
  * @example
@@ -23,16 +23,20 @@ import {ElementRemovalFormatter} from "../java/formatting-utils";
23
23
  * maybeRemoveImport(visitor, 'react', 'default');
24
24
  *
25
25
  * @example
26
- * // Remove an import by name if unused (no module specified)
27
- * maybeRemoveImport(visitor, 'fs');
26
+ * // Remove all unused imports from 'react' module
27
+ * maybeRemoveImport(visitor, 'react');
28
+ *
29
+ * @example
30
+ * // Remove namespace import if unused
31
+ * maybeRemoveImport(visitor, 'fs', '*');
28
32
  */
29
- export function maybeRemoveImport(visitor: JavaScriptVisitor<any>, target: string, member?: string) {
33
+ export function maybeRemoveImport(visitor: JavaScriptVisitor<any>, module: string, member?: string) {
30
34
  for (const v of visitor.afterVisit || []) {
31
- if (v instanceof RemoveImport && v.target === target && v.member === member) {
35
+ if (v instanceof RemoveImport && v.module === module && v.member === member) {
32
36
  return;
33
37
  }
34
38
  }
35
- visitor.afterVisit.push(new RemoveImport(target, member));
39
+ visitor.afterVisit.push(new RemoveImport(module, member));
36
40
  }
37
41
 
38
42
  // Type alias for RightPadded elements to simplify type signatures
@@ -45,15 +49,15 @@ type RightPaddedElement<T extends J> = {
45
49
 
46
50
  export class RemoveImport<P> extends JavaScriptVisitor<P> {
47
51
  /**
48
- * @param target Either the module name (e.g., 'fs') to remove specific members from,
49
- * or the name of the import to remove entirely
52
+ * @param module The module name (e.g., 'fs', 'react') to remove imports from
50
53
  * @param member Optionally, the specific member to remove from the import.
51
- * If not specified, removes the import matching `target`.
54
+ * If not specified, removes all unused imports from the module.
52
55
  * Special values:
53
- * - 'default': Removes the default import from the target module if unused,
56
+ * - 'default': Removes the default import from the module if unused,
54
57
  * regardless of its local name
58
+ * - '*': Removes the namespace import if unused
55
59
  */
56
- constructor(readonly target: string,
60
+ constructor(readonly module: string,
57
61
  readonly member?: string) {
58
62
  super();
59
63
  }
@@ -245,7 +249,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
245
249
  const name = identifier.simpleName;
246
250
 
247
251
  // Check if we should remove this default import
248
- let shouldRemove = false;
252
+ let shouldRemove: boolean;
249
253
  if (this.member === 'default') {
250
254
  // Special case: member 'default' means remove any default import from the target module if unused
251
255
  shouldRemove = !usedIdentifiers.has(name) && !usedTypes.has(name);
@@ -425,22 +429,17 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
425
429
  * Check if the module name matches the target module
426
430
  */
427
431
  private matchesTargetModule(moduleName: string): boolean {
428
- return this.member === undefined ? moduleName === this.target : moduleName === this.target;
432
+ return moduleName === this.module;
429
433
  }
430
434
 
431
435
  /**
432
436
  * Check if an identifier should be removed based on usage
433
437
  */
434
438
  private shouldRemoveIdentifier(name: string, usedIdentifiers: Set<string>, usedTypes: Set<string>): boolean {
435
- // If member is specified, we're removing a specific member
436
- if (this.member !== undefined) {
437
- // Only remove if the identifier is not used
438
- return !usedIdentifiers.has(name) && !usedTypes.has(name);
439
- } else {
440
- // We're removing based on the target name
441
- // Check if the name matches and is not used
442
- return this.target === name && !usedIdentifiers.has(name) && !usedTypes.has(name);
443
- }
439
+ // For CommonJS and import-equals-require, we're removing the entire import
440
+ // if the identifier is not used (member is typically undefined for these cases,
441
+ // or we're checking if a specific binding is used)
442
+ return !usedIdentifiers.has(name) && !usedTypes.has(name);
444
443
  }
445
444
 
446
445
  private async processNamedImports(
@@ -681,40 +680,32 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
681
680
  usedIdentifiers: Set<string>,
682
681
  usedTypes: Set<string>
683
682
  ): boolean {
684
- // If member is specified, we're removing a specific member from a module
683
+ // If member is specified, we're removing a specific member from the module
685
684
  if (this.member !== undefined) {
686
685
  // Only remove if this is the specific member we're looking for
687
686
  if (this.member !== name) {
688
687
  return false;
689
688
  }
690
- } else {
691
- // If no member specified, we're removing based on the import name itself
692
- if (this.target !== name) {
693
- return false;
694
- }
695
689
  }
690
+ // If no member specified, we're removing all unused imports from the module
691
+ // So we check if this particular import is unused
696
692
 
697
693
  // Check if it's used
698
694
  return !(usedIdentifiers.has(name) || usedTypes.has(name));
699
695
  }
700
696
 
701
697
  private isTargetModule(jsImport: JS.Import): boolean {
702
- // If member is specified, we're looking for imports from a specific module
703
- if (this.member !== undefined) {
704
- const moduleSpecifier = jsImport.moduleSpecifier?.element;
705
- if (!moduleSpecifier || moduleSpecifier.kind !== J.Kind.Literal) {
706
- return false;
707
- }
708
-
709
- const literal = moduleSpecifier as J.Literal;
710
- const moduleName = literal.value?.toString().replace(/['"`]/g, '');
711
-
712
- // Match the module name
713
- return moduleName === this.target;
698
+ // Always check if the import is from the specified module
699
+ const moduleSpecifier = jsImport.moduleSpecifier?.element;
700
+ if (!moduleSpecifier || moduleSpecifier.kind !== J.Kind.Literal) {
701
+ return false;
714
702
  }
715
703
 
716
- // If no member specified, we process all imports to check their names
717
- return true;
704
+ const literal = moduleSpecifier as J.Literal;
705
+ const moduleName = literal.value?.toString().replace(/['"`]/g, '');
706
+
707
+ // Match the module name
708
+ return moduleName === this.module;
718
709
  }
719
710
 
720
711
  /**
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import {Cursor} from '../..';
17
17
  import {J, Type} from '../../java';
18
- import {Any, Capture, CaptureOptions, ConstraintFunction, TemplateParam, VariadicOptions} from './types';
18
+ import {Any, Capture, CaptureConstraintContext, CaptureOptions, ConstraintFunction, TemplateParam, VariadicOptions} from './types';
19
19
 
20
20
  /**
21
21
  * Combines multiple constraints with AND logic.
@@ -30,8 +30,8 @@ import {Any, Capture, CaptureOptions, ConstraintFunction, TemplateParam, Variadi
30
30
  * )
31
31
  * });
32
32
  */
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));
33
+ export function and<T>(...constraints: ConstraintFunction<T>[]): ConstraintFunction<T> {
34
+ return (node: T, context: CaptureConstraintContext) => constraints.every(c => c(node, context));
35
35
  }
36
36
 
37
37
  /**
@@ -46,8 +46,8 @@ export function and<T>(...constraints: ((node: T, cursor?: Cursor) => boolean)[]
46
46
  * )
47
47
  * });
48
48
  */
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));
49
+ export function or<T>(...constraints: ConstraintFunction<T>[]): ConstraintFunction<T> {
50
+ return (node: T, context: CaptureConstraintContext) => constraints.some(c => c(node, context));
51
51
  }
52
52
 
53
53
  /**
@@ -59,8 +59,8 @@ export function or<T>(...constraints: ((node: T, cursor?: Cursor) => boolean)[])
59
59
  * constraint: not((node) => typeof node.value === 'string')
60
60
  * });
61
61
  */
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);
62
+ export function not<T>(constraint: ConstraintFunction<T>): ConstraintFunction<T> {
63
+ return (node: T, context: CaptureConstraintContext) => !constraint(node, context);
64
64
  }
65
65
 
66
66
  // Symbol to access the internal capture name without triggering Proxy
@@ -18,7 +18,8 @@ import {J} from '../../java';
18
18
  import {JS} from '../index';
19
19
  import {JavaScriptSemanticComparatorVisitor} from '../comparator';
20
20
  import {CaptureMarker, CaptureStorageValue, PlaceholderUtils} from './utils';
21
- import {DebugLogEntry, MatchExplanation} from './types';
21
+ import {Capture, CaptureConstraintContext, CaptureMap, DebugLogEntry, MatchExplanation} from './types';
22
+ import {CAPTURE_NAME_SYMBOL} from './capture';
22
23
 
23
24
  /**
24
25
  * Debug callbacks for pattern matching.
@@ -62,6 +63,28 @@ export interface MatcherCallbacks {
62
63
  debug?: DebugCallbacks;
63
64
  }
64
65
 
66
+ /**
67
+ * Implementation of CaptureMap that wraps the capture storage.
68
+ * Provides read-only access to previously matched captures.
69
+ */
70
+ class CaptureMapImpl implements CaptureMap {
71
+ constructor(private readonly storage: Map<string, CaptureStorageValue>) {}
72
+
73
+ get<T>(capture: Capture<T>): T | undefined;
74
+ get(capture: string): any;
75
+ get(capture: Capture | string): any {
76
+ // Use symbol to get internal name without triggering Proxy
77
+ const name = typeof capture === 'string' ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
78
+ return this.storage.get(name);
79
+ }
80
+
81
+ has(capture: Capture | string): boolean {
82
+ // Use symbol to get internal name without triggering Proxy
83
+ const name = typeof capture === 'string' ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
84
+ return this.storage.has(name);
85
+ }
86
+ }
87
+
65
88
  /**
66
89
  * A comparator for pattern matching that is lenient about optional properties.
67
90
  * Allows patterns without type annotations to match actual code with type annotations.
@@ -76,6 +99,19 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
76
99
  super(lenientTypeMatching);
77
100
  }
78
101
 
102
+ /**
103
+ * Builds the constraint context with the cursor and current captures.
104
+ * @param cursor The cursor to include in the context
105
+ * @returns The constraint context for evaluating capture constraints
106
+ */
107
+ protected buildConstraintContext(cursor: Cursor): CaptureConstraintContext {
108
+ const state = this.matcher.saveState();
109
+ return {
110
+ cursor,
111
+ captures: new CaptureMapImpl(state.storage)
112
+ };
113
+ }
114
+
79
115
  override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
80
116
  // Check if the pattern node is a capture - this handles unwrapped captures
81
117
  // (Wrapped captures in J.RightPadded are handled by visitRightPadded override)
@@ -91,12 +127,15 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
91
127
  : new Cursor(p);
92
128
  this.targetCursor = cursorAtCapturedNode;
93
129
  try {
94
- // Evaluate constraint with cursor at the captured node (always defined)
130
+ // Evaluate constraint with context (cursor + previous captures)
95
131
  // 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;
132
+ if (captureMarker.constraint && !captureMarker.variadicOptions) {
133
+ const context = this.buildConstraintContext(cursorAtCapturedNode);
134
+ if (!captureMarker.constraint(p, context)) {
135
+ const captureName = captureMarker.captureName || 'unnamed';
136
+ const targetKind = (p as any).kind || 'unknown';
137
+ return this.constraintFailed(captureName, targetKind) as R;
138
+ }
100
139
  }
101
140
 
102
141
  const success = this.matcher.handleCapture(captureMarker, p, undefined);
@@ -164,7 +203,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
164
203
  try {
165
204
  // Evaluate constraint with cursor at the captured node (always defined)
166
205
  // 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)) {
206
+ if (captureMarker.constraint && !captureMarker.variadicOptions && !captureMarker.constraint(targetElement as J, this.buildConstraintContext(cursorAtCapturedNode))) {
168
207
  const captureName = captureMarker.captureName || 'unnamed';
169
208
  const targetKind = (targetElement as any).kind || 'unknown';
170
209
  return this.constraintFailed(captureName, targetKind);
@@ -604,7 +643,7 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
604
643
  // The targetCursor points to the parent container (always defined in container matching)
605
644
  if (captureMarker.constraint) {
606
645
  const cursor = this.targetCursor || new Cursor(targetElements[0]);
607
- if (!captureMarker.constraint(capturedElements as any, cursor)) {
646
+ if (!captureMarker.constraint(capturedElements as any, this.buildConstraintContext(cursor))) {
608
647
  continue; // Try next consumption amount
609
648
  }
610
649
  }
@@ -868,7 +907,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
868
907
  try {
869
908
  if (captureMarker.constraint && !captureMarker.variadicOptions) {
870
909
  this.debug.log('debug', 'constraint', `Evaluating constraint for capture: ${captureMarker.captureName}`);
871
- const constraintResult = captureMarker.constraint(p, cursorAtCapturedNode);
910
+ const constraintResult = captureMarker.constraint(p, this.buildConstraintContext(cursorAtCapturedNode));
872
911
  if (!constraintResult) {
873
912
  this.debug.log('info', 'constraint', `Constraint failed for capture: ${captureMarker.captureName}`);
874
913
  this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(p as any).kind}`, `Constraint evaluation returned false`);
@@ -965,7 +1004,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
965
1004
  try {
966
1005
  if (captureMarker.constraint && !captureMarker.variadicOptions) {
967
1006
  this.debug.log('debug', 'constraint', `Evaluating constraint for wrapped capture: ${captureMarker.captureName}`);
968
- const constraintResult = captureMarker.constraint(targetElement as J, cursorAtCapturedNode);
1007
+ const constraintResult = captureMarker.constraint(targetElement as J, this.buildConstraintContext(cursorAtCapturedNode));
969
1008
  if (!constraintResult) {
970
1009
  this.debug.log('info', 'constraint', `Constraint failed for wrapped capture: ${captureMarker.captureName}`);
971
1010
  this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(targetElement as any).kind}`, `Constraint evaluation returned false`);
@@ -1316,7 +1355,7 @@ export class DebugPatternMatchingComparator extends PatternMatchingComparator {
1316
1355
  if (captureMarker.constraint) {
1317
1356
  this.debug.log('debug', 'constraint', `Evaluating variadic constraint for capture: ${captureMarker.captureName} (${capturedElements.length} elements)`);
1318
1357
  const cursor = this.targetCursor || new Cursor(targetElements[0]);
1319
- const constraintResult = captureMarker.constraint(capturedElements as any, cursor);
1358
+ const constraintResult = captureMarker.constraint(capturedElements as any, this.buildConstraintContext(cursor));
1320
1359
  if (!constraintResult) {
1321
1360
  this.debug.log('info', 'constraint', `Variadic constraint failed for capture: ${captureMarker.captureName}`);
1322
1361
  continue;
@@ -15,7 +15,17 @@
15
15
  */
16
16
  import {Cursor} from '../..';
17
17
  import {J} from '../../java';
18
- import {Any, Capture, DebugLogEntry, DebugOptions, MatchAttemptResult, MatchExplanation, MatchOptions, PatternOptions, MatchResult as IMatchResult} from './types';
18
+ import {
19
+ Any,
20
+ Capture,
21
+ DebugLogEntry,
22
+ DebugOptions,
23
+ MatchAttemptResult,
24
+ MatchExplanation,
25
+ MatchOptions,
26
+ MatchResult as IMatchResult,
27
+ PatternOptions
28
+ } from './types';
19
29
  import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CaptureImpl, RAW_CODE_SYMBOL, RawCode} from './capture';
20
30
  import {DebugPatternMatchingComparator, MatcherCallbacks, MatcherState, PatternMatchingComparator} from './comparator';
21
31
  import {CaptureMarker, CaptureStorageValue, generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
@@ -190,7 +200,7 @@ export class Pattern {
190
200
  * })
191
201
  */
192
202
  configure(options: PatternOptions): Pattern {
193
- this._options = {...this._options, ...options};
203
+ this._options = { ...this._options, ...options };
194
204
  // Invalidate cache when configuration changes
195
205
  this._cachedAstPattern = undefined;
196
206
  return this;
@@ -252,23 +262,22 @@ export class Pattern {
252
262
  /**
253
263
  * Creates a matcher for this pattern against a specific AST node.
254
264
  *
255
- * @param ast The AST node to match against
256
- * @param cursor Optional cursor at the node's position in a larger tree. Used for context-aware
257
- * capture constraints to navigate to parent nodes. If omitted, a cursor will be
258
- * created at the ast root, allowing constraints to navigate within the matched subtree.
265
+ * @param tree The AST node to match against
266
+ * @param cursor Cursor at the node's position in a larger tree. Used for context-aware
267
+ * capture constraints to navigate to parent nodes.
259
268
  * @param options Optional match options (e.g., debug flag)
260
269
  * @returns A MatchResult if the pattern matches, undefined otherwise
261
270
  *
262
271
  * @example
263
272
  * ```typescript
264
273
  * // Normal match
265
- * const match = await pattern.match(node);
274
+ * const match = await pattern.match(node, cursor);
266
275
  *
267
276
  * // Debug this specific call
268
277
  * const match = await pattern.match(node, cursor, { debug: true });
269
278
  * ```
270
279
  */
271
- async match(ast: J, cursor?: Cursor, options?: MatchOptions): Promise<MatchResult | undefined> {
280
+ async match(tree: J, cursor: Cursor, options?: MatchOptions): Promise<MatchResult | undefined> {
272
281
  // Three-level precedence: call > pattern > global
273
282
  const debugEnabled =
274
283
  options?.debug !== undefined
@@ -279,8 +288,8 @@ export class Pattern {
279
288
 
280
289
  if (debugEnabled) {
281
290
  // Use matchWithExplanation and log the result
282
- const result = await this.matchWithExplanation(ast, cursor);
283
- await this.logMatchResult(ast, cursor, result);
291
+ const result = await this.matchWithExplanation(tree, cursor);
292
+ await this.logMatchResult(tree, cursor, result);
284
293
 
285
294
  if (result.matched) {
286
295
  // result.result is the MatchResult class instance
@@ -291,7 +300,7 @@ export class Pattern {
291
300
  }
292
301
 
293
302
  // Fast path - no debug
294
- const matcher = new Matcher(this, ast, cursor);
303
+ const matcher = new Matcher(this, tree, cursor);
295
304
  const success = await matcher.matches();
296
305
  if (!success) {
297
306
  return undefined;
@@ -305,10 +314,10 @@ export class Pattern {
305
314
  * Formats and logs the match result to stderr.
306
315
  * @private
307
316
  */
308
- private async logMatchResult(ast: J, cursor: Cursor | undefined, result: MatchAttemptResult): Promise<void> {
317
+ private async logMatchResult(tree: J, cursor: Cursor | undefined, result: MatchAttemptResult): Promise<void> {
309
318
  const patternSource = this.getPatternSource();
310
319
  const patternId = `Pattern #${this.patternId}`;
311
- const nodeKind = (ast as any).kind || 'unknown';
320
+ const nodeKind = (tree as any).kind || 'unknown';
312
321
  // Format kind: extract short name (e.g., "org.openrewrite.java.tree.J$Binary" -> "J$Binary")
313
322
  const shortKind = typeof nodeKind === 'string'
314
323
  ? nodeKind.split('.').pop() || nodeKind
@@ -324,7 +333,7 @@ export class Pattern {
324
333
  let treeStr: string;
325
334
  try {
326
335
  const printer = TreePrinters.printer(JS.Kind.CompilationUnit);
327
- treeStr = await printer.print(ast);
336
+ treeStr = await printer.print(tree);
328
337
  } catch (e) {
329
338
  treeStr = '(tree printing unavailable)';
330
339
  }
@@ -508,15 +517,15 @@ export class Pattern {
508
517
  * - Explanation of failure (if not matched)
509
518
  * - Debug log entries showing the matching process
510
519
  *
511
- * @param ast The AST node to match against
512
- * @param cursor Optional cursor at the node's position in a larger tree
520
+ * @param tree The AST node to match against
521
+ * @param cursor Cursor at the node's position in a larger tree
513
522
  * @param debugOptions Optional debug options (defaults to all logging enabled)
514
523
  * @returns Detailed result with debug information
515
524
  *
516
525
  * @example
517
526
  * const x = capture('x');
518
527
  * const pat = pattern`console.log(${x})`;
519
- * const attempt = await pat.matchWithExplanation(node);
528
+ * const attempt = await pat.matchWithExplanation(node, cursor);
520
529
  * if (attempt.matched) {
521
530
  * console.log('Matched!');
522
531
  * console.log('Captured x:', attempt.result.get('x'));
@@ -526,8 +535,8 @@ export class Pattern {
526
535
  * }
527
536
  */
528
537
  async matchWithExplanation(
529
- ast: J,
530
- cursor?: Cursor,
538
+ tree: J,
539
+ cursor: Cursor,
531
540
  debugOptions?: DebugOptions
532
541
  ): Promise<MatchAttemptResult> {
533
542
  // Default to full debug logging if not specified
@@ -538,7 +547,7 @@ export class Pattern {
538
547
  ...debugOptions
539
548
  };
540
549
 
541
- const matcher = new Matcher(this, ast, cursor, options);
550
+ const matcher = new Matcher(this, tree, cursor, options);
542
551
  const success = await matcher.matches();
543
552
 
544
553
  if (success) {
@@ -671,17 +680,16 @@ class Matcher {
671
680
  *
672
681
  * @param pattern The pattern to match
673
682
  * @param ast The AST node to match against
674
- * @param cursor Optional cursor at the AST node's position
683
+ * @param cursor Cursor at the AST node's position
675
684
  * @param debugOptions Optional debug options for instrumentation
676
685
  */
677
686
  constructor(
678
687
  private readonly pattern: Pattern,
679
688
  private readonly ast: J,
680
- cursor?: Cursor,
689
+ cursor: Cursor,
681
690
  debugOptions?: DebugOptions
682
691
  ) {
683
- // If no cursor provided, create one at the ast root so constraints can navigate up
684
- this.cursor = cursor ?? new Cursor(ast, undefined);
692
+ this.cursor = cursor;
685
693
  this.debugOptions = debugOptions ?? {};
686
694
  }
687
695
 
@@ -57,10 +57,10 @@ class RewriteRuleImpl implements RewriteRule {
57
57
  if (typeof this.after === 'function') {
58
58
  // Call the function to get a template, then apply it
59
59
  const template = this.after(match);
60
- result = await template.apply(cursor, node, match);
60
+ result = await template.apply(node, cursor, { values: match });
61
61
  } else {
62
62
  // Use template.apply() as before
63
- result = await this.after.apply(cursor, node, match);
63
+ result = await this.after.apply(node, cursor, { values: match });
64
64
  }
65
65
 
66
66
  if (result) {
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import {Cursor, Tree} from '../..';
17
17
  import {J} from '../../java';
18
- import {Capture, Parameter, TemplateOptions, TemplateParameter} from './types';
18
+ import {ApplyOptions, Parameter, TemplateOptions, TemplateParameter} from './types';
19
19
  import {MatchResult} from './pattern';
20
20
  import {generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
21
21
  import {CAPTURE_NAME_SYMBOL, RAW_CODE_SYMBOL} from './capture';
@@ -174,6 +174,18 @@ export class Template {
174
174
  private options: TemplateOptions = {};
175
175
  private _cachedTemplate?: J;
176
176
 
177
+ /**
178
+ * Creates a new template.
179
+ *
180
+ * @param templateParts The string parts of the template
181
+ * @param parameters The parameters between the string parts
182
+ */
183
+ constructor(
184
+ private readonly templateParts: TemplateStringsArray,
185
+ private readonly parameters: Parameter[]
186
+ ) {
187
+ }
188
+
177
189
  /**
178
190
  * Creates a new builder for constructing templates programmatically.
179
191
  *
@@ -191,18 +203,6 @@ export class Template {
191
203
  return new TemplateBuilder();
192
204
  }
193
205
 
194
- /**
195
- * Creates a new template.
196
- *
197
- * @param templateParts The string parts of the template
198
- * @param parameters The parameters between the string parts
199
- */
200
- constructor(
201
- private readonly templateParts: TemplateStringsArray,
202
- private readonly parameters: Parameter[]
203
- ) {
204
- }
205
-
206
206
  /**
207
207
  * Configures this template with additional options.
208
208
  *
@@ -217,7 +217,7 @@ export class Template {
217
217
  * })
218
218
  */
219
219
  configure(options: TemplateOptions): Template {
220
- this.options = { ...this.options, ...options };
220
+ this.options = {...this.options, ...options};
221
221
  // Invalidate cache when configuration changes
222
222
  this._cachedTemplate = undefined;
223
223
  return this;
@@ -236,7 +236,7 @@ export class Template {
236
236
  * @returns The cached or newly computed template tree
237
237
  * @internal
238
238
  */
239
- async getTemplateTree(): Promise<JS.CompilationUnit> {
239
+ private async getTemplateTree(): Promise<JS.CompilationUnit> {
240
240
  // Level 1: Instance cache (fastest path)
241
241
  if (this._cachedTemplate) {
242
242
  return this._cachedTemplate as JS.CompilationUnit;
@@ -286,12 +286,30 @@ export class Template {
286
286
  /**
287
287
  * Applies this template and returns the resulting tree.
288
288
  *
289
+ * @param tree Input tree to transform
289
290
  * @param cursor The cursor pointing to the current location in the AST
290
- * @param tree Input tree
291
- * @param values values for parameters in template
291
+ * @param options Optional configuration including values for parameters
292
292
  * @returns A Promise resolving to the generated AST node
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * // Simple application without values
297
+ * const result = await tmpl.apply(node, cursor);
298
+ *
299
+ * // With values from pattern match
300
+ * const match = await pat.match(node, cursor);
301
+ * const result = await tmpl.apply(node, cursor, { values: match });
302
+ *
303
+ * // With explicit values
304
+ * const result = await tmpl.apply(node, cursor, {
305
+ * values: { x: someNode, y: anotherNode }
306
+ * });
307
+ * ```
293
308
  */
294
- async apply(cursor: Cursor, tree: J, values?: Map<Capture | string, J> | Pick<Map<string, J>, 'get'> | Record<string, J>): Promise<J | undefined> {
309
+ async apply(tree: J, cursor: Cursor, options?: ApplyOptions): Promise<J | undefined> {
310
+ // Extract values from options
311
+ const values = options?.values;
312
+
295
313
  // Normalize the values map: convert any Capture keys to string keys
296
314
  let normalizedValues: Pick<Map<string, J>, 'get'> | undefined;
297
315
  let wrappersMap: Map<string, J.RightPadded<J> | J.RightPadded<J>[]> = new Map();