@openrewrite/rewrite 8.66.0 → 8.66.1

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 (75) hide show
  1. package/dist/javascript/comparator.d.ts +67 -4
  2. package/dist/javascript/comparator.d.ts.map +1 -1
  3. package/dist/javascript/comparator.js +523 -2794
  4. package/dist/javascript/comparator.js.map +1 -1
  5. package/dist/javascript/format.d.ts.map +1 -1
  6. package/dist/javascript/format.js +4 -3
  7. package/dist/javascript/format.js.map +1 -1
  8. package/dist/javascript/index.d.ts +1 -1
  9. package/dist/javascript/index.d.ts.map +1 -1
  10. package/dist/javascript/index.js +1 -1
  11. package/dist/javascript/index.js.map +1 -1
  12. package/dist/javascript/parser.d.ts.map +1 -1
  13. package/dist/javascript/parser.js +18 -16
  14. package/dist/javascript/parser.js.map +1 -1
  15. package/dist/javascript/templating/capture.d.ts +226 -0
  16. package/dist/javascript/templating/capture.d.ts.map +1 -0
  17. package/dist/javascript/templating/capture.js +371 -0
  18. package/dist/javascript/templating/capture.js.map +1 -0
  19. package/dist/javascript/templating/comparator.d.ts +61 -0
  20. package/dist/javascript/templating/comparator.d.ts.map +1 -0
  21. package/dist/javascript/templating/comparator.js +393 -0
  22. package/dist/javascript/templating/comparator.js.map +1 -0
  23. package/dist/javascript/templating/engine.d.ts +75 -0
  24. package/dist/javascript/templating/engine.d.ts.map +1 -0
  25. package/dist/javascript/templating/engine.js +228 -0
  26. package/dist/javascript/templating/engine.js.map +1 -0
  27. package/dist/javascript/templating/index.d.ts +6 -0
  28. package/dist/javascript/templating/index.d.ts.map +1 -0
  29. package/dist/javascript/templating/index.js +42 -0
  30. package/dist/javascript/templating/index.js.map +1 -0
  31. package/dist/javascript/templating/pattern.d.ts +171 -0
  32. package/dist/javascript/templating/pattern.d.ts.map +1 -0
  33. package/dist/javascript/templating/pattern.js +681 -0
  34. package/dist/javascript/templating/pattern.js.map +1 -0
  35. package/dist/javascript/templating/placeholder-replacement.d.ts +58 -0
  36. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -0
  37. package/dist/javascript/templating/placeholder-replacement.js +365 -0
  38. package/dist/javascript/templating/placeholder-replacement.js.map +1 -0
  39. package/dist/javascript/templating/rewrite.d.ts +39 -0
  40. package/dist/javascript/templating/rewrite.d.ts.map +1 -0
  41. package/dist/javascript/templating/rewrite.js +81 -0
  42. package/dist/javascript/templating/rewrite.js.map +1 -0
  43. package/dist/javascript/templating/template.d.ts +204 -0
  44. package/dist/javascript/templating/template.d.ts.map +1 -0
  45. package/dist/javascript/templating/template.js +293 -0
  46. package/dist/javascript/templating/template.js.map +1 -0
  47. package/dist/javascript/templating/types.d.ts +263 -0
  48. package/dist/javascript/templating/types.d.ts.map +1 -0
  49. package/dist/javascript/templating/types.js +3 -0
  50. package/dist/javascript/templating/types.js.map +1 -0
  51. package/dist/javascript/templating/utils.d.ts +118 -0
  52. package/dist/javascript/templating/utils.d.ts.map +1 -0
  53. package/dist/javascript/templating/utils.js +253 -0
  54. package/dist/javascript/templating/utils.js.map +1 -0
  55. package/dist/version.txt +1 -1
  56. package/package.json +2 -1
  57. package/src/javascript/comparator.ts +554 -3323
  58. package/src/javascript/format.ts +3 -2
  59. package/src/javascript/index.ts +1 -1
  60. package/src/javascript/parser.ts +19 -17
  61. package/src/javascript/templating/capture.ts +503 -0
  62. package/src/javascript/templating/comparator.ts +430 -0
  63. package/src/javascript/templating/engine.ts +252 -0
  64. package/src/javascript/templating/index.ts +60 -0
  65. package/src/javascript/templating/pattern.ts +727 -0
  66. package/src/javascript/templating/placeholder-replacement.ts +372 -0
  67. package/src/javascript/templating/rewrite.ts +95 -0
  68. package/src/javascript/templating/template.ts +326 -0
  69. package/src/javascript/templating/types.ts +300 -0
  70. package/src/javascript/templating/utils.ts +284 -0
  71. package/dist/javascript/templating.d.ts +0 -265
  72. package/dist/javascript/templating.d.ts.map +0 -1
  73. package/dist/javascript/templating.js +0 -1027
  74. package/dist/javascript/templating.js.map +0 -1
  75. package/src/javascript/templating.ts +0 -1226
