@openrewrite/rewrite 8.67.0-20251106-160325 → 8.67.0-20251107-071946

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 (39) hide show
  1. package/dist/java/tree.d.ts +2 -0
  2. package/dist/java/tree.d.ts.map +1 -1
  3. package/dist/java/tree.js +5 -1
  4. package/dist/java/tree.js.map +1 -1
  5. package/dist/javascript/assertions.js +2 -2
  6. package/dist/javascript/assertions.js.map +1 -1
  7. package/dist/javascript/format.js +1 -1
  8. package/dist/javascript/format.js.map +1 -1
  9. package/dist/javascript/templating/engine.d.ts +34 -8
  10. package/dist/javascript/templating/engine.d.ts.map +1 -1
  11. package/dist/javascript/templating/engine.js +317 -60
  12. package/dist/javascript/templating/engine.js.map +1 -1
  13. package/dist/javascript/templating/pattern.d.ts +11 -0
  14. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  15. package/dist/javascript/templating/pattern.js +36 -295
  16. package/dist/javascript/templating/pattern.js.map +1 -1
  17. package/dist/javascript/templating/placeholder-replacement.d.ts +1 -1
  18. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  19. package/dist/javascript/templating/template.d.ts +9 -2
  20. package/dist/javascript/templating/template.d.ts.map +1 -1
  21. package/dist/javascript/templating/template.js +37 -0
  22. package/dist/javascript/templating/template.js.map +1 -1
  23. package/dist/javascript/templating/types.d.ts +26 -11
  24. package/dist/javascript/templating/types.d.ts.map +1 -1
  25. package/dist/javascript/templating/utils.d.ts +41 -22
  26. package/dist/javascript/templating/utils.d.ts.map +1 -1
  27. package/dist/javascript/templating/utils.js +111 -76
  28. package/dist/javascript/templating/utils.js.map +1 -1
  29. package/dist/version.txt +1 -1
  30. package/package.json +3 -1
  31. package/src/java/tree.ts +2 -0
  32. package/src/javascript/assertions.ts +1 -1
  33. package/src/javascript/format.ts +1 -1
  34. package/src/javascript/templating/engine.ts +376 -54
  35. package/src/javascript/templating/pattern.ts +55 -322
  36. package/src/javascript/templating/placeholder-replacement.ts +1 -1
  37. package/src/javascript/templating/template.ts +57 -3
  38. package/src/javascript/templating/types.ts +27 -11
  39. package/src/javascript/templating/utils.ts +113 -81
@@ -13,29 +13,169 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {Cursor, isTree} from '../..';
16
+ import ts from 'typescript';
17
+ import {Cursor, isTree, produceAsync, Tree, updateIfChanged} from '../..';
17
18
  import {J, Type} from '../../java';
18
19
  import {JS} from '..';
19
20
  import {produce} from 'immer';
20
- import {PlaceholderUtils, TemplateCache} from './utils';
21
+ import {CaptureMarker, PlaceholderUtils, WRAPPER_FUNCTION_NAME} from './utils';
21
22
  import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, TemplateParamImpl} from './capture';
22
23
  import {PlaceholderReplacementVisitor} from './placeholder-replacement';
23
24
  import {JavaCoordinates} from './template';
25
+ import {JavaScriptVisitor} from '../visitor';
26
+ import {Any, Capture, Parameter} from './types';
27
+ import {JavaScriptParser} from '../parser';
28
+ import {DependencyWorkspace} from '../dependency-workspace';
24
29
 
25
30
  /**
26
- * Cache for compiled templates.
31
+ * Simple LRU (Least Recently Used) cache implementation.
32
+ * Used for template/pattern compilation caching with bounded memory usage.
27
33
  */
