@openrewrite/rewrite 8.63.2 → 8.63.4

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 (59) hide show
  1. package/dist/java/rpc.d.ts +2 -2
  2. package/dist/java/rpc.d.ts.map +1 -1
  3. package/dist/java/rpc.js +10 -4
  4. package/dist/java/rpc.js.map +1 -1
  5. package/dist/java/type.d.ts +1 -1
  6. package/dist/java/type.d.ts.map +1 -1
  7. package/dist/java/type.js +3 -3
  8. package/dist/java/type.js.map +1 -1
  9. package/dist/javascript/assertions.d.ts +1 -1
  10. package/dist/javascript/assertions.d.ts.map +1 -1
  11. package/dist/javascript/assertions.js +35 -65
  12. package/dist/javascript/assertions.js.map +1 -1
  13. package/dist/javascript/comparator.d.ts +2 -2
  14. package/dist/javascript/comparator.d.ts.map +1 -1
  15. package/dist/javascript/comparator.js.map +1 -1
  16. package/dist/javascript/dependency-workspace.d.ts +44 -0
  17. package/dist/javascript/dependency-workspace.d.ts.map +1 -0
  18. package/dist/javascript/dependency-workspace.js +335 -0
  19. package/dist/javascript/dependency-workspace.js.map +1 -0
  20. package/dist/javascript/parser.d.ts.map +1 -1
  21. package/dist/javascript/parser.js +5 -2
  22. package/dist/javascript/parser.js.map +1 -1
  23. package/dist/javascript/preconditions.js +2 -2
  24. package/dist/javascript/preconditions.js.map +1 -1
  25. package/dist/javascript/templating.d.ts +110 -5
  26. package/dist/javascript/templating.d.ts.map +1 -1
  27. package/dist/javascript/templating.js +412 -38
  28. package/dist/javascript/templating.js.map +1 -1
  29. package/dist/javascript/type-mapping.js +2 -2
  30. package/dist/javascript/type-mapping.js.map +1 -1
  31. package/dist/rpc/queue.d.ts +1 -0
  32. package/dist/rpc/queue.d.ts.map +1 -1
  33. package/dist/rpc/queue.js +11 -1
  34. package/dist/rpc/queue.js.map +1 -1
  35. package/dist/rpc/request/install-recipes.d.ts.map +1 -1
  36. package/dist/rpc/request/install-recipes.js +116 -21
  37. package/dist/rpc/request/install-recipes.js.map +1 -1
  38. package/dist/rpc/server.d.ts.map +1 -1
  39. package/dist/rpc/server.js +5 -0
  40. package/dist/rpc/server.js.map +1 -1
  41. package/dist/test/rewrite-test.d.ts +1 -1
  42. package/dist/test/rewrite-test.d.ts.map +1 -1
  43. package/dist/test/rewrite-test.js +27 -5
  44. package/dist/test/rewrite-test.js.map +1 -1
  45. package/dist/version.txt +1 -1
  46. package/package.json +1 -1
  47. package/src/java/rpc.ts +4 -4
  48. package/src/java/type.ts +3 -3
  49. package/src/javascript/assertions.ts +14 -21
  50. package/src/javascript/comparator.ts +2 -2
  51. package/src/javascript/dependency-workspace.ts +317 -0
  52. package/src/javascript/parser.ts +6 -3
  53. package/src/javascript/preconditions.ts +2 -2
  54. package/src/javascript/templating.ts +535 -44
  55. package/src/javascript/type-mapping.ts +2 -2
  56. package/src/rpc/queue.ts +11 -1
  57. package/src/rpc/request/install-recipes.ts +127 -24
  58. package/src/rpc/server.ts +5 -0
  59. package/src/test/rewrite-test.ts +11 -3
@@ -17,9 +17,106 @@ import {JS} from '.';
17
17
  import {JavaScriptParser} from './parser';
18
18
  import {JavaScriptVisitor} from './visitor';
19
19
  import {Cursor, isTree, Tree} from '..';
20
- import {J} from '../java';
20
+ import {J, Type} from '../java';
21
21
  import {produce} from "immer";
22
22
  import {JavaScriptComparatorVisitor} from "./comparator";
