@openrewrite/rewrite 8.63.3 → 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 (55) 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/server.d.ts.map +1 -1
  36. package/dist/rpc/server.js +5 -0
  37. package/dist/rpc/server.js.map +1 -1
  38. package/dist/test/rewrite-test.d.ts +1 -1
  39. package/dist/test/rewrite-test.d.ts.map +1 -1
  40. package/dist/test/rewrite-test.js +27 -5
  41. package/dist/test/rewrite-test.js.map +1 -1
  42. package/dist/version.txt +1 -1
  43. package/package.json +1 -1
  44. package/src/java/rpc.ts +4 -4
  45. package/src/java/type.ts +3 -3
  46. package/src/javascript/assertions.ts +14 -21
  47. package/src/javascript/comparator.ts +2 -2
  48. package/src/javascript/dependency-workspace.ts +317 -0
  49. package/src/javascript/parser.ts +6 -3
  50. package/src/javascript/preconditions.ts +2 -2
  51. package/src/javascript/templating.ts +535 -44
  52. package/src/javascript/type-mapping.ts +2 -2
  53. package/src/rpc/queue.ts +11 -1
  54. package/src/rpc/server.ts +5 -0
  55. package/src/test/rewrite-test.ts +11 -3
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.Template = exports.MatchResult = exports.Pattern = void 0;
12
+ exports.Template = exports.MatchResult = exports.Pattern = exports._ = void 0;
13
13
  exports.capture = capture;
14
14
  exports.pattern = pattern;
15
15
  exports.template = template;
@@ -36,6 +36,79 @@ const __1 = require("..");
36
36
  const java_1 = require("../java");
37
37
  const immer_1 = require("immer");
38
38
  const comparator_1 = require("./comparator");