@@ -0,0 +1,430 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {Cursor, Tree} from '../..';
17
+ import {J} from '../../java';
18
+ import {JS} from '../index';
19
+ import {JavaScriptSemanticComparatorVisitor} from '../comparator';
20
+ import {PlaceholderUtils, CaptureStorageValue} from './utils';
21
+
22
+ /**
23
+ * A comparator for pattern matching that is lenient about optional properties.
24
+ * Allows patterns without type annotations to match actual code with type annotations.
25
+ * Uses semantic comparison to match semantically equivalent code (e.g., isDate() and util.isDate()).
26
+ */
27
+ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisitor {
28
+ constructor(
29
+ private readonly matcher: {
30
+ handleCapture: (pattern: J, target: J, wrapper?: J.RightPadded<J>) => boolean;
31
+ handleVariadicCapture: (pattern: J, targets: J[], wrappers?: J.RightPadded<J>[]) => boolean;
32
+ saveState: () => Map<string, CaptureStorageValue>;
33
+ restoreState: (state: Map<string, CaptureStorageValue>) => void;
34
+ },
35
+ lenientTypeMatching: boolean = true
36
+ ) {
37
+ // Enable lenient type matching based on pattern configuration (default: true for backward compatibility)
38
+ super(lenientTypeMatching);
39
+ }
40
+
41
+ /**
42
+ * Extracts the wrapper from the cursor if the parent is a RightPadded.
43
+ */
44
+ private getWrapperFromCursor(cursor: Cursor): J.RightPadded<J> | undefined {
45
+ if (!cursor.parent) {
46
+ return undefined;
47
+ }
48
+ const parent = cursor.parent.value;
49
+ // Check if parent is a RightPadded by checking its kind
50
+ if ((parent as any).kind === J.Kind.RightPadded) {
51
+ return parent as J.RightPadded<J>;
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
57
+ // Check if the pattern node is a capture - this handles captures anywhere in the tree
58
+ if (PlaceholderUtils.isCapture(j as J)) {
59
+ const wrapper = this.getWrapperFromCursor(this.cursor);
60
+ const success = this.matcher.handleCapture(j as J, p, wrapper);
61
+ if (!success) {
62
+ return this.abort(j) as R;
63
+ }
64
+ return j as R;
65
+ }
66
+
67
+ if (!this.match) {
68
+ return j as R;
69
+ }
70
+
71
+ return super.visit(j, p, parent);
72
+ }
73
+
74
+ protected hasSameKind(j: J, other: J): boolean {
75
+ return super.hasSameKind(j, other) ||
76
+ (j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
77
+ }
78
+
79
+ override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
80
+ if (PlaceholderUtils.isCapture(identifier)) {
81
+ const wrapper = this.getWrapperFromCursor(this.cursor);
82
+ const success = this.matcher.handleCapture(identifier, other, wrapper);
83
+ return success ? identifier : this.abort(identifier);
84
+ }
85
+ return super.visitIdentifier(identifier, other);
86
+ }
87
+
88
+ override async visitMethodInvocation(methodInvocation: J.MethodInvocation, other: J): Promise<J | undefined> {
89
+ // Check if any arguments are variadic captures
90
+ const hasVariadicCapture = methodInvocation.arguments.elements.some(arg =>
91
+ PlaceholderUtils.isVariadicCapture(arg.element)
92
+ );
93
+
94
+ // If no variadic captures, use parent implementation (which includes semantic/type-aware matching)
95
+ if (!hasVariadicCapture) {
96
+ return super.visitMethodInvocation(methodInvocation, other);
97
+ }
98
+
99
+ // Otherwise, handle variadic captures ourselves
100
+ if (!this.match || other.kind !== J.Kind.MethodInvocation) {
101
+ return this.abort(methodInvocation);
102
+ }
103
+
104
+ const otherMethodInvocation = other as J.MethodInvocation;
105
+
106
+ // Compare select
107
+ if ((methodInvocation.select === undefined) !== (otherMethodInvocation.select === undefined)) {
108
+ return this.abort(methodInvocation);
109
+ }
110
+
111
+ // Visit select if present
112
+ if (methodInvocation.select && otherMethodInvocation.select) {
113
+ await this.visit(methodInvocation.select.element, otherMethodInvocation.select.element);
114
+ if (!this.match) return methodInvocation;
115
+ }
116
+
117
+ // Compare typeParameters
118
+ if ((methodInvocation.typeParameters === undefined) !== (otherMethodInvocation.typeParameters === undefined)) {
119
+ return this.abort(methodInvocation);
120
+ }
121
+
122
+ // Visit typeParameters if present
123
+ if (methodInvocation.typeParameters && otherMethodInvocation.typeParameters) {
124
+ if (methodInvocation.typeParameters.elements.length !== otherMethodInvocation.typeParameters.elements.length) {
125
+ return this.abort(methodInvocation);
126
+ }
127
+
128
+ // Visit each type parameter in lock step
129
+ for (let i = 0; i < methodInvocation.typeParameters.elements.length; i++) {
130
+ await this.visit(methodInvocation.typeParameters.elements[i].element, otherMethodInvocation.typeParameters.elements[i].element);
131
+ if (!this.match) return methodInvocation;
132
+ }
133
+ }
134
+
135
+ // Visit name
136
+ await this.visit(methodInvocation.name, otherMethodInvocation.name);
137
+ if (!this.match) return methodInvocation;
138
+
139
+ // Special handling for variadic captures in arguments
140
+ if (!await this.matchArguments(methodInvocation.arguments.elements, otherMethodInvocation.arguments.elements)) {
141
+ return this.abort(methodInvocation);
142
+ }
143
+
144
+ return methodInvocation;
145
+ }
146
+
147
+ override async visitBlock(block: J.Block, other: J): Promise<J | undefined> {
148
+ // Check if any statements have CaptureMarker indicating they're variadic
149
+ const hasVariadicCapture = block.statements.some(stmt => {
150
+ const captureMarker = PlaceholderUtils.getCaptureMarker(stmt.element);
151
+ return captureMarker?.variadicOptions !== undefined;
152
+ });
153
+
154
+ // If no variadic captures, use parent implementation
155
+ if (!hasVariadicCapture) {
156
+ return super.visitBlock(block, other);
157
+ }
158
+
159
+ // Otherwise, handle variadic captures ourselves
160
+ if (!this.match || other.kind !== J.Kind.Block) {
161
+ return this.abort(block);
162
+ }
163
+
164
+ const otherBlock = other as J.Block;
165
+
166
+ // Special handling for variadic captures in statements
167
+ if (!await this.matchSequence(block.statements, otherBlock.statements, false)) {
168
+ return this.abort(block);
169
+ }
170
+
171
+ return block;
172
+ }
173
+
174
+ override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, other: J): Promise<J | undefined> {
175
+ // Check if any statements are variadic captures (unwrap ExpressionStatement wrappers first)
176
+ const hasVariadicCapture = compilationUnit.statements.some(stmt => {
177
+ const unwrapped = PlaceholderUtils.unwrapStatementCapture(stmt.element);
178
+ return PlaceholderUtils.isVariadicCapture(unwrapped);
179
+ });
180
+
181
+ // If no variadic captures, use parent implementation
182
+ if (!hasVariadicCapture) {
183
+ return super.visitJsCompilationUnit(compilationUnit, other);
184
+ }
185
+
186
+ // Otherwise, handle variadic captures ourselves
187
+ if (!this.match || other.kind !== JS.Kind.CompilationUnit) {
188
+ return this.abort(compilationUnit);
189
+ }
190
+
191
+ const otherCompilationUnit = other as JS.CompilationUnit;
192
+
193
+ // Special handling for variadic captures in top-level statements
194
+ if (!await this.matchSequence(compilationUnit.statements, otherCompilationUnit.statements, false)) {
195
+ return this.abort(compilationUnit);
196
+ }
197
+
198
+ return compilationUnit;
199
+ }
200
+
201
+ /**
202
+ * Matches argument lists, with special handling for variadic captures.
203
+ * A variadic capture can match zero or more consecutive arguments.
204
+ */
205
+ private async matchArguments(patternArgs: J.RightPadded<J>[], targetArgs: J.RightPadded<J>[]): Promise<boolean> {
206
+ return this.matchSequence(patternArgs, targetArgs, true);
207
+ }
208
+
209
+ /**
210
+ * Generic sequence matching with variadic capture support.
211
+ * Works for any sequence of JRightPadded elements (arguments, statements, etc.).
212
+ * A variadic capture can match zero or more consecutive elements.
213
+ *
214
+ * Uses pivot detection to optimize matching, with backtracking as fallback.
215
+ *
216
+ * @param patternElements The pattern elements (JRightPadded)
217
+ * @param targetElements The target elements to match against (JRightPadded)
218
+ * @param filterEmpty Whether to filter out J.Empty elements when capturing (true for arguments, false for statements)
219
+ * @returns true if the sequence matches, false otherwise
220
+ */
221
+ private async matchSequence(patternElements: J.RightPadded<J>[], targetElements: J.RightPadded<J>[], filterEmpty: boolean): Promise<boolean> {
222
+ return await this.matchSequenceOptimized(patternElements, targetElements, 0, 0, filterEmpty);
223
+ }
224
+
225
+ /**
226
+ * Optimized sequence matcher with pivot detection and backtracking.
227
+ * For variadic patterns, tries to detect pivots (where next pattern matches) to avoid
228
+ * unnecessary backtracking. Falls back to full backtracking when pivots are ambiguous.
229
+ *
230
+ * @param patternElements The pattern elements (JRightPadded)
231
+ * @param targetElements The target elements to match against (JRightPadded)
232
+ * @param patternIdx Current position in pattern
233
+ * @param targetIdx Current position in target
234
+ * @param filterEmpty Whether to filter out J.Empty elements when capturing
235
+ * @returns true if the remaining sequence matches, false otherwise
236
+ */
237
+ private async matchSequenceOptimized(
238
+ patternElements: J.RightPadded<J>[],
239
+ targetElements: J.RightPadded<J>[],
240
+ patternIdx: number,
241
+ targetIdx: number,
242
+ filterEmpty: boolean
243
+ ): Promise<boolean> {
244
+ // Base case: all patterns matched
245
+ if (patternIdx >= patternElements.length) {
246
+ return targetIdx >= targetElements.length; // Success if all targets consumed
247
+ }
248
+
249
+ const patternElement = patternElements[patternIdx].element;
250
+ const captureMarker = PlaceholderUtils.getCaptureMarker(patternElement);
251
+ const isVariadic = captureMarker?.variadicOptions !== undefined;
252
+
253
+ if (isVariadic) {
254
+ // Variadic pattern: try different consumption amounts with backtracking
255
+ const variadicOptions = captureMarker!.variadicOptions;
256
+ const min = variadicOptions?.min ?? 0;
257
+ const max = variadicOptions?.max ?? Infinity;
258
+
259
+ // Calculate maximum possible consumption
260
+ let nonVariadicRemainingPatterns = 0;
261
+ for (let i = patternIdx + 1; i < patternElements.length; i++) {
262
+ const nextPatternElement = patternElements[i].element;
263
+ const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(nextPatternElement);
264
+ const nextIsVariadic = nextCaptureMarker?.variadicOptions !== undefined;
265
+ if (!nextIsVariadic) {
266
+ nonVariadicRemainingPatterns++;
267
+ }
268
+ }
269
+ const remainingTargetElements = targetElements.length - targetIdx;
270
+ const maxPossible = Math.min(remainingTargetElements - nonVariadicRemainingPatterns, max);
271
+
272
+ // Pivot detection optimization: try to find where next pattern matches
273
+ // This avoids unnecessary backtracking when constraints make the split point obvious
274
+ let pivotDetected = false;
275
+ let pivotAt = -1;
276
+
277
+ if (patternIdx + 1 < patternElements.length && min <= maxPossible) {
278
+ const nextPattern = patternElements[patternIdx + 1].element;
279
+
280
+ // Scan through possible consumption amounts starting from min
281
+ for (let tryConsume = min; tryConsume <= maxPossible; tryConsume++) {
282
+ // Check if element after our consumption would match next pattern
283
+ if (targetIdx + tryConsume < targetElements.length) {
284
+ const candidateElement = targetElements[targetIdx + tryConsume].element;
285
+
286
+ // Skip J.Empty for arguments
287
+ if (filterEmpty && candidateElement.kind === J.Kind.Empty) {
288
+ continue;
289
+ }
290
+
291
+ // Test if next pattern matches this element
292
+ const savedMatch = this.match;
293
+ const savedState = this.matcher.saveState();
294
+
295
+ await this.visit(nextPattern, candidateElement);
296
+ const matchesNext = this.match;
297
+
298
+ this.match = savedMatch;
299
+ this.matcher.restoreState(savedState);
300
+
301
+ if (matchesNext) {
302
+ // Found pivot! Try this consumption amount first
303
+ pivotDetected = true;
304
+ pivotAt = tryConsume;
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ // Try different consumption amounts
312
+ // If pivot detected, try that first; otherwise use greedy approach (max to min)
313
+ const consumptionOrder: number[] = [];
314
+ if (pivotDetected && pivotAt >= 0) {
315
+ // Try pivot first, then others as fallback
316
+ consumptionOrder.push(pivotAt);
317
+ for (let c = maxPossible; c >= min; c--) {
318
+ if (c !== pivotAt) {
319
+ consumptionOrder.push(c);
320
+ }
321
+ }
322
+ } else {
323
+ // Greedy approach: max to min
324
+ for (let c = maxPossible; c >= min; c--) {
325
+ consumptionOrder.push(c);
326
+ }
327
+ }
328
+
329
+ for (const consume of consumptionOrder) {
330
+ // Capture elements for this consumption amount
331
+ const capturedWrappers: J.RightPadded<J>[] = [];
332
+ for (let i = 0; i < consume; i++) {
333
+ const wrapped = targetElements[targetIdx + i];
334
+ const element = wrapped.element;
335
+ // For arguments, filter out J.Empty as it represents an empty argument list
336
+ // For statements, include all elements
337
+ if (!filterEmpty || element.kind !== J.Kind.Empty) {
338
+ capturedWrappers.push(wrapped);
339
+ }
340
+ }
341
+
342
+ // Extract just the elements for the constraint check
343
+ const capturedElements: J[] = capturedWrappers.map(w => w.element);
344
+
345
+ // Re-check min/max constraints against actual captured elements (after filtering if applicable)
346
+ if (capturedElements.length < min || capturedElements.length > max) {
347
+ continue; // Try next consumption amount
348
+ }
349
+
350
+ // Save current state for backtracking
351
+ const savedState = this.matcher.saveState();
352
+
353
+ // Handle the variadic capture
354
+ const success = this.matcher.handleVariadicCapture(patternElement, capturedElements, capturedWrappers);
355
+ if (!success) {
356
+ // Restore state and try next amount
357
+ this.matcher.restoreState(savedState);
358
+ continue;
359
+ }
360
+
361
+ // Try to match the rest of the pattern
362
+ const restMatches = await this.matchSequenceOptimized(
363
+ patternElements,
364
+ targetElements,
365
+ patternIdx + 1,
366
+ targetIdx + consume,
367
+ filterEmpty
368
+ );
369
+
370
+ if (restMatches) {
371
+ return true; // Found a valid matching
372
+ }
373
+
374
+ // Backtrack: restore state and try next amount
375
+ this.matcher.restoreState(savedState);
376
+ }
377
+
378
+ return false; // No consumption amount worked
379
+ } else {
380
+ // Regular non-variadic element - must match exactly one target element
381
+ if (targetIdx >= targetElements.length) {
382
+ return false; // Pattern has more elements than target
383
+ }
384
+
385
+ const targetWrapper = targetElements[targetIdx];
386
+ const targetElement = targetWrapper.element;
387
+
388
+ // For arguments, J.Empty represents no argument, so regular captures should not match it
389
+ if (filterEmpty && targetElement.kind === J.Kind.Empty) {
390
+ return false;
391
+ }
392
+
393
+ // Save current state for backtracking (both match state and capture bindings)
394
+ const savedMatch = this.match;
395
+ const savedState = this.matcher.saveState();
396
+
397
+ // Push wrapper onto cursor so captures can access it
398
+ const savedCursor = this.cursor;
399
+ this.cursor = new Cursor(targetWrapper, this.cursor);
400
+ try {
401
+ await this.visit(patternElement, targetElement);
402
+ } finally {
403
+ this.cursor = savedCursor;
404
+ }
405
+ if (!this.match) {
406
+ // Restore state on match failure
407
+ this.match = savedMatch;
408
+ this.matcher.restoreState(savedState);
409
+ return false;
410
+ }
411
+
412
+ // Continue matching the rest
413
+ const restMatches = await this.matchSequenceOptimized(
414
+ patternElements,
415
+ targetElements,
416
+ patternIdx + 1,
417
+ targetIdx + 1,
418
+ filterEmpty
419
+ );
420
+
421
+ if (!restMatches) {
422
+ // Restore full state on backtracking failure
423
+ this.match = savedMatch;
424
+ this.matcher.restoreState(savedState);
425
+ }
426
+
427
+ return restMatches;
428
+ }
429
+ }
430
+ }
@@ -0,0 +1,252 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {Cursor, isTree} from '../..';
17
+ import {J} from '../../java';
18
+ import {JS} from '..';
19
+ import {produce} from 'immer';
20
+ import {TemplateCache, PlaceholderUtils} from './utils';
21
+ import {CaptureImpl, TemplateParamImpl, CaptureValue, CAPTURE_NAME_SYMBOL} from './capture';
22
+ import {PlaceholderReplacementVisitor} from './placeholder-replacement';
23
+ import {JavaCoordinates} from './template';
24
+
25
+ /**
26
+ * Cache for compiled templates.
27
+ */
28
+ const templateCache = new TemplateCache();
29
+
30
+ /**
31
+ * Parameter specification for template generation.
32
+ * Represents a placeholder in a template that will be replaced with a parameter value.
33
+ */
34
+ export interface Parameter {
35
+ /**
36
+ * The value to substitute into the template.
37
+ */
38
+ value: any;
39
+ }
40
+
41
+ /**
42
+ * Internal template engine - handles the core templating logic.
43
+ * Not exported from index, so only visible within the templating module.
44
+ */
45
+ export class TemplateEngine {
46
+ /**
47
+ * Applies a template with optional match results from pattern matching.
48
+ *
49
+ * @param templateParts The string parts of the template
50
+ * @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
+ * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
56
+ * @param dependencies NPM dependencies for type attribution
57
+ * @returns A Promise resolving to the generated AST node
58
+ */
59
+ static async applyTemplate(
60
+ templateParts: TemplateStringsArray,
61
+ 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
+ contextStatements: string[] = [],
67
+ dependencies: Record<string, string> = {}
68
+ ): Promise<J | undefined> {
69
+ // Build the template string with parameter placeholders
70
+ const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
71
+
72
+ // If the template string is empty, return undefined
73
+ if (!templateString.trim()) {
74
+ return undefined;
75
+ }
76
+
77
+ // Use cache to get or parse the compilation unit
78
+ // For templates, we don't have captures, so use empty array
79
+ const cu = await templateCache.getOrParse(
80
+ templateString,
81
+ [], // templates don't have captures in the cache key
82
+ contextStatements,
83
+ dependencies
84
+ );
85
+
86
+ // Check if there are any statements
87
+ if (!cu.statements || cu.statements.length === 0) {
88
+ throw new Error(`Failed to parse template code (no statements):\n${templateString}`);
89
+ }
90
+
91
+ // Skip context statements to get to the actual template code
92
+ const templateStatementIndex = contextStatements.length;
93
+ if (templateStatementIndex >= cu.statements.length) {
94
+ return undefined;
95
+ }
96
+
97
+ // Extract the relevant part of the AST
98
+ const firstStatement = cu.statements[templateStatementIndex].element;
99
+ let extracted: J;
100
+
101
+ // Check if this is a wrapped template (function __TEMPLATE__() { ... })
102
+ if (firstStatement.kind === J.Kind.MethodDeclaration) {
103
+ const func = firstStatement as J.MethodDeclaration;
104
+ if (func.name.simpleName === '__TEMPLATE__' && func.body) {
105
+ // __TEMPLATE__ wrapper indicates the original template was a block.
106
+ // Always return the block to preserve the block structure.
107
+ extracted = func.body;
108
+ } else {
109
+ // Not a __TEMPLATE__ wrapper
110
+ extracted = firstStatement;
111
+ }
112
+ } else if (firstStatement.kind === JS.Kind.ExpressionStatement) {
113
+ extracted = (firstStatement as JS.ExpressionStatement).expression;
114
+ } else {
115
+ extracted = firstStatement;
116
+ }
117
+
118
+ // Create a copy to avoid sharing cached AST instances
119
+ const ast = produce(extracted, _ => {});
120
+
121
+ // Create substitutions map for placeholders
122
+ const substitutions = new Map<string, Parameter>();
123
+ for (let i = 0; i < parameters.length; i++) {
124
+ const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
125
+ substitutions.set(placeholder, parameters[i]);
126
+ }
127
+
128
+ // Unsubstitute placeholders with actual parameter values and match results
129
+ const visitor = new PlaceholderReplacementVisitor(substitutions, values, wrappersMap);
130
+ const unsubstitutedAst = (await visitor.visit(ast, null))!;
131
+
132
+ // Apply the template to the current AST
133
+ return new TemplateApplier(cursor, coordinates, unsubstitutedAst).apply();
134
+ }
135
+
136
+ /**
137
+ * Builds a template string with parameter placeholders.
138
+ *
139
+ * @param templateParts The string parts of the template
140
+ * @param parameters The parameters between the string parts
141
+ * @returns The template string
142
+ */
143
+ private static buildTemplateString(
144
+ templateParts: TemplateStringsArray,
145
+ parameters: Parameter[]
146
+ ): string {
147
+ let result = '';
148
+ for (let i = 0; i < templateParts.length; i++) {
149
+ result += templateParts[i];
150
+ if (i < parameters.length) {
151
+ const param = parameters[i].value;
152
+ // Use a placeholder for Captures, TemplateParams, CaptureValues, Tree nodes, and Tree arrays
153
+ // Inline everything else (strings, numbers, booleans) directly
154
+ // Check for Capture (could be a Proxy, so check for symbol property)
155
+ const isCapture = param instanceof CaptureImpl ||
156
+ (param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL]);
157
+ const isTemplateParam = param instanceof TemplateParamImpl;
158
+ const isCaptureValue = param instanceof CaptureValue;
159
+ const isTreeArray = Array.isArray(param) && param.length > 0 && isTree(param[0]);
160
+ if (isCapture || isTemplateParam || isCaptureValue || isTree(param) || isTreeArray) {
161
+ const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
162
+ result += placeholder;
163
+ } else {
164
+ result += param;
165
+ }
166
+ }
167
+ }
168
+
169
+ // Detect if this is a block template that needs wrapping
170
+ const trimmed = result.trim();
171
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
172
+ result = `function __TEMPLATE__() ${result}`;
173
+ }
174
+
175
+ return result;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Helper class for applying a template to an AST.
181
+ */
182
+ export class TemplateApplier {
183
+ constructor(
184
+ private readonly cursor: Cursor,
185
+ private readonly coordinates: JavaCoordinates,
186
+ private readonly ast: J
187
+ ) {
188
+ }
189
+
190
+ /**
191
+ * Applies the template to the current AST.
192
+ *
193
+ * @returns A Promise resolving to the modified AST
194
+ */
195
+ async apply(): Promise<J | undefined> {
196
+ const {loc} = this.coordinates;
197
+
198
+ // Apply the template based on the location and mode
199
+ switch (loc || 'EXPRESSION_PREFIX') {
200
+ case 'EXPRESSION_PREFIX':
201
+ return this.applyToExpression();
202
+ case 'STATEMENT_PREFIX':
203
+ return this.applyToStatement();
204
+ case 'BLOCK_END':
205
+ return this.applyToBlock();
206
+ default:
207
+ throw new Error(`Unsupported location: ${loc}`);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Applies the template to an expression.
213
+ *
214
+ * @returns A Promise resolving to the modified AST
215
+ */
216
+ private async applyToExpression(): Promise<J | undefined> {
217
+ const {tree} = this.coordinates;
218
+
219
+ // Create a copy of the AST with the prefix from the target
220
+ return tree ? produce(this.ast, draft => {
221
+ draft.prefix = (tree as J).prefix;
222
+ }) : this.ast;
223
+ }
224
+
225
+ /**
226
+ * Applies the template to a statement.
227
+ *
228
+ * @returns A Promise resolving to the modified AST
229
+ */
230
+ private async applyToStatement(): Promise<J | undefined> {
231
+ const {tree} = this.coordinates;
232
+
233
+ // Create a copy of the AST with the prefix from the target
234
+ return produce(this.ast, draft => {
235
+ draft.prefix = (tree as J).prefix;
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Applies the template to a block.
241
+ *
242
+ * @returns A Promise resolving to the modified AST
243
+ */
244
+ private async applyToBlock(): Promise<J | undefined> {
245
+ const {tree} = this.coordinates;
246
+
247
+ // Create a copy of the AST with the prefix from the target
248
+ return produce(this.ast, draft => {
249
+ draft.prefix = (tree as J).prefix;
250
+ });
251
+ }
252
+ }