23
+ import {DependencyWorkspace} from './dependency-workspace';
24
+ import {Marker} from '../markers';
25
+ import {randomId} from '../uuid';
26
+
27
+ /**
28
+ * Cache for compiled templates and patterns.
29
+ * Stores parsed ASTs to avoid expensive re-parsing and dependency resolution.
30
+ */
31
+ class TemplateCache {
32
+ private cache = new Map<string, JS.CompilationUnit>();
33
+
34
+ /**
35
+ * Generates a cache key from template string, captures, and options.
36
+ */
37
+ private generateKey(
38
+ templateString: string,
39
+ captures: Capture[],
40
+ imports: string[],
41
+ dependencies: Record<string, string>
42
+ ): string {
43
+ // Use the actual template string (with placeholders) as the primary key
44
+ const templateKey = templateString;
45
+
46
+ // Capture names
47
+ const capturesKey = captures.map(c => c.name).join(',');
48
+
49
+ // Imports
50
+ const importsKey = imports.join(';');
51
+
52
+ // Dependencies
53
+ const depsKey = JSON.stringify(dependencies || {});
54
+
55
+ return `${templateKey}::${capturesKey}::${importsKey}::${depsKey}`;
56
+ }
57
+
58
+ /**
59
+ * Gets a cached compilation unit or creates and caches a new one.
60
+ */
61
+ async getOrParse(
62
+ templateString: string,
63
+ captures: Capture[],
64
+ imports: string[],
65
+ dependencies: Record<string, string>
66
+ ): Promise<JS.CompilationUnit> {
67
+ const key = this.generateKey(templateString, captures, imports, dependencies);
68
+
69
+ let cu = this.cache.get(key);
70
+ if (cu) {
71
+ return cu;
72
+ }
73
+
74
+ // Create workspace if dependencies are provided
75
+ // DependencyWorkspace has its own cache, so multiple templates with
76
+ // the same dependencies will automatically share the same workspace
77
+ let workspaceDir: string | undefined;
78
+ if (dependencies && Object.keys(dependencies).length > 0) {
79
+ workspaceDir = await DependencyWorkspace.getOrCreateWorkspace(dependencies);
80
+ }
81
+
82
+ // Prepend imports for type attribution context
83
+ const fullTemplateString = imports.length > 0
84
+ ? imports.join('\n') + '\n' + templateString
85
+ : templateString;
86
+
87
+ // Parse and cache (workspace only needed during parsing)
88
+ const parser = new JavaScriptParser({relativeTo: workspaceDir});
89
+ const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.ts'});
90
+ cu = (await parseGenerator.next()).value as JS.CompilationUnit;
91
+
92
+ this.cache.set(key, cu);
93
+ return cu;
94
+ }
95
+
96
+ /**
97
+ * Clears the cache.
98
+ */
99
+ clear(): void {
100
+ this.cache.clear();
101
+ }
102
+ }
103
+
104
+ // Global cache instance
105
+ const templateCache = new TemplateCache();
106
+
107
+ /**
108
+ * Marker that stores capture metadata on pattern AST nodes.
109
+ * This avoids the need to parse capture names from identifiers during matching.
110
+ */
111
+ class CaptureMarker implements Marker {
112
+ readonly kind = 'org.openrewrite.javascript.CaptureMarker';
113
+ readonly id = randomId();
114
+
115
+ constructor(
116
+ public readonly captureName: string
117
+ ) {
118
+ }
119
+ }
23
120
 
24
121
  /**
25
122
  * Capture specification for pattern matching.
@@ -42,28 +139,76 @@ class CaptureImpl implements Capture {
42
139
  /**
43
140
  * Creates a capture specification for use in template patterns.
44
141
  *
142
+ * @param name Optional name for the capture. If not provided, an auto-generated name is used.
45
143
  * @returns A Capture object
46
144
  *
47
145
  * @example
48
- * // Multiple captures
146
+ * // Named inline captures
147
+ * const pattern = pattern`${capture('left')} + ${capture('right')}`;
148
+ *
149
+ * // Unnamed captures
49
150
  * const {left, right} = {left: capture(), right: capture()};
50
151
  * const pattern = pattern`${left} + ${right}`;
51
152
  *
52
153
  * // Repeated patterns using the same capture
53
- * const expr = capture();
154
+ * const expr = capture('expr');
54
155
  * const redundantOr = pattern`${expr} || ${expr}`;
55
156
  */