39
+ const dependency_workspace_1 = require("./dependency-workspace");
40
+ const uuid_1 = require("../uuid");
41
+ /**
42
+ * Cache for compiled templates and patterns.
43
+ * Stores parsed ASTs to avoid expensive re-parsing and dependency resolution.
44
+ */
45
+ class TemplateCache {
46
+ constructor() {
47
+ this.cache = new Map();
48
+ }
49
+ /**
50
+ * Generates a cache key from template string, captures, and options.
51
+ */
52
+ generateKey(templateString, captures, imports, dependencies) {
53
+ // Use the actual template string (with placeholders) as the primary key
54
+ const templateKey = templateString;
55
+ // Capture names
56
+ const capturesKey = captures.map(c => c.name).join(',');
57
+ // Imports
58
+ const importsKey = imports.join(';');
59
+ // Dependencies
60
+ const depsKey = JSON.stringify(dependencies || {});
61
+ return `${templateKey}::${capturesKey}::${importsKey}::${depsKey}`;
62
+ }
63
+ /**
64
+ * Gets a cached compilation unit or creates and caches a new one.
65
+ */
66
+ getOrParse(templateString, captures, imports, dependencies) {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ const key = this.generateKey(templateString, captures, imports, dependencies);
69
+ let cu = this.cache.get(key);
70
+ if (cu) {
71
+ return cu;
72
+ }
73
+ // Create workspace if dependencies are provided
74
+ // DependencyWorkspace has its own cache, so multiple templates with
75
+ // the same dependencies will automatically share the same workspace
76
+ let workspaceDir;
77
+ if (dependencies && Object.keys(dependencies).length > 0) {
78
+ workspaceDir = yield dependency_workspace_1.DependencyWorkspace.getOrCreateWorkspace(dependencies);
79
+ }
80
+ // Prepend imports for type attribution context
81
+ const fullTemplateString = imports.length > 0
82
+ ? imports.join('\n') + '\n' + templateString
83
+ : templateString;
84
+ // Parse and cache (workspace only needed during parsing)
85
+ const parser = new parser_1.JavaScriptParser({ relativeTo: workspaceDir });
86
+ const parseGenerator = parser.parse({ text: fullTemplateString, sourcePath: 'template.ts' });
87
+ cu = (yield parseGenerator.next()).value;
88
+ this.cache.set(key, cu);
89
+ return cu;
90
+ });
91
+ }
92
+ /**
93
+ * Clears the cache.
94
+ */
95
+ clear() {
96
+ this.cache.clear();
97
+ }
98
+ }
99
+ // Global cache instance
100
+ const templateCache = new TemplateCache();
101
+ /**
102
+ * Marker that stores capture metadata on pattern AST nodes.
103
+ * This avoids the need to parse capture names from identifiers during matching.
104
+ */
105
+ class CaptureMarker {
106
+ constructor(captureName) {
107
+ this.captureName = captureName;
108
+ this.kind = 'org.openrewrite.javascript.CaptureMarker';
109
+ this.id = (0, uuid_1.randomId)();
110
+ }
111
+ }
39
112
  class CaptureImpl {
40
113
  constructor(name) {
41
114
  this.name = name;
@@ -44,26 +117,52 @@ class CaptureImpl {
44
117
  /**
45
118
  * Creates a capture specification for use in template patterns.
46
119
  *
120
+ * @param name Optional name for the capture. If not provided, an auto-generated name is used.
47
121
  * @returns A Capture object
48
122
  *
49
123
  * @example
50
- * // Multiple captures
124
+ * // Named inline captures
125
+ * const pattern = pattern`${capture('left')} + ${capture('right')}`;
126
+ *
127
+ * // Unnamed captures
51
128
  * const {left, right} = {left: capture(), right: capture()};
52
129
  * const pattern = pattern`${left} + ${right}`;
53
130
  *
54
131
  * // Repeated patterns using the same capture
55
- * const expr = capture();
132
+ * const expr = capture('expr');
56
133
  * const redundantOr = pattern`${expr} || ${expr}`;
57
134
  */
58
- function capture() {
135
+ function capture(name) {
136
+ if (name) {
137
+ return new CaptureImpl(name);
138
+ }
59
139
  return new CaptureImpl(`unnamed_${capture.nextUnnamedId++}`);
60
140
  }
61
141
  // Static counter for generating unique IDs for unnamed captures
62
142
  capture.nextUnnamedId = 1;
143
+ /**
144
+ * Concise alias for `capture`. Works well for inline captures in patterns and templates.
145
+ *
146
+ * @param name Optional name for the capture. If not provided, an auto-generated name is used.
147
+ * @returns A Capture object
148
+ *
149
+ * @example
150
+ * // Inline captures with _ alias
151
+ * pattern`isDate(${_('dateArg')})`
152
+ * template`${_('dateArg')} instanceof Date`
153
+ */
154
+ exports._ = capture;
63
155
  /**
64
156
  * Represents a pattern that can be matched against AST nodes.
65
157
  */
66
158
  class Pattern {
159
+ /**
160
+ * Gets the configuration options for this pattern.
161
+ * @readonly
162
+ */
163
+ get options() {
164
+ return this._options;
165
+ }
67
166
  /**
68
167
  * Creates a new pattern from template parts and captures.
69
168
  *
@@ -73,6 +172,24 @@ class Pattern {
73
172
  constructor(templateParts, captures) {
74
173
  this.templateParts = templateParts;
75
174
  this.captures = captures;
175
+ this._options = {};
176
+ }
177
+ /**
178
+ * Configures this pattern with additional options.
179
+ *
180
+ * @param options Configuration options
181
+ * @returns This pattern for method chaining
182
+ *
183
+ * @example
184
+ * pattern`isDate(${capture('date')})`
185
+ * .configure({
186
+ * imports: ['import { isDate } from "util"'],
187
+ * dependencies: { 'util': '^1.0.0' }
188
+ * })
189
+ */
190
+ configure(options) {
191
+ this._options = Object.assign(Object.assign({}, this._options), options);
192
+ return this;
76
193
  }
77
194
  /**
78
195
  * Creates a matcher for this pattern against a specific AST node.
@@ -99,6 +216,142 @@ class MatchResult {
99
216
  }
100
217
  }
101
218
  exports.MatchResult = MatchResult;
219
+ /**
220
+ * A comparator visitor that checks semantic equality including type attribution.
221
+ * This ensures that patterns only match code with compatible types, not just
222
+ * structurally similar code.
223
+ */
224
+ class JavaScriptTemplateSemanticallyEqualVisitor extends comparator_1.JavaScriptComparatorVisitor {
225
+ /**
226
+ * Checks if two types are semantically equal.
227
+ * For method types, this checks that the declaring type and method name match.
228
+ */
229
+ isOfType(target, source) {
230
+ if (!target || !source) {
231
+ return target === source;
232
+ }
233
+ if (target.kind !== source.kind) {
234
+ return false;
235
+ }
236
+ // For method types, check declaring type
237
+ // Note: We don't check the name field because it might not be fully resolved in patterns
238
+ // The method invocation visitor already checks that simple names match
239
+ if (target.kind === java_1.Type.Kind.Method && source.kind === java_1.Type.Kind.Method) {
240
+ const targetMethod = target;
241
+ const sourceMethod = source;
242
+ // Only check that declaring types match
243
+ return this.isOfType(targetMethod.declaringType, sourceMethod.declaringType);
244
+ }
245
+ // For fully qualified types, check the fully qualified name
246
+ if (java_1.Type.isFullyQualified(target) && java_1.Type.isFullyQualified(source)) {
247
+ return java_1.Type.FullyQualified.getFullyQualifiedName(target) ===
248
+ java_1.Type.FullyQualified.getFullyQualifiedName(source);
249
+ }
250
+ // Default: types are equal if they're the same kind
251
+ return true;
252
+ }
253
+ /**
254
+ * Override method invocation comparison to include type attribution checking.
255
+ * When types match semantically, we allow matching even if one has a receiver
256
+ * and the other doesn't (e.g., `isDate(x)` vs `util.isDate(x)`).
257
+ */
258
+ visitMethodInvocation(method, other) {
259
+ const _super = Object.create(null, {
260
+ visitMethodInvocation: { get: () => super.visitMethodInvocation }
261
+ });
262
+ return __awaiter(this, void 0, void 0, function* () {
263
+ if (other.kind !== java_1.J.Kind.MethodInvocation) {
264
+ return method;
265
+ }
266
+ const otherMethod = other;
267
+ // Check basic structural equality first
268
+ if (method.name.simpleName !== otherMethod.name.simpleName ||
269
+ method.arguments.elements.length !== otherMethod.arguments.elements.length) {
270
+ this.abort();
271
+ return method;
272
+ }
273
+ // Check type attribution
274
+ // Both must have method types for semantic equality
275
+ if (!method.methodType || !otherMethod.methodType) {
276
+ // If template has type but target doesn't, they don't match
277
+ if (method.methodType || otherMethod.methodType) {
278
+ this.abort();
279
+ return method;
280
+ }
281
+ // If neither has type, fall through to structural comparison
282
+ return _super.visitMethodInvocation.call(this, method, other);
283
+ }
284
+ // Both have types - check they match semantically
285
+ const typesMatch = this.isOfType(method.methodType, otherMethod.methodType);
286
+ if (!typesMatch) {
287
+ // Types don't match - abort comparison
288
+ this.abort();
289
+ return method;
290
+ }
291
+ // Types match! Now we can ignore receiver differences and just compare arguments.
292
+ // This allows pattern `isDate(x)` to match both `isDate(x)` and `util.isDate(x)`
293
+ // when they have the same type attribution.
294
+ // Compare type parameters
295
+ if ((method.typeParameters === undefined) !== (otherMethod.typeParameters === undefined)) {
296
+ this.abort();
297
+ return method;
298
+ }
299
+ if (method.typeParameters && otherMethod.typeParameters) {
300
+ if (method.typeParameters.elements.length !== otherMethod.typeParameters.elements.length) {
301
+ this.abort();
302
+ return method;
303
+ }
304
+ for (let i = 0; i < method.typeParameters.elements.length; i++) {
305
+ yield this.visit(method.typeParameters.elements[i].element, otherMethod.typeParameters.elements[i].element);
306
+ if (!this.match)
307
+ return method;
308
+ }
309
+ }
310
+ // Compare name (already checked simpleName above, but visit for markers/prefix)
311
+ yield this.visit(method.name, otherMethod.name);
312
+ if (!this.match)
313
+ return method;
314
+ // Compare arguments
315
+ for (let i = 0; i < method.arguments.elements.length; i++) {
316
+ yield this.visit(method.arguments.elements[i].element, otherMethod.arguments.elements[i].element);
317
+ if (!this.match)
318
+ return method;
319
+ }
320
+ return method;
321
+ });
322
+ }
323
+ /**
324
+ * Override identifier comparison to include type checking for field access.
325
+ */
326
+ visitIdentifier(identifier, other) {
327
+ const _super = Object.create(null, {
328
+ visitIdentifier: { get: () => super.visitIdentifier }
329
+ });
330
+ return __awaiter(this, void 0, void 0, function* () {
331
+ if (other.kind !== java_1.J.Kind.Identifier) {
332
+ return identifier;
333
+ }
334
+ const otherIdentifier = other;
335
+ // Check name matches
336
+ if (identifier.simpleName !== otherIdentifier.simpleName) {
337
+ return identifier;
338
+ }
339
+ // For identifiers with field types, check type attribution
340
+ if (identifier.fieldType && otherIdentifier.fieldType) {
341
+ if (!this.isOfType(identifier.fieldType, otherIdentifier.fieldType)) {
342
+ this.abort();
343
+ return identifier;
344
+ }
345
+ }
346
+ else if (identifier.fieldType || otherIdentifier.fieldType) {
347
+ // If only one has a type, they don't match
348
+ this.abort();
349
+ return identifier;
350
+ }
351
+ return _super.visitIdentifier.call(this, identifier, other);
352
+ });
353
+ }
354
+ }
102
355
  /**
103
356
  * Matcher for checking if a pattern matches an AST node and extracting captured nodes.
104
357
  */
@@ -122,8 +375,8 @@ class Matcher {
122
375
  matches() {
123
376
  return __awaiter(this, void 0, void 0, function* () {
124
377
  if (!this.patternAst) {
125
- this.templateProcessor = new TemplateProcessor(this.pattern.templateParts, this.pattern.captures);
126
- this.patternAst = yield this.templateProcessor.toAstPattern();
378
+ const templateProcessor = new TemplateProcessor(this.pattern.templateParts, this.pattern.captures, this.pattern.options.imports || [], this.pattern.options.dependencies || {});
379
+ this.patternAst = yield templateProcessor.toAstPattern();
127
380
  }
128
381
  return this.matchNode(this.patternAst, this.ast);
129
382
  });
@@ -154,7 +407,7 @@ class Matcher {
154
407
  return false;
155
408
  }
156
409
  const matcher = this;
157
- return yield ((new class extends comparator_1.JavaScriptComparatorVisitor {
410
+ return yield ((new class extends JavaScriptTemplateSemanticallyEqualVisitor {
158
411
  hasSameKind(j, other) {
159
412
  return super.hasSameKind(j, other) || j.kind == java_1.J.Kind.Identifier && this.matchesParameter(j, other);
160
413
  }
@@ -167,8 +420,7 @@ class Matcher {
167
420
  });
168
421
  }
169
422
  matchesParameter(identifier, other) {
170
- return PlaceholderUtils.isCapture(identifier) &&
171
- matcher.handleCapture(identifier, other);
423
+ return PlaceholderUtils.isCapture(identifier) && matcher.handleCapture(identifier, other);
172
424
  }
173
425
  }).compare(pattern, target));
174
426
  });
@@ -181,13 +433,12 @@ class Matcher {
181
433
  * @returns true if the capture is successful, false otherwise
182
434
  */
183
435
  handleCapture(pattern, target) {
184
- const id = pattern;
185
- const captureInfo = PlaceholderUtils.parseCapture(id.simpleName);
186
- if (!captureInfo) {
436
+ const captureName = PlaceholderUtils.getCaptureName(pattern);
437
+ if (!captureName) {
187
438
  return false;
188
439
  }
189
440
  // Store the binding
190
- this.bindings.set(captureInfo.name, target);
441
+ this.bindings.set(captureName, target);
191
442
  return true;
192
443
  }
193
444
  }
@@ -243,6 +494,24 @@ class Template {
243
494
  constructor(templateParts, parameters) {
244
495
  this.templateParts = templateParts;
245
496
  this.parameters = parameters;
497
+ this.options = {};
498
+ }
499
+ /**
500
+ * Configures this template with additional options.
501
+ *
502
+ * @param options Configuration options
503
+ * @returns This template for method chaining
504
+ *
505
+ * @example
506
+ * template`isDate(${capture('date')})`
507
+ * .configure({
508
+ * imports: ['import { isDate } from "util"'],
509
+ * dependencies: { 'util': '^1.0.0' }
510
+ * })
511
+ */
512
+ configure(options) {
513
+ this.options = Object.assign(Object.assign({}, this.options), options);
514
+ return this;
246
515
  }
247
516
  /**
248
517
  * Applies this template and returns the resulting tree.
@@ -257,7 +526,7 @@ class Template {
257
526
  return TemplateEngine.applyTemplate(this.templateParts, this.parameters, cursor, {
258
527
  tree,
259
528
  mode: JavaCoordinates.Mode.Replace
260
- }, values);
529
+ }, values, this.options.imports || [], this.options.dependencies || {});
261
530
  });
262
531
  }
263
532
  }
@@ -283,34 +552,43 @@ class TemplateEngine {
283
552
  * @param cursor The cursor pointing to the current location in the AST
284
553
  * @param coordinates The coordinates specifying where and how to insert the generated AST
285
554
  * @param values Map of capture names to values to replace the parameters with
555
+ * @param imports Import statements to prepend for type attribution
556
+ * @param dependencies NPM dependencies for type attribution
286
557
  * @returns A Promise resolving to the generated AST node
287
558
  */
288
559
  static applyTemplate(templateParts_1, parameters_1, cursor_1, coordinates_1) {
289
- return __awaiter(this, arguments, void 0, function* (templateParts, parameters, cursor, coordinates, values = new Map()) {
560
+ return __awaiter(this, arguments, void 0, function* (templateParts, parameters, cursor, coordinates, values = new Map(), imports = [], dependencies = {}) {
290
561
  // Build the template string with parameter placeholders
291
562
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
292
563
  // If the template string is empty, return undefined
293
564
  if (!templateString.trim()) {
294
565
  return undefined;
295
566
  }
296
- // Parse the template string into an AST
297
- const parser = new parser_1.JavaScriptParser();
298
- const parseGenerator = parser.parse({ text: templateString, sourcePath: 'template.ts' });
299
- const cu = (yield parseGenerator.next()).value;
567
+ // Use cache to get or parse the compilation unit
568
+ // For templates, we don't have captures, so use empty array
569
+ const cu = yield templateCache.getOrParse(templateString, [], // templates don't have captures in the cache key
570
+ imports, dependencies);
300
571
  // Check if there are any statements
301
572
  if (!cu.statements || cu.statements.length === 0) {
302
573
  return undefined;
303
574
  }
575
+ // Skip import statements to get to the actual template code
576
+ const templateStatementIndex = imports.length;
577
+ if (templateStatementIndex >= cu.statements.length) {
578
+ return undefined;
579
+ }
304
580
  // Extract the relevant part of the AST
305
- const firstStatement = cu.statements[0].element;
306
- const ast = firstStatement.kind === _1.JS.Kind.ExpressionStatement ?
581
+ const firstStatement = cu.statements[templateStatementIndex].element;
582
+ let extracted = firstStatement.kind === _1.JS.Kind.ExpressionStatement ?
307
583
  firstStatement.expression :
308
584
  firstStatement;
585
+ // Create a copy to avoid sharing cached AST instances
586
+ const ast = (0, immer_1.produce)(extracted, draft => { });
309
587
  // Create substitutions map for placeholders
310
588
  const substitutions = new Map();
311
589
  for (let i = 0; i < parameters.length; i++) {
312
590
  const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
313
- substitutions.set(placeholder, typeof parameters[i].value === 'string' ? { value: values.get(parameters[i].value) || parameters[i].value } : parameters[i]);
591
+ substitutions.set(placeholder, parameters[i]);
314
592
  }
315
593
  // Unsubstitute placeholders with actual parameter values and match results
316
594
  const visitor = new PlaceholderReplacementVisitor(substitutions, values);
@@ -331,12 +609,15 @@ class TemplateEngine {
331
609
  for (let i = 0; i < templateParts.length; i++) {
332
610
  result += templateParts[i];
333
611
  if (i < parameters.length) {
334
- if (parameters[i].value instanceof CaptureImpl || typeof parameters[i].value === 'string' || (0, __1.isTree)(parameters[i].value)) {
612
+ const param = parameters[i].value;
613
+ // Use a placeholder for Captures and Tree nodes
614
+ // Inline everything else (strings, numbers, booleans) directly
615
+ if (param instanceof CaptureImpl || (0, __1.isTree)(param)) {
335
616
  const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
336
617
  result += placeholder;
337
618
  }
338
619
  else {
339
- result += parameters[i].value;
620
+ result += param;
340
621
  }
341
622
  }
342
623
  }
@@ -355,12 +636,29 @@ class PlaceholderUtils {
355
636
  * @returns true if the node is a capture placeholder, false otherwise
356
637
  */
357
638
  static isCapture(node) {
358
- if (node.kind === java_1.J.Kind.Identifier) {
359
- const id = node;
360
- return id.simpleName.startsWith(this.CAPTURE_PREFIX);
639
+ // Check for CaptureMarker first (efficient)
640
+ for (const marker of node.markers.markers) {
641
+ if (marker instanceof CaptureMarker) {
642
+ return true;
643
+ }
361
644
  }
362
645
  return false;
363
646
  }
647
+ /**
648
+ * Gets the capture name from a node with a CaptureMarker.
649
+ *
650
+ * @param node The node to extract capture name from
651
+ * @returns The capture name, or null if not a capture
652
+ */
653
+ static getCaptureName(node) {
654
+ // Check for CaptureMarker
655
+ for (const marker of node.markers.markers) {
656
+ if (marker instanceof CaptureMarker) {
657
+ return marker.captureName;
658
+ }
659
+ }
660
+ return undefined;
661
+ }
364
662
  /**
365
663
  * Parses a capture placeholder to extract name and type constraint.
366
664
  *
@@ -582,10 +880,14 @@ class TemplateProcessor {
582
880
  *
583
881
  * @param templateParts The string parts of the template
584
882
  * @param captures The captures between the string parts
883
+ * @param imports Import statements to prepend for type attribution
884
+ * @param dependencies NPM dependencies for type attribution
585
885
  */
586
- constructor(templateParts, captures) {
886
+ constructor(templateParts, captures, imports = [], dependencies = {}) {
587
887
  this.templateParts = templateParts;
588
888
  this.captures = captures;
889
+ this.imports = imports;
890
+ this.dependencies = dependencies;
589
891
  }
590
892
  /**
591
893
  * Converts the template to an AST pattern.
@@ -596,10 +898,8 @@ class TemplateProcessor {
596
898
  return __awaiter(this, void 0, void 0, function* () {
597
899
  // Combine template parts and placeholders
598
900
  const templateString = this.buildTemplateString();
599
- // Parse template string to AST
600
- const parser = new parser_1.JavaScriptParser();
601
- const parseGenerator = parser.parse({ text: templateString, sourcePath: 'template.ts' });
602
- const cu = (yield parseGenerator.next()).value;
901
+ // Use cache to get or parse the compilation unit
902
+ const cu = yield templateCache.getOrParse(templateString, this.captures, this.imports, this.dependencies);
603
903
  // Extract the relevant part of the AST
604
904
  return this.extractPatternFromAst(cu);
605
905
  });
@@ -627,14 +927,75 @@ class TemplateProcessor {
627
927
  * @returns The extracted pattern
628
928
  */
629
929
  extractPatternFromAst(cu) {
930
+ // Skip import statements to get to the actual pattern code
931
+ const patternStatementIndex = this.imports.length;
630
932
  // Extract the relevant part of the AST based on the template content
631
- const firstStatement = cu.statements[0].element;
933
+ const firstStatement = cu.statements[patternStatementIndex].element;
934
+ let extracted;
632
935
  // If the first statement is an expression statement, extract the expression
633
936
  if (firstStatement.kind === _1.JS.Kind.ExpressionStatement) {
634
- return firstStatement.expression;
937
+ extracted = firstStatement.expression;
938
+ }
939
+ else {
940
+ // Otherwise, return the statement itself
941
+ extracted = firstStatement;
942
+ }
943
+ // Attach CaptureMarkers to capture identifiers
944
+ return this.attachCaptureMarkers(extracted);
945
+ }
946
+ /**
947
+ * Attaches CaptureMarkers to capture identifiers in the AST.
948
+ * This allows efficient capture detection without string parsing.
949
+ *
950
+ * @param ast The AST to process
951
+ * @returns The AST with CaptureMarkers attached
952
+ */
953
+ attachCaptureMarkers(ast) {
954
+ const visited = new Set();
955
+ return (0, immer_1.produce)(ast, draft => {
956
+ this.visitAndAttachMarkers(draft, visited);
957
+ });
958
+ }
959
+ /**
960
+ * Recursively visits AST nodes and attaches CaptureMarkers to capture identifiers.
961
+ *
962
+ * @param node The node to visit
963
+ * @param visited Set of already visited nodes to avoid cycles
964
+ */
965
+ visitAndAttachMarkers(node, visited) {
966
+ var _a;
967
+ if (!node || typeof node !== 'object' || visited.has(node)) {
968
+ return;
969
+ }
970
+ // Mark as visited to avoid cycles
971
+ visited.add(node);
972
+ // If this is an identifier that looks like a capture, attach a marker
973
+ if (node.kind === java_1.J.Kind.Identifier && ((_a = node.simpleName) === null || _a === void 0 ? void 0 : _a.startsWith(PlaceholderUtils.CAPTURE_PREFIX))) {
974
+ const captureInfo = PlaceholderUtils.parseCapture(node.simpleName);
975
+ if (captureInfo) {
976
+ // Initialize markers if needed
977
+ if (!node.markers) {
978
+ node.markers = { kind: 'org.openrewrite.marker.Markers', id: (0, uuid_1.randomId)(), markers: [] };
979
+ }
980
+ if (!node.markers.markers) {
981
+ node.markers.markers = [];
982
+ }
983
+ // Add CaptureMarker
984
+ node.markers.markers.push(new CaptureMarker(captureInfo.name));
985
+ }
986
+ }
987
+ // Recursively visit all properties
988
+ for (const key in node) {
989
+ if (node.hasOwnProperty(key)) {
990
+ const value = node[key];
991
+ if (Array.isArray(value)) {
992
+ value.forEach(item => this.visitAndAttachMarkers(item, visited));
993
+ }
994
+ else if (typeof value === 'object' && value !== null) {
995
+ this.visitAndAttachMarkers(value, visited);
996
+ }
997
+ }
635
998
  }
636
- // Otherwise, return the statement itself
637
- return firstStatement;
638
999
  }
639
1000
  }
640
1001
  /**
@@ -669,20 +1030,33 @@ class RewriteRuleImpl {
669
1030
  *
670
1031
  * @example
671
1032
  * // Single pattern
672
- * const swapOperands = replace<J.Binary>(() => ({
1033
+ * const swapOperands = rewrite(() => ({
673
1034
  * before: pattern`${"left"} + ${"right"}`,
674
1035
  * after: template`${"right"} + ${"left"}`
675
1036
  * }));
676
1037
  *
677
1038
  * @example
678
1039
  * // Multiple patterns
679
- * const normalizeComparisons = replace<J.Binary>(() => ({
1040
+ * const normalizeComparisons = rewrite(() => ({
680
1041
  * before: [
681
1042
  * pattern`${"left"} == ${"right"}`,
682
1043
  * pattern`${"left"} === ${"right"}`
683
1044
  * ],
684
1045
  * after: template`${"left"} === ${"right"}`
685
1046
  * }));
1047
+ *
1048
+ * @example
1049
+ * // Using in a visitor - IMPORTANT: use `|| node` to handle undefined when no match
1050
+ * class MyVisitor extends JavaScriptVisitor<any> {
1051
+ * override async visitBinary(binary: J.Binary, p: any): Promise<J | undefined> {
1052
+ * const rule = rewrite(() => ({
1053
+ * before: pattern`${capture('a')} + ${capture('b')}`,
1054
+ * after: template`${capture('b')} + ${capture('a')}`
1055
+ * }));
1056
+ * // tryOn() returns undefined if no pattern matches, so always use || node
1057
+ * return await rule.tryOn(this.cursor, binary) || binary;
1058
+ * }
1059
+ * }
686
1060
  */
687
1061
  function rewrite(builderFn) {
688
1062
  const config = builderFn();