28
- const templateCache = new TemplateCache();
34
+ class LRUCache<K, V> {
35
+ private cache = new Map<K, V>();
36
+
37
+ constructor(private maxSize: number) {}
38
+
39
+ get(key: K): V | undefined {
40
+ const value = this.cache.get(key);
41
+ if (value !== undefined) {
42
+ // Move to end (most recently used)
43
+ this.cache.delete(key);
44
+ this.cache.set(key, value);
45
+ }
46
+ return value;
47
+ }
48
+
49
+ set(key: K, value: V): void {
50
+ // Remove if exists (to update position)
51
+ this.cache.delete(key);
52
+
53
+ // Add to end
54
+ this.cache.set(key, value);
55
+
56
+ // Evict oldest if over capacity
57
+ if (this.cache.size > this.maxSize) {
58
+ const iterator = this.cache.keys();
59
+ const firstEntry = iterator.next();
60
+ if (!firstEntry.done) {
61
+ this.cache.delete(firstEntry.value);
62
+ }
63
+ }
64
+ }
65
+
66
+ clear(): void {
67
+ this.cache.clear();
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Module-level TypeScript sourceFileCache for template parsing.
73
+ */
74
+ let templateSourceFileCache: Map<string, ts.SourceFile> | undefined;
29
75
 
30
76
  /**
31
- * Parameter specification for template generation.
32
- * Represents a placeholder in a template that will be replaced with a parameter value.
77
+ * Configure the sourceFileCache used for template parsing.
78
+ *
79
+ * @param cache The sourceFileCache to use, or undefined to disable caching
33
80
  */
34
- export interface Parameter {
81
+ export function setTemplateSourceFileCache(cache?: Map<string, ts.SourceFile>): void {
82
+ templateSourceFileCache = cache;
83
+ }
84
+
85
+ /**
86
+ * Cache for compiled templates and patterns.
87
+ * Stores parsed ASTs to avoid expensive re-parsing and dependency resolution.
88
+ * Bounded to 100 entries using LRU eviction to prevent unbounded memory growth.
89
+ */
90
+ class TemplateCache {
91
+ private cache = new LRUCache<string, JS.CompilationUnit>(100);
92
+
35
93
  /**
36
- * The value to substitute into the template.
94
+ * Generates a cache key from template string, captures, and options.
37
95
  */
38
- value: any;
96
+ private generateKey(
97
+ templateString: string,
98
+ captures: (Capture | Any<any>)[],
99
+ contextStatements: string[],
100
+ dependencies: Record<string, string>
101
+ ): string {
102
+ // Use the actual template string (with placeholders) as the primary key
103
+ const templateKey = templateString;
104
+
105
+ // Capture names
106
+ const capturesKey = captures.map(c => c.getName()).join(',');
107
+
108
+ // Context statements
109
+ const contextKey = contextStatements.join(';');
110
+
111
+ // Dependencies
112
+ const depsKey = JSON.stringify(dependencies || {});
113
+
114
+ return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}`;
115
+ }
116
+
117
+ /**
118
+ * Gets a cached compilation unit or creates and caches a new one.
119
+ */
120
+ async getOrParse(
121
+ templateString: string,
122
+ captures: (Capture | Any)[],
123
+ contextStatements: string[],
124
+ dependencies: Record<string, string>
125
+ ): Promise<JS.CompilationUnit> {
126
+ const key = this.generateKey(templateString, captures, contextStatements, dependencies);
127
+
128
+ let cu = this.cache.get(key);
129
+ if (cu) {
130
+ return cu;
131
+ }
132
+
133
+ // Create workspace if dependencies are provided
134
+ // DependencyWorkspace has its own cache, so multiple templates with
135
+ // the same dependencies will automatically share the same workspace
136
+ let workspaceDir: string | undefined;
137
+ if (dependencies && Object.keys(dependencies).length > 0) {
138
+ workspaceDir = await DependencyWorkspace.getOrCreateWorkspace(dependencies);
139
+ }
140
+
141
+ // Prepend context statements for type attribution context
142
+ const fullTemplateString = contextStatements.length > 0
143
+ ? contextStatements.join('\n') + '\n' + templateString
144
+ : templateString;
145
+
146
+ // Parse and cache (workspace only needed during parsing)
147
+ // Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
148
+ const parser = new JavaScriptParser({
149
+ relativeTo: workspaceDir,
150
+ sourceFileCache: templateSourceFileCache
151
+ });
152
+ const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.ts'});
153
+ cu = (await parseGenerator.next()).value as JS.CompilationUnit;
154
+
155
+ this.cache.set(key, cu);
156
+ return cu;
157
+ }
158
+
159
+ /**
160
+ * Clears the cache.
161
+ */
162
+ clear(): void {
163
+ this.cache.clear();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Cache for compiled templates and patterns.
169
+ * Private to the engine module - encapsulates caching implementation.
170
+ */
171
+ const templateCache = new TemplateCache();
172
+
173
+ /**
174
+ * Clears the template cache. Only exported for testing and benchmarking purposes.
175
+ * Normal application code should not need to call this.
176
+ */
177
+ export function clearTemplateCache(): void {
178
+ templateCache.clear();
39
179
  }
40
180
 
41
181
  /**
@@ -44,49 +184,36 @@ export interface Parameter {
44
184
  */
45
185
  export class TemplateEngine {
46
186
  /**
47
- * Applies a template with optional match results from pattern matching.
187
+ * Gets the parsed and extracted template tree (before value substitution).
188
+ * This is the cacheable part of template processing.
48
189
  *
49
190
  * @param templateParts The string parts of the template
50
191
  * @param parameters The parameters between the string parts
51
- * @param cursor The cursor pointing to the current location in the AST
52
- * @param coordinates The coordinates specifying where and how to insert the generated AST
53
- * @param values Map of capture names to values to replace the parameters with
54
- * @param wrappersMap Map of capture names to J.RightPadded wrappers (for preserving markers)
55
192
  * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
56
193
  * @param dependencies NPM dependencies for type attribution
57
- * @returns A Promise resolving to the generated AST node
194
+ * @returns A Promise resolving to the extracted template AST
58
195
  */
59
- static async applyTemplate(
196
+ static async getTemplateTree(
60
197
  templateParts: TemplateStringsArray,
61
198
  parameters: Parameter[],
62
- cursor: Cursor,
63
- coordinates: JavaCoordinates,
64
- values: Pick<Map<string, J>, 'get'> = new Map(),
65
- wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map(),
66
199
  contextStatements: string[] = [],
67
200
  dependencies: Record<string, string> = {}
68
- ): Promise<J | undefined> {
201
+ ): Promise<J> {
69
202
  // Generate type preamble for captures/parameters with types
70
203
  const preamble = TemplateEngine.generateTypePreamble(parameters);
71
204
 
72
205
  // Build the template string with parameter placeholders
73
206
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
74
207
 
75
- // If the template string is empty, return undefined
76
- if (!templateString.trim()) {
77
- return undefined;
78
- }
79
-
80
208
  // Add preamble to context statements (so they're skipped during extraction)
81
209
  const contextWithPreamble = preamble.length > 0
82
210
  ? [...contextStatements, ...preamble]
83
211
  : contextStatements;
84
212
 
85
213
  // Use cache to get or parse the compilation unit
86
- // For templates, we don't have captures, so use empty array
87
214
  const cu = await templateCache.getOrParse(
88
215
  templateString,
89
- [], // templates don't have captures in the cache key
216
+ [],
90
217
  contextWithPreamble,
91
218
  dependencies
92
219
  );
@@ -98,27 +225,50 @@ export class TemplateEngine {
98
225
 
99
226
  // The template code is always the last statement (after context + preamble)
100
227
  const lastStatement = cu.statements[cu.statements.length - 1].element;
101
- let extracted: J;
102
-
103
- // Check if this is a wrapped template (function __TEMPLATE__() { ... })
104
- if (lastStatement.kind === J.Kind.MethodDeclaration) {
105
- const func = lastStatement as J.MethodDeclaration;
106
- if (func.name.simpleName === '__TEMPLATE__' && func.body) {
107
- // __TEMPLATE__ wrapper indicates the original template was a block.
108
- // Always return the block to preserve the block structure.
109
- extracted = func.body;
110
- } else {
111
- // Not a __TEMPLATE__ wrapper
112
- extracted = lastStatement;
113
- }
114
- } else if (lastStatement.kind === JS.Kind.ExpressionStatement) {
115
- extracted = (lastStatement as JS.ExpressionStatement).expression;
116
- } else {
117
- extracted = lastStatement;
118
- }
228
+
229
+ // Extract from wrapper using shared utility
230
+ const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Template');
119
231
 
120
232
  // Create a copy to avoid sharing cached AST instances
121
- const ast = produce(extracted, _ => {});
233
+ return produce(extracted, _ => {});
234
+ }
235
+
236
+ /**
237
+ * Applies a template with optional match results from pattern matching.
238
+ *
239
+ * @param templateParts The string parts of the template
240
+ * @param parameters The parameters between the string parts
241
+ * @param cursor The cursor pointing to the current location in the AST
242
+ * @param coordinates The coordinates specifying where and how to insert the generated AST
243
+ * @param values Map of capture names to values to replace the parameters with
244
+ * @param wrappersMap Map of capture names to J.RightPadded wrappers (for preserving markers)
245
+ * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
246
+ * @param dependencies NPM dependencies for type attribution
247
+ * @returns A Promise resolving to the generated AST node
248
+ */
249
+ static async applyTemplate(
250
+ templateParts: TemplateStringsArray,
251
+ parameters: Parameter[],
252
+ cursor: Cursor,
253
+ coordinates: JavaCoordinates,
254
+ values: Pick<Map<string, J>, 'get'> = new Map(),
255
+ wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map(),
256
+ contextStatements: string[] = [],
257
+ dependencies: Record<string, string> = {}
258
+ ): Promise<J | undefined> {
259
+ // Build the template string to check if empty
260
+ const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
261
+ if (!templateString.trim()) {
262
+ return undefined;
263
+ }
264
+
265
+ // Get the parsed and extracted template tree
266
+ const ast = await TemplateEngine.getTemplateTree(
267
+ templateParts,
268
+ parameters,
269
+ contextStatements,
270
+ dependencies
271
+ );
122
272
 
123
273
  // Create substitutions map for placeholders
124
274
  const substitutions = new Map<string, Parameter>();
@@ -220,13 +370,9 @@ export class TemplateEngine {
220
370
  }
221
371
  }
222
372
 
223
- // Detect if this is a block template that needs wrapping
224
- const trimmed = result.trim();
225
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
226
- result = `function __TEMPLATE__() ${result}`;
227
- }
228
-
229
- return result;
373
+ // Always wrap in function body - let the parser decide what it is,
374
+ // then we'll extract intelligently based on what was parsed
375
+ return `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
230
376
  }
231
377
 
232
378
  /**
@@ -275,6 +421,182 @@ export class TemplateEngine {
275
421
  // TODO: Implement proper Type to string conversion for other Type.Kind values
276
422
  return 'any';
277
423
  }
424
+
425
+ /**
426
+ * Gets the parsed and extracted pattern tree with capture markers attached.
427
+ * This is the entry point for pattern processing, providing pattern-specific
428
+ * functionality on top of the shared template tree generation.
429
+ *
430
+ * @param templateParts The string parts of the template
431
+ * @param captures The captures between the string parts
432
+ * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
433
+ * @param dependencies NPM dependencies for type attribution
434
+ * @returns A Promise resolving to the extracted pattern AST with capture markers
435
+ */
436
+ static async getPatternTree(
437
+ templateParts: TemplateStringsArray,
438
+ captures: (Capture | Any<any>)[],
439
+ contextStatements: string[] = [],
440
+ dependencies: Record<string, string> = {}
441
+ ): Promise<J> {
442
+ // Generate type preamble for captures with types
443
+ const preamble: string[] = [];
444
+ for (const capture of captures) {
445
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
446
+ const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
447
+ if (captureType) {
448
+ // Convert Type to string if needed
449
+ const typeString = typeof captureType === 'string'
450
+ ? captureType
451
+ : this.typeToString(captureType);
452
+ const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
453
+ preamble.push(`let ${placeholder}: ${typeString};`);
454
+ } else {
455
+ const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
456
+ preamble.push(`let ${placeholder};`);
457
+ }
458
+ }
459
+
460
+ // Build the template string with placeholders for captures
461
+ let result = '';
462
+ for (let i = 0; i < templateParts.length; i++) {
463
+ result += templateParts[i];
464
+ if (i < captures.length) {
465
+ const capture = captures[i];
466
+ // Use symbol to access capture name without triggering Proxy
467
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
468
+ result += PlaceholderUtils.createCapture(captureName, undefined);
469
+ }
470
+ }
471
+
472
+ // Always wrap in function body - let the parser decide what it is,
473
+ // then we'll extract intelligently based on what was parsed
474
+ const templateString = `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
475
+
476
+ // Add preamble to context statements (so they're skipped during extraction)
477
+ const contextWithPreamble = preamble.length > 0
478
+ ? [...contextStatements, ...preamble]
479
+ : contextStatements;
480
+
481
+ // Use cache to get or parse the compilation unit
482
+ const cu = await templateCache.getOrParse(
483
+ templateString,
484
+ captures,
485
+ contextWithPreamble,
486
+ dependencies
487
+ );
488
+
489
+ // Check if there are any statements
490
+ if (!cu.statements || cu.statements.length === 0) {
491
+ throw new Error(`Failed to parse pattern code (no statements):\n${templateString}`);
492
+ }
493
+
494
+ // The pattern code is always the last statement (after context + preamble)
495
+ const lastStatement = cu.statements[cu.statements.length - 1].element;
496
+
497
+ // Extract from wrapper using shared utility
498
+ const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
499
+
500
+ // Attach CaptureMarkers to capture identifiers
501
+ const visitor = new MarkerAttachmentVisitor(captures);
502
+ return (await visitor.visit(extracted, undefined))!;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Visitor that attaches CaptureMarkers to capture identifiers in pattern ASTs.
508
+ * This allows efficient capture detection without string parsing during matching.
509
+ * Used by TemplateEngine.getPatternTree() for pattern-specific processing.
510
+ */
511
+ class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
512
+ constructor(private readonly captures: (Capture | Any<any>)[]) {
513
+ super();
514
+ }
515
+
516
+ /**
517
+ * Attaches CaptureMarker to capture identifiers.
518
+ */
519
+ protected override async visitIdentifier(ident: J.Identifier, p: undefined): Promise<J | undefined> {
520
+ // First call parent to handle standard visitation
521
+ const visited = await super.visitIdentifier(ident, p);
522
+ if (!visited || visited.kind !== J.Kind.Identifier) {
523
+ return visited;
524
+ }
525
+ ident = visited as J.Identifier;
526
+
527
+ // Check if this is a capture placeholder
528
+ if (ident.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
529
+ const captureInfo = PlaceholderUtils.parseCapture(ident.simpleName);
530
+ if (captureInfo) {
531
+ // Find the original capture object to get variadic options and constraint
532
+ const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
533
+ const variadicOptions = captureObj?.getVariadicOptions();
534
+ const constraint = captureObj?.getConstraint?.();
535
+
536
+ // Add CaptureMarker to the Identifier with constraint
537
+ const marker = new CaptureMarker(captureInfo.name, variadicOptions, constraint);
538
+ return updateIfChanged(ident, {
539
+ markers: {
540
+ ...ident.markers,
541
+ markers: [...ident.markers.markers, marker]
542
+ }
543
+ });
544
+ }
545
+ }
546
+
547
+ return ident;
548
+ }
549
+
550
+ /**
551
+ * Propagates markers from element to RightPadded wrapper.
552
+ */
553
+ public override async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: undefined): Promise<J.RightPadded<T>> {
554
+ if (!isTree(right.element)) {
555
+ return right;
556
+ }
557
+
558
+ const visitedElement = await this.visit(right.element as J, p);
559
+ if (visitedElement && visitedElement !== right.element as Tree) {
560
+ return produceAsync<J.RightPadded<T>>(right, async (draft: any) => {
561
+ // Visit element first
562
+ if (right.element && (right.element as any).kind) {
563
+ // Check if element has a CaptureMarker
564
+ const elementMarker = PlaceholderUtils.getCaptureMarker(visitedElement);
565
+ if (elementMarker) {
566
+ draft.markers.markers.push(elementMarker);
567
+ } else {
568
+ draft.element = visitedElement;
569
+ }
570
+ }
571
+ });
572
+ }
573
+
574
+ return right;
575
+ }
576
+
577
+ /**
578
+ * Propagates markers from expression to ExpressionStatement.
579
+ */
580
+ protected override async visitExpressionStatement(expressionStatement: JS.ExpressionStatement, p: undefined): Promise<J | undefined> {
581
+ // Visit the expression
582
+ const visitedExpression = await this.visit(expressionStatement.expression, p);
583
+
584
+ // Check if expression has a CaptureMarker
585
+ const expressionMarker = PlaceholderUtils.getCaptureMarker(visitedExpression as any);
586
+ if (expressionMarker) {
587
+ return updateIfChanged(expressionStatement, {
588
+ markers: {
589
+ ...expressionStatement.markers,
590
+ markers: [...expressionStatement.markers.markers, expressionMarker]
591
+ },
592
+ });
593
+ }
594
+
595
+ // No marker to move, just update with visited expression
596
+ return updateIfChanged(expressionStatement, {
597
+ expression: visitedExpression
598
+ });
599
+ }
278
600
  }
279
601
 
280
602
  /**