56
- export function capture(): Capture {
157
+ export function capture(name?: string): Capture {
158
+ if (name) {
159
+ return new CaptureImpl(name);
160
+ }
57
161
  return new CaptureImpl(`unnamed_${capture.nextUnnamedId++}`);
58
162
  }
59
163
 
60
164
  // Static counter for generating unique IDs for unnamed captures
61
165
  capture.nextUnnamedId = 1;
62
166
 
167
+ /**
168
+ * Concise alias for `capture`. Works well for inline captures in patterns and templates.
169
+ *
170
+ * @param name Optional name for the capture. If not provided, an auto-generated name is used.
171
+ * @returns A Capture object
172
+ *
173
+ * @example
174
+ * // Inline captures with _ alias
175
+ * pattern`isDate(${_('dateArg')})`
176
+ * template`${_('dateArg')} instanceof Date`
177
+ */
178
+ export const _ = capture;
179
+
180
+ /**
181
+ * Configuration options for patterns.
182
+ */
183
+ export interface PatternOptions {
184
+ /**
185
+ * Import statements to provide type attribution context.
186
+ * These are prepended to the pattern when parsing to ensure proper type information.
187
+ */
188
+ imports?: string[];
189
+
190
+ /**
191
+ * NPM dependencies required for import resolution and type attribution.
192
+ * Maps package names to version specifiers (e.g., { 'util': '^1.0.0' }).
193
+ * The template engine will create a package.json with these dependencies.
194
+ */
195
+ dependencies?: Record<string, string>;
196
+ }
197
+
63
198
  /**
64
199
  * Represents a pattern that can be matched against AST nodes.
65
200
  */
66
201
  export class Pattern {
202
+ private _options: PatternOptions = {};
203
+
204
+ /**
205
+ * Gets the configuration options for this pattern.
206
+ * @readonly
207
+ */
208
+ get options(): Readonly<PatternOptions> {
209
+ return this._options;
210
+ }
211
+
67
212
  /**
68
213
  * Creates a new pattern from template parts and captures.
69
214
  *
@@ -76,6 +221,24 @@ export class Pattern {
76
221
  ) {
77
222
  }
78
223
 
224
+ /**
225
+ * Configures this pattern with additional options.
226
+ *
227
+ * @param options Configuration options
228
+ * @returns This pattern for method chaining
229
+ *
230
+ * @example
231
+ * pattern`isDate(${capture('date')})`
232
+ * .configure({
233
+ * imports: ['import { isDate } from "util"'],
234
+ * dependencies: { 'util': '^1.0.0' }
235
+ * })
236
+ */
237
+ configure(options: PatternOptions): Pattern {
238
+ this._options = { ...this._options, ...options };
239
+ return this;
240
+ }
241
+
79
242
  /**
80
243
  * Creates a matcher for this pattern against a specific AST node.
81
244
  *
@@ -101,13 +264,156 @@ export class MatchResult implements Pick<Map<string, J>, "get"> {
101
264
  }
102
265
  }
103
266
 
267
+ /**
268
+ * A comparator visitor that checks semantic equality including type attribution.
269
+ * This ensures that patterns only match code with compatible types, not just
270
+ * structurally similar code.
271
+ */
272
+ class JavaScriptTemplateSemanticallyEqualVisitor extends JavaScriptComparatorVisitor {
273
+ /**
274
+ * Checks if two types are semantically equal.
275
+ * For method types, this checks that the declaring type and method name match.
276
+ */
277
+ private isOfType(target?: Type, source?: Type): boolean {
278
+ if (!target || !source) {
279
+ return target === source;
280
+ }
281
+
282
+ if (target.kind !== source.kind) {
283
+ return false;
284
+ }
285
+
286
+ // For method types, check declaring type
287
+ // Note: We don't check the name field because it might not be fully resolved in patterns
288
+ // The method invocation visitor already checks that simple names match
289
+ if (target.kind === Type.Kind.Method && source.kind === Type.Kind.Method) {
290
+ const targetMethod = target as Type.Method;
291
+ const sourceMethod = source as Type.Method;
292
+
293
+ // Only check that declaring types match
294
+ return this.isOfType(targetMethod.declaringType, sourceMethod.declaringType);
295
+ }
296
+
297
+ // For fully qualified types, check the fully qualified name
298
+ if (Type.isFullyQualified(target) && Type.isFullyQualified(source)) {
299
+ return Type.FullyQualified.getFullyQualifiedName(target) ===
300
+ Type.FullyQualified.getFullyQualifiedName(source);
301
+ }
302
+
303
+ // Default: types are equal if they're the same kind
304
+ return true;
305
+ }
306
+
307
+ /**
308
+ * Override method invocation comparison to include type attribution checking.
309
+ * When types match semantically, we allow matching even if one has a receiver
310
+ * and the other doesn't (e.g., `isDate(x)` vs `util.isDate(x)`).
311
+ */
312
+ override async visitMethodInvocation(method: J.MethodInvocation, other: J): Promise<J | undefined> {
313
+ if (other.kind !== J.Kind.MethodInvocation) {
314
+ return method;
315
+ }
316
+
317
+ const otherMethod = other as J.MethodInvocation;
318
+
319
+ // Check basic structural equality first
320
+ if (method.name.simpleName !== otherMethod.name.simpleName ||
321
+ method.arguments.elements.length !== otherMethod.arguments.elements.length) {
322
+ this.abort();
323
+ return method;
324
+ }
325
+
326
+ // Check type attribution
327
+ // Both must have method types for semantic equality
328
+ if (!method.methodType || !otherMethod.methodType) {
329
+ // If template has type but target doesn't, they don't match
330
+ if (method.methodType || otherMethod.methodType) {
331
+ this.abort();
332
+ return method;
333
+ }
334
+ // If neither has type, fall through to structural comparison
335
+ return super.visitMethodInvocation(method, other);
336
+ }
337
+
338
+ // Both have types - check they match semantically
339
+ const typesMatch = this.isOfType(method.methodType, otherMethod.methodType);
340
+ if (!typesMatch) {
341
+ // Types don't match - abort comparison
342
+ this.abort();
343
+ return method;
344
+ }
345
+
346
+ // Types match! Now we can ignore receiver differences and just compare arguments.
347
+ // This allows pattern `isDate(x)` to match both `isDate(x)` and `util.isDate(x)`
348
+ // when they have the same type attribution.
349
+
350
+ // Compare type parameters
351
+ if ((method.typeParameters === undefined) !== (otherMethod.typeParameters === undefined)) {
352
+ this.abort();
353
+ return method;
354
+ }
355
+
356
+ if (method.typeParameters && otherMethod.typeParameters) {
357
+ if (method.typeParameters.elements.length !== otherMethod.typeParameters.elements.length) {
358
+ this.abort();
359
+ return method;
360
+ }
361
+ for (let i = 0; i < method.typeParameters.elements.length; i++) {
362
+ await this.visit(method.typeParameters.elements[i].element, otherMethod.typeParameters.elements[i].element);
363
+ if (!this.match) return method;
364
+ }
365
+ }
366
+
367
+ // Compare name (already checked simpleName above, but visit for markers/prefix)
368
+ await this.visit(method.name, otherMethod.name);
369
+ if (!this.match) return method;
370
+
371
+ // Compare arguments
372
+ for (let i = 0; i < method.arguments.elements.length; i++) {
373
+ await this.visit(method.arguments.elements[i].element, otherMethod.arguments.elements[i].element);
374
+ if (!this.match) return method;
375
+ }
376
+
377
+ return method;
378
+ }
379
+
380
+ /**
381
+ * Override identifier comparison to include type checking for field access.
382
+ */
383
+ override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
384
+ if (other.kind !== J.Kind.Identifier) {
385
+ return identifier;
386
+ }
387
+
388
+ const otherIdentifier = other as J.Identifier;
389
+
390
+ // Check name matches
391
+ if (identifier.simpleName !== otherIdentifier.simpleName) {
392
+ return identifier;
393
+ }
394
+
395
+ // For identifiers with field types, check type attribution
396
+ if (identifier.fieldType && otherIdentifier.fieldType) {
397
+ if (!this.isOfType(identifier.fieldType, otherIdentifier.fieldType)) {
398
+ this.abort();
399
+ return identifier;
400
+ }
401
+ } else if (identifier.fieldType || otherIdentifier.fieldType) {
402
+ // If only one has a type, they don't match
403
+ this.abort();
404
+ return identifier;
405
+ }
406
+
407
+ return super.visitIdentifier(identifier, other);
408
+ }
409
+ }
410
+
104
411
  /**
105
412
  * Matcher for checking if a pattern matches an AST node and extracting captured nodes.
106
413
  */
107
414
  class Matcher {
108
415
  private readonly bindings = new Map<string, J>();
109
416
  private patternAst?: J;
110
- private templateProcessor?: TemplateProcessor;
111
417
 
112
418
  /**
113
419
  * Creates a new matcher for a pattern against an AST node.
@@ -128,8 +434,13 @@ class Matcher {
128
434
  */
129
435
  async matches(): Promise<boolean> {
130
436
  if (!this.patternAst) {
131
- this.templateProcessor = new TemplateProcessor(this.pattern.templateParts, this.pattern.captures);
132
- this.patternAst = await this.templateProcessor.toAstPattern();
437
+ const templateProcessor = new TemplateProcessor(
438
+ this.pattern.templateParts,
439
+ this.pattern.captures,
440
+ this.pattern.options.imports || [],
441
+ this.pattern.options.dependencies || {}
442
+ );
443
+ this.patternAst = await templateProcessor.toAstPattern();
133
444
  }
134
445
 
135
446
  return this.matchNode(this.patternAst, this.ast);
@@ -163,7 +474,7 @@ class Matcher {
163
474
  }
164
475
 
165
476
  const matcher = this;
166
- return await ((new class extends JavaScriptComparatorVisitor {
477
+ return await ((new class extends JavaScriptTemplateSemanticallyEqualVisitor {
167
478
  protected hasSameKind(j: J, other: J): boolean {
168
479
  return super.hasSameKind(j, other) || j.kind == J.Kind.Identifier && this.matchesParameter(j as J.Identifier, other);
169
480
  }
@@ -173,8 +484,7 @@ class Matcher {
173
484
  }
174
485
 
175
486
  private matchesParameter(identifier: J.Identifier, other: J): boolean {
176
- return PlaceholderUtils.isCapture(identifier) &&
177
- matcher.handleCapture(identifier, other);
487
+ return PlaceholderUtils.isCapture(identifier) && matcher.handleCapture(identifier, other);
178
488
  }
179
489
  }).compare(pattern, target));
180
490
  }
@@ -187,15 +497,14 @@ class Matcher {
187
497
  * @returns true if the capture is successful, false otherwise
188
498
  */
189
499
  private handleCapture(pattern: J, target: J): boolean {
190
- const id = pattern as J.Identifier;
191
- const captureInfo = PlaceholderUtils.parseCapture(id.simpleName);
500
+ const captureName = PlaceholderUtils.getCaptureName(pattern);
192
501
 
193
- if (!captureInfo) {
502
+ if (!captureName) {
194
503
  return false;
195
504
  }
196
505
 
197
506
  // Store the binding
198
- this.bindings.set(captureInfo.name, target);
507
+ this.bindings.set(captureName, target);
199
508
  return true;
200
509
  }
201
510
  }
@@ -245,6 +554,24 @@ namespace JavaCoordinates {
245
554
  */
246
555
  export type TemplateParameter = Capture | Tree | string | number | boolean;
247
556
 
557
+ /**
558
+ * Configuration options for templates.
559
+ */
560
+ export interface TemplateOptions {
561
+ /**
562
+ * Import statements to provide type attribution context.
563
+ * These are prepended to the template when parsing to ensure proper type information.
564
+ */
565
+ imports?: string[];
566
+
567
+ /**
568
+ * NPM dependencies required for import resolution and type attribution.
569
+ * Maps package names to version specifiers (e.g., { 'util': '^1.0.0' }).
570
+ * The template engine will create a package.json with these dependencies.
571
+ */
572
+ dependencies?: Record<string, string>;
573
+ }
574
+
248
575
  /**
249
576
  * Template for creating AST nodes.
250
577
  *
@@ -260,6 +587,8 @@ export type TemplateParameter = Capture | Tree | string | number | boolean;
260
587
  * const result = template`${capture()}`.apply(cursor, coordinates);
261
588
  */
262
589
  export class Template {
590
+ private options: TemplateOptions = {};
591
+
263
592
  /**
264
593
  * Creates a new template.
265
594
  *
@@ -272,6 +601,24 @@ export class Template {
272
601
  ) {
273
602
  }
274
603
 
604
+ /**
605
+ * Configures this template with additional options.
606
+ *
607
+ * @param options Configuration options
608
+ * @returns This template for method chaining
609
+ *
610
+ * @example
611
+ * template`isDate(${capture('date')})`
612
+ * .configure({
613
+ * imports: ['import { isDate } from "util"'],
614
+ * dependencies: { 'util': '^1.0.0' }
615
+ * })
616
+ */
617
+ configure(options: TemplateOptions): Template {
618
+ this.options = { ...this.options, ...options };
619
+ return this;
620
+ }
621
+
275
622
  /**
276
623
  * Applies this template and returns the resulting tree.
277
624
  *
@@ -281,10 +628,18 @@ export class Template {
281
628
  * @returns A Promise resolving to the generated AST node
282
629
  */
283
630
  async apply(cursor: Cursor, tree: J, values?: Pick<Map<string, J>, 'get'>): Promise<J | undefined> {
284
- return TemplateEngine.applyTemplate(this.templateParts, this.parameters, cursor, {
285
- tree,
286
- mode: JavaCoordinates.Mode.Replace
287
- }, values);
631
+ return TemplateEngine.applyTemplate(
632
+ this.templateParts,
633
+ this.parameters,
634
+ cursor,
635
+ {
636
+ tree,
637
+ mode: JavaCoordinates.Mode.Replace
638
+ },
639
+ values,
640
+ this.options.imports || [],
641
+ this.options.dependencies || {}
642
+ );
288
643
  }
289
644
  }
290
645
 
@@ -322,6 +677,8 @@ class TemplateEngine {
322
677
  * @param cursor The cursor pointing to the current location in the AST
323
678
  * @param coordinates The coordinates specifying where and how to insert the generated AST
324
679
  * @param values Map of capture names to values to replace the parameters with
680
+ * @param imports Import statements to prepend for type attribution
681
+ * @param dependencies NPM dependencies for type attribution
325
682
  * @returns A Promise resolving to the generated AST node
326
683
  */
327
684
  static async applyTemplate(
@@ -329,7 +686,9 @@ class TemplateEngine {
329
686
  parameters: Parameter[],
330
687
  cursor: Cursor,
331
688
  coordinates: JavaCoordinates,
332
- values: Pick<Map<string, J>, 'get'> = new Map()
689
+ values: Pick<Map<string, J>, 'get'> = new Map(),
690
+ imports: string[] = [],
691
+ dependencies: Record<string, string> = {}
333
692
  ): Promise<J | undefined> {
334
693
  // Build the template string with parameter placeholders
335
694
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
@@ -339,27 +698,40 @@ class TemplateEngine {
339
698
  return undefined;
340
699
  }
341
700
 
342
- // Parse the template string into an AST
343
- const parser = new JavaScriptParser();
344
- const parseGenerator = parser.parse({text: templateString, sourcePath: 'template.ts'});
345
- const cu: JS.CompilationUnit = (await parseGenerator.next()).value as JS.CompilationUnit;
701
+ // Use cache to get or parse the compilation unit
702
+ // For templates, we don't have captures, so use empty array
703
+ const cu = await templateCache.getOrParse(
704
+ templateString,
705
+ [], // templates don't have captures in the cache key
706
+ imports,
707
+ dependencies
708
+ );
346
709
 
347
710
  // Check if there are any statements
348
711
  if (!cu.statements || cu.statements.length === 0) {
349
712
  return undefined;
350
713
  }
351
714
 
715
+ // Skip import statements to get to the actual template code
716
+ const templateStatementIndex = imports.length;
717
+ if (templateStatementIndex >= cu.statements.length) {
718
+ return undefined;
719
+ }
720
+
352
721
  // Extract the relevant part of the AST
353
- const firstStatement = cu.statements[0].element;
354
- const ast = firstStatement.kind === JS.Kind.ExpressionStatement ?
722
+ const firstStatement = cu.statements[templateStatementIndex].element;
723
+ let extracted = firstStatement.kind === JS.Kind.ExpressionStatement ?
355
724
  (firstStatement as JS.ExpressionStatement).expression :
356
725
  firstStatement;
357
726
 
727
+ // Create a copy to avoid sharing cached AST instances
728
+ const ast = produce(extracted, draft => {});
729
+
358
730
  // Create substitutions map for placeholders
359
731
  const substitutions = new Map<string, Parameter>();
360
732
  for (let i = 0; i < parameters.length; i++) {
361
733
  const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
362
- substitutions.set(placeholder, typeof parameters[i].value === 'string' ? {value: values.get(parameters[i].value) || parameters[i].value} : parameters[i]);
734
+ substitutions.set(placeholder, parameters[i]);
363
735
  }
364
736
 
365
737
  // Unsubstitute placeholders with actual parameter values and match results
@@ -377,16 +749,22 @@ class TemplateEngine {
377
749
  * @param parameters The parameters between the string parts
378
750
  * @returns The template string
379
751
  */
380
- private static buildTemplateString(templateParts: TemplateStringsArray, parameters: Parameter[]): string {
752
+ private static buildTemplateString(
753
+ templateParts: TemplateStringsArray,
754
+ parameters: Parameter[]
755
+ ): string {
381
756
  let result = '';
382
757
  for (let i = 0; i < templateParts.length; i++) {
383
758
  result += templateParts[i];
384
759
  if (i < parameters.length) {
385
- if (parameters[i].value instanceof CaptureImpl || typeof parameters[i].value === 'string' || isTree(parameters[i].value)) {
760
+ const param = parameters[i].value;
761
+ // Use a placeholder for Captures and Tree nodes
762
+ // Inline everything else (strings, numbers, booleans) directly
763
+ if (param instanceof CaptureImpl || isTree(param)) {
386
764
  const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
387
765
  result += placeholder;
388
766
  } else {
389
- result += parameters[i].value;
767
+ result += param;
390
768
  }
391
769
  }
392
770
  }
@@ -409,13 +787,32 @@ class PlaceholderUtils {
409
787
  * @returns true if the node is a capture placeholder, false otherwise
410
788
  */
411
789
  static isCapture(node: J): boolean {
412
- if (node.kind === J.Kind.Identifier) {
413
- const id = node as J.Identifier;
414
- return id.simpleName.startsWith(this.CAPTURE_PREFIX);
790
+ // Check for CaptureMarker first (efficient)
791
+ for (const marker of node.markers.markers) {
792
+ if (marker instanceof CaptureMarker) {
793
+ return true;
794
+ }
415
795
  }
416
796
  return false;
417
797
  }
418
798
 
799
+ /**
800
+ * Gets the capture name from a node with a CaptureMarker.
801
+ *
802
+ * @param node The node to extract capture name from
803
+ * @returns The capture name, or null if not a capture
804
+ */
805
+ static getCaptureName(node: J): string | undefined {
806
+ // Check for CaptureMarker
807
+ for (const marker of node.markers.markers) {
808
+ if (marker instanceof CaptureMarker) {
809
+ return marker.captureName;
810
+ }
811
+ }
812
+
813
+ return undefined;
814
+ }
815
+
419
816
  /**
420
817
  * Parses a capture placeholder to extract name and type constraint.
421
818
  *
@@ -646,10 +1043,14 @@ class TemplateProcessor {
646
1043
  *
647
1044
  * @param templateParts The string parts of the template
648
1045
  * @param captures The captures between the string parts
1046
+ * @param imports Import statements to prepend for type attribution
1047
+ * @param dependencies NPM dependencies for type attribution
649
1048
  */
650
1049
  constructor(
651
1050
  private readonly templateParts: TemplateStringsArray,
652
- private readonly captures: Capture[]
1051
+ private readonly captures: Capture[],
1052
+ private readonly imports: string[] = [],
1053
+ private readonly dependencies: Record<string, string> = {}
653
1054
  ) {
654
1055
  }
655
1056
 
@@ -662,10 +1063,14 @@ class TemplateProcessor {
662
1063
  // Combine template parts and placeholders
663
1064
  const templateString = this.buildTemplateString();
664
1065
 
665
- // Parse template string to AST
666
- const parser = new JavaScriptParser();
667
- const parseGenerator = parser.parse({text: templateString, sourcePath: 'template.ts'});
668
- const cu: JS.CompilationUnit = (await parseGenerator.next()).value as JS.CompilationUnit;
1066
+ // Use cache to get or parse the compilation unit
1067
+ const cu = await templateCache.getOrParse(
1068
+ templateString,
1069
+ this.captures,
1070
+ this.imports,
1071
+ this.dependencies
1072
+ );
1073
+
669
1074
  // Extract the relevant part of the AST
670
1075
  return this.extractPatternFromAst(cu);
671
1076
  }
@@ -694,16 +1099,81 @@ class TemplateProcessor {
694
1099
  * @returns The extracted pattern
695
1100
  */
696
1101
  private extractPatternFromAst(cu: JS.CompilationUnit): J {
1102
+ // Skip import statements to get to the actual pattern code
1103
+ const patternStatementIndex = this.imports.length;
1104
+
697
1105
  // Extract the relevant part of the AST based on the template content
698
- const firstStatement = cu.statements[0].element;
1106
+ const firstStatement = cu.statements[patternStatementIndex].element;
699
1107
 
1108
+ let extracted: J;
700
1109
  // If the first statement is an expression statement, extract the expression
701
1110
  if (firstStatement.kind === JS.Kind.ExpressionStatement) {
702
- return (firstStatement as JS.ExpressionStatement).expression;
1111
+ extracted = (firstStatement as JS.ExpressionStatement).expression;
1112
+ } else {
1113
+ // Otherwise, return the statement itself
1114
+ extracted = firstStatement;
703
1115
  }
704
1116
 
705
- // Otherwise, return the statement itself
706
- return firstStatement;
1117
+ // Attach CaptureMarkers to capture identifiers
1118
+ return this.attachCaptureMarkers(extracted);
1119
+ }
1120
+
1121
+ /**
1122
+ * Attaches CaptureMarkers to capture identifiers in the AST.
1123
+ * This allows efficient capture detection without string parsing.
1124
+ *
1125
+ * @param ast The AST to process
1126
+ * @returns The AST with CaptureMarkers attached
1127
+ */
1128
+ private attachCaptureMarkers(ast: J): J {
1129
+ const visited = new Set<any>();
1130
+ return produce(ast, draft => {
1131
+ this.visitAndAttachMarkers(draft, visited);
1132
+ });
1133
+ }
1134
+
1135
+ /**
1136
+ * Recursively visits AST nodes and attaches CaptureMarkers to capture identifiers.
1137
+ *
1138
+ * @param node The node to visit
1139
+ * @param visited Set of already visited nodes to avoid cycles
1140
+ */
1141
+ private visitAndAttachMarkers(node: any, visited: Set<any>): void {
1142
+ if (!node || typeof node !== 'object' || visited.has(node)) {
1143
+ return;
1144
+ }
1145
+
1146
+ // Mark as visited to avoid cycles
1147
+ visited.add(node);
1148
+
1149
+ // If this is an identifier that looks like a capture, attach a marker
1150
+ if (node.kind === J.Kind.Identifier && node.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
1151
+ const captureInfo = PlaceholderUtils.parseCapture(node.simpleName);
1152
+ if (captureInfo) {
1153
+ // Initialize markers if needed
1154
+ if (!node.markers) {
1155
+ node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
1156
+ }
1157
+ if (!node.markers.markers) {
1158
+ node.markers.markers = [];
1159
+ }
1160
+
1161
+ // Add CaptureMarker
1162
+ node.markers.markers.push(new CaptureMarker(captureInfo.name));
1163
+ }
1164
+ }
1165
+
1166
+ // Recursively visit all properties
1167
+ for (const key in node) {
1168
+ if (node.hasOwnProperty(key)) {
1169
+ const value = node[key];
1170
+ if (Array.isArray(value)) {
1171
+ value.forEach(item => this.visitAndAttachMarkers(item, visited));
1172
+ } else if (typeof value === 'object' && value !== null) {
1173
+ this.visitAndAttachMarkers(value, visited);
1174
+ }
1175
+ }
1176
+ }
707
1177
  }
708
1178
  }
709
1179
 
@@ -711,6 +1181,15 @@ class TemplateProcessor {
711
1181
  * Represents a replacement rule that can match a pattern and apply a template.
712
1182
  */
713
1183
  export interface RewriteRule {
1184
+ /**
1185
+ * Attempts to apply this rewrite rule to the given AST node.
1186
+ *
1187
+ * @param cursor The cursor context at the current position in the AST
1188
+ * @param node The AST node to try matching and transforming
1189
+ * @returns The transformed node if a pattern matched, or `undefined` if no pattern matched.
1190
+ * When using in a visitor, always use the `|| node` pattern to return the original
1191
+ * node when there's no match: `return await rule.tryOn(this.cursor, node) || node;`
1192
+ */
714
1193
  tryOn(cursor: Cursor, node: J): Promise<J | undefined>;
715
1194
  }
716
1195
 
@@ -735,7 +1214,6 @@ class RewriteRuleImpl implements RewriteRule {
735
1214
  async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
736
1215
  for (const pattern of this.before) {
737
1216
  const match = await pattern.match(node);
738
-
739
1217
  if (match) {
740
1218
  const result = await this.after.apply(cursor, node, match);
741
1219
  if (result) {
@@ -757,20 +1235,33 @@ class RewriteRuleImpl implements RewriteRule {
757
1235
  *
758
1236
  * @example
759
1237
  * // Single pattern
760
- * const swapOperands = replace<J.Binary>(() => ({
1238
+ * const swapOperands = rewrite(() => ({
761
1239
  * before: pattern`${"left"} + ${"right"}`,
762
1240
  * after: template`${"right"} + ${"left"}`
763
1241
  * }));
764
1242
  *
765
1243
  * @example
766
1244
  * // Multiple patterns
767
- * const normalizeComparisons = replace<J.Binary>(() => ({
1245
+ * const normalizeComparisons = rewrite(() => ({
768
1246
  * before: [
769
1247
  * pattern`${"left"} == ${"right"}`,
770
1248
  * pattern`${"left"} === ${"right"}`
771
1249
  * ],
772
1250
  * after: template`${"left"} === ${"right"}`
773
1251
  * }));
1252
+ *
1253
+ * @example
1254
+ * // Using in a visitor - IMPORTANT: use `|| node` to handle undefined when no match
1255
+ * class MyVisitor extends JavaScriptVisitor<any> {
1256
+ * override async visitBinary(binary: J.Binary, p: any): Promise<J | undefined> {
1257
+ * const rule = rewrite(() => ({
1258
+ * before: pattern`${capture('a')} + ${capture('b')}`,
1259
+ * after: template`${capture('b')} + ${capture('a')}`
1260
+ * }));
1261
+ * // tryOn() returns undefined if no pattern matches, so always use || node
1262
+ * return await rule.tryOn(this.cursor, binary) || binary;
1263
+ * }
1264
+ * }
774
1265
  */
775
1266
  export function rewrite(
776
1267
  builderFn: () => RewriteConfig