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

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 (44) 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 +41 -25
  10. package/dist/javascript/templating/engine.d.ts.map +1 -1
  11. package/dist/javascript/templating/engine.js +378 -92
  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/rewrite.d.ts +17 -11
  20. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  21. package/dist/javascript/templating/rewrite.js +17 -11
  22. package/dist/javascript/templating/rewrite.js.map +1 -1
  23. package/dist/javascript/templating/template.d.ts +17 -3
  24. package/dist/javascript/templating/template.d.ts.map +1 -1
  25. package/dist/javascript/templating/template.js +45 -5
  26. package/dist/javascript/templating/template.js.map +1 -1
  27. package/dist/javascript/templating/types.d.ts +36 -13
  28. package/dist/javascript/templating/types.d.ts.map +1 -1
  29. package/dist/javascript/templating/utils.d.ts +41 -22
  30. package/dist/javascript/templating/utils.d.ts.map +1 -1
  31. package/dist/javascript/templating/utils.js +111 -76
  32. package/dist/javascript/templating/utils.js.map +1 -1
  33. package/dist/version.txt +1 -1
  34. package/package.json +3 -1
  35. package/src/java/tree.ts +2 -0
  36. package/src/javascript/assertions.ts +1 -1
  37. package/src/javascript/format.ts +1 -1
  38. package/src/javascript/templating/engine.ts +439 -105
  39. package/src/javascript/templating/pattern.ts +55 -322
  40. package/src/javascript/templating/placeholder-replacement.ts +1 -1
  41. package/src/javascript/templating/rewrite.ts +17 -11
  42. package/src/javascript/templating/template.ts +66 -11
  43. package/src/javascript/templating/types.ts +37 -13
  44. package/src/javascript/templating/utils.ts +114 -81
@@ -13,29 +13,170 @@
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 '../..';
17
- import {J, Type} from '../../java';
18
- import {JS} from '..';
16
+ import {Cursor, isTree, produceAsync, Tree, updateIfChanged} from '../..';
17
+ import {emptySpace, J, Statement, Type} from '../../java';
18
+ import {Any, Capture, JavaScriptParser, JavaScriptVisitor, JS} from '..';
19
19
  import {produce} from 'immer';
20
- import {PlaceholderUtils, TemplateCache} from './utils';
21
- import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, TemplateParamImpl} from './capture';
20
+ import {CaptureMarker, PlaceholderUtils, WRAPPER_FUNCTION_NAME} from './utils';
21
+ import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue} from './capture';
22
22
  import {PlaceholderReplacementVisitor} from './placeholder-replacement';
23
23
  import {JavaCoordinates} from './template';
24
+ import {maybeAutoFormat} from '../format';
25
+ import {isExpression, isStatement} from '../parser-utils';
26
+ import {randomId} from '../../uuid';
27
+ import ts from "typescript";
28
+ import {DependencyWorkspace} from "../dependency-workspace";
29
+ import {Parameter} from "./types";
24
30
 
25
31
  /**
26
- * Cache for compiled templates.
32
+ * Simple LRU (Least Recently Used) cache implementation.
33
+ * Used for template/pattern compilation caching with bounded memory usage.
27
34
  */
28
- const templateCache = new TemplateCache();
35
+ class LRUCache<K, V> {
36
+ private cache = new Map<K, V>();
37
+
38
+ constructor(private maxSize: number) {}
39
+
40
+ get(key: K): V | undefined {
41
+ const value = this.cache.get(key);
42
+ if (value !== undefined) {
43
+ // Move to end (most recently used)
44
+ this.cache.delete(key);
45
+ this.cache.set(key, value);
46
+ }
47
+ return value;
48
+ }
49
+
50
+ set(key: K, value: V): void {
51
+ // Remove if exists (to update position)
52
+ this.cache.delete(key);
53
+
54
+ // Add to end
55
+ this.cache.set(key, value);
56
+
57
+ // Evict oldest if over capacity
58
+ if (this.cache.size > this.maxSize) {
59
+ const iterator = this.cache.keys();
60
+ const firstEntry = iterator.next();
61
+ if (!firstEntry.done) {
62
+ this.cache.delete(firstEntry.value);
63
+ }
64
+ }
65
+ }
66
+
67
+ clear(): void {
68
+ this.cache.clear();
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Module-level TypeScript sourceFileCache for template parsing.
74
+ */
75
+ let templateSourceFileCache: Map<string, ts.SourceFile> | undefined;
29
76
 
30
77
  /**
31
- * Parameter specification for template generation.
32
- * Represents a placeholder in a template that will be replaced with a parameter value.
78
+ * Configure the sourceFileCache used for template parsing.
79
+ *
80
+ * @param cache The sourceFileCache to use, or undefined to disable caching
33
81
  */
34
- export interface Parameter {
82
+ export function setTemplateSourceFileCache(cache?: Map<string, ts.SourceFile>): void {
83
+ templateSourceFileCache = cache;
84
+ }
85
+
86
+ /**
87
+ * Cache for compiled templates and patterns.
88
+ * Stores parsed ASTs to avoid expensive re-parsing and dependency resolution.
89
+ * Bounded to 100 entries using LRU eviction to prevent unbounded memory growth.
90
+ */
91
+ class TemplateCache {
92
+ private cache = new LRUCache<string, JS.CompilationUnit>(100);
93
+
35
94
  /**
36
- * The value to substitute into the template.
95
+ * Generates a cache key from template string, captures, and options.
37
96
  */
38
- value: any;
97
+ private generateKey(
98
+ templateString: string,
99
+ captures: (Capture | Any)[],
100
+ contextStatements: string[],
101
+ dependencies: Record<string, string>
102
+ ): string {
103
+ // Use the actual template string (with placeholders) as the primary key
104
+ const templateKey = templateString;
105
+
106
+ // Capture names
107
+ const capturesKey = captures.map(c => c.getName()).join(',');
108
+
109
+ // Context statements
110
+ const contextKey = contextStatements.join(';');
111
+
112
+ // Dependencies
113
+ const depsKey = JSON.stringify(dependencies || {});
114
+
115
+ return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}`;
116
+ }
117
+
118
+ /**
119
+ * Gets a cached compilation unit or creates and caches a new one.
120
+ */
121
+ async getOrParse(
122
+ templateString: string,
123
+ captures: (Capture | Any)[],
124
+ contextStatements: string[],
125
+ dependencies: Record<string, string>
126
+ ): Promise<JS.CompilationUnit> {
127
+ const key = this.generateKey(templateString, captures, contextStatements, dependencies);
128
+
129
+ let cu = this.cache.get(key);
130
+ if (cu) {
131
+ return cu;
132
+ }
133
+
134
+ // Create workspace if dependencies are provided
135
+ // DependencyWorkspace has its own cache, so multiple templates with
136
+ // the same dependencies will automatically share the same workspace
137
+ let workspaceDir: string | undefined;
138
+ if (dependencies && Object.keys(dependencies).length > 0) {
139
+ workspaceDir = await DependencyWorkspace.getOrCreateWorkspace(dependencies);
140
+ }
141
+
142
+ // Prepend context statements for type attribution context
143
+ const fullTemplateString = contextStatements.length > 0
144
+ ? contextStatements.join('\n') + '\n' + templateString
145
+ : templateString;
146
+
147
+ // Parse and cache (workspace only needed during parsing)
148
+ // Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
149
+ const parser = new JavaScriptParser({
150
+ relativeTo: workspaceDir,
151
+ sourceFileCache: templateSourceFileCache
152
+ });
153
+ const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.ts'});
154
+ cu = (await parseGenerator.next()).value as JS.CompilationUnit;
155
+
156
+ this.cache.set(key, cu);
157
+ return cu;
158
+ }
159
+
160
+ /**
161
+ * Clears the cache.
162
+ */
163
+ clear(): void {
164
+ this.cache.clear();
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Cache for compiled templates and patterns.
170
+ * Private to the engine module - encapsulates caching implementation.
171
+ */
172
+ const templateCache = new TemplateCache();
173
+
174
+ /**
175
+ * Clears the template cache. Only exported for testing and benchmarking purposes.
176
+ * Normal application code should not need to call this.
177
+ */
178
+ export function clearTemplateCache(): void {
179
+ templateCache.clear();
39
180
  }
40
181
 
41
182
  /**
@@ -44,49 +185,36 @@ export interface Parameter {
44
185
  */
45
186
  export class TemplateEngine {
46
187
  /**
47
- * Applies a template with optional match results from pattern matching.
188
+ * Gets the parsed and extracted template tree (before value substitution).
189
+ * This is the cacheable part of template processing.
48
190
  *
49
191
  * @param templateParts The string parts of the template
50
192
  * @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
193
  * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
56
194
  * @param dependencies NPM dependencies for type attribution
57
- * @returns A Promise resolving to the generated AST node
195
+ * @returns A Promise resolving to the extracted template AST
58
196
  */
59
- static async applyTemplate(
197
+ static async getTemplateTree(
60
198
  templateParts: TemplateStringsArray,
61
199
  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
200
  contextStatements: string[] = [],
67
201
  dependencies: Record<string, string> = {}
68
- ): Promise<J | undefined> {
202
+ ): Promise<J> {
69
203
  // Generate type preamble for captures/parameters with types
70
204
  const preamble = TemplateEngine.generateTypePreamble(parameters);
71
205
 
72
206
  // Build the template string with parameter placeholders
73
207
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
74
208
 
75
- // If the template string is empty, return undefined
76
- if (!templateString.trim()) {
77
- return undefined;
78
- }
79
-
80
209
  // Add preamble to context statements (so they're skipped during extraction)
81
210
  const contextWithPreamble = preamble.length > 0
82
211
  ? [...contextStatements, ...preamble]
83
212
  : contextStatements;
84
213
 
85
214
  // Use cache to get or parse the compilation unit
86
- // For templates, we don't have captures, so use empty array
87
215
  const cu = await templateCache.getOrParse(
88
216
  templateString,
89
- [], // templates don't have captures in the cache key
217
+ [],
90
218
  contextWithPreamble,
91
219
  dependencies
92
220
  );
@@ -98,28 +226,33 @@ export class TemplateEngine {
98
226
 
99
227
  // The template code is always the last statement (after context + preamble)
100
228
  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
- }
119
229
 
120
- // Create a copy to avoid sharing cached AST instances
121
- const ast = produce(extracted, _ => {});
230
+ // Extract from wrapper using shared utility
231
+ const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Template');
122
232
 
233
+ return produce(extracted, _ => {});
234
+ }
235
+
236
+ /**
237
+ * Applies a template from a pre-parsed AST and returns the resulting AST.
238
+ * This method is used by Template.apply() after getting the cached template tree.
239
+ *
240
+ * @param ast The pre-parsed template AST
241
+ * @param parameters The parameters between the string parts
242
+ * @param cursor The cursor pointing to the current location in the AST
243
+ * @param coordinates The coordinates specifying where and how to insert the generated AST
244
+ * @param values Map of capture names to values to replace the parameters with
245
+ * @param wrappersMap Map of capture names to J.RightPadded wrappers (for preserving markers)
246
+ * @returns A Promise resolving to the generated AST node
247
+ */
248
+ static async applyTemplateFromAst(
249
+ ast: JS.CompilationUnit,
250
+ parameters: Parameter[],
251
+ cursor: Cursor,
252
+ coordinates: JavaCoordinates,
253
+ values: Pick<Map<string, J>, 'get'> = new Map(),
254
+ wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map()
255
+ ): Promise<J | undefined> {
123
256
  // Create substitutions map for placeholders
124
257
  const substitutions = new Map<string, Parameter>();
125
258
  for (let i = 0; i < parameters.length; i++) {
@@ -150,7 +283,7 @@ export class TemplateEngine {
150
283
 
151
284
  // Check for Capture (could be a Proxy, so check for symbol property)
152
285
  const isCapture = param instanceof CaptureImpl ||
153
- (param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL]);
286
+ (param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL]);
154
287
  const isCaptureValue = param instanceof CaptureValue;
155
288
  const isTreeArray = Array.isArray(param) && param.length > 0 && isTree(param[0]);
156
289
 
@@ -202,31 +335,16 @@ export class TemplateEngine {
202
335
  for (let i = 0; i < templateParts.length; i++) {
203
336
  result += templateParts[i];
204
337
  if (i < parameters.length) {
205
- const param = parameters[i].value;
206
- // Use a placeholder for Captures, TemplateParams, CaptureValues, Tree nodes, and Tree arrays
207
- // Inline everything else (strings, numbers, booleans) directly
208
- // Check for Capture (could be a Proxy, so check for symbol property)
209
- const isCapture = param instanceof CaptureImpl ||
210
- (param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL]);
211
- const isTemplateParam = param instanceof TemplateParamImpl;
212
- const isCaptureValue = param instanceof CaptureValue;
213
- const isTreeArray = Array.isArray(param) && param.length > 0 && isTree(param[0]);
214
- if (isCapture || isTemplateParam || isCaptureValue || isTree(param) || isTreeArray) {
215
- const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
216
- result += placeholder;
217
- } else {
218
- result += param;
219
- }
338
+ // All parameters are now placeholders (no primitive inlining)
339
+ // This ensures templates with the same structure always produce the same AST
340
+ const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
341
+ result += placeholder;
220
342
  }
221
343
  }
222
344
 
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;
345
+ // Always wrap in function body - let the parser decide what it is,
346
+ // then we'll extract intelligently based on what was parsed
347
+ return `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
230
348
  }
231
349
 
232
350
  /**
@@ -275,6 +393,182 @@ export class TemplateEngine {
275
393
  // TODO: Implement proper Type to string conversion for other Type.Kind values
276
394
  return 'any';
277
395
  }
396
+
397
+ /**
398
+ * Gets the parsed and extracted pattern tree with capture markers attached.
399
+ * This is the entry point for pattern processing, providing pattern-specific
400
+ * functionality on top of the shared template tree generation.
401
+ *
402
+ * @param templateParts The string parts of the template
403
+ * @param captures The captures between the string parts
404
+ * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
405
+ * @param dependencies NPM dependencies for type attribution
406
+ * @returns A Promise resolving to the extracted pattern AST with capture markers
407
+ */
408
+ static async getPatternTree(
409
+ templateParts: TemplateStringsArray,
410
+ captures: (Capture | Any)[],
411
+ contextStatements: string[] = [],
412
+ dependencies: Record<string, string> = {}
413
+ ): Promise<J> {
414
+ // Generate type preamble for captures with types
415
+ const preamble: string[] = [];
416
+ for (const capture of captures) {
417
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
418
+ const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
419
+ if (captureType) {
420
+ // Convert Type to string if needed
421
+ const typeString = typeof captureType === 'string'
422
+ ? captureType
423
+ : this.typeToString(captureType);
424
+ const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
425
+ preamble.push(`let ${placeholder}: ${typeString};`);
426
+ } else {
427
+ const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
428
+ preamble.push(`let ${placeholder};`);
429
+ }
430
+ }
431
+
432
+ // Build the template string with placeholders for captures
433
+ let result = '';
434
+ for (let i = 0; i < templateParts.length; i++) {
435
+ result += templateParts[i];
436
+ if (i < captures.length) {
437
+ const capture = captures[i];
438
+ // Use symbol to access capture name without triggering Proxy
439
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
440
+ result += PlaceholderUtils.createCapture(captureName, undefined);
441
+ }
442
+ }
443
+
444
+ // Always wrap in function body - let the parser decide what it is,
445
+ // then we'll extract intelligently based on what was parsed
446
+ const templateString = `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
447
+
448
+ // Add preamble to context statements (so they're skipped during extraction)
449
+ const contextWithPreamble = preamble.length > 0
450
+ ? [...contextStatements, ...preamble]
451
+ : contextStatements;
452
+
453
+ // Use cache to get or parse the compilation unit
454
+ const cu = await templateCache.getOrParse(
455
+ templateString,
456
+ captures,
457
+ contextWithPreamble,
458
+ dependencies
459
+ );
460
+
461
+ // Check if there are any statements
462
+ if (!cu.statements || cu.statements.length === 0) {
463
+ throw new Error(`Failed to parse pattern code (no statements):\n${templateString}`);
464
+ }
465
+
466
+ // The pattern code is always the last statement (after context + preamble)
467
+ const lastStatement = cu.statements[cu.statements.length - 1].element;
468
+
469
+ // Extract from wrapper using shared utility
470
+ const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
471
+
472
+ // Attach CaptureMarkers to capture identifiers
473
+ const visitor = new MarkerAttachmentVisitor(captures);
474
+ return (await visitor.visit(extracted, undefined))!;
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Visitor that attaches CaptureMarkers to capture identifiers in pattern ASTs.
480
+ * This allows efficient capture detection without string parsing during matching.
481
+ * Used by TemplateEngine.getPatternTree() for pattern-specific processing.
482
+ */
483
+ class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
484
+ constructor(private readonly captures: (Capture | Any)[]) {
485
+ super();
486
+ }
487
+
488
+ /**
489
+ * Attaches CaptureMarker to capture identifiers.
490
+ */
491
+ protected override async visitIdentifier(ident: J.Identifier, p: undefined): Promise<J | undefined> {
492
+ // First call parent to handle standard visitation
493
+ const visited = await super.visitIdentifier(ident, p);
494
+ if (!visited || visited.kind !== J.Kind.Identifier) {
495
+ return visited;
496
+ }
497
+ ident = visited as J.Identifier;
498
+
499
+ // Check if this is a capture placeholder
500
+ if (ident.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
501
+ const captureInfo = PlaceholderUtils.parseCapture(ident.simpleName);
502
+ if (captureInfo) {
503
+ // Find the original capture object to get variadic options and constraint
504
+ const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
505
+ const variadicOptions = captureObj?.getVariadicOptions();
506
+ const constraint = captureObj?.getConstraint?.();
507
+
508
+ // Add CaptureMarker to the Identifier with constraint
509
+ const marker = new CaptureMarker(captureInfo.name, variadicOptions, constraint);
510
+ return updateIfChanged(ident, {
511
+ markers: {
512
+ ...ident.markers,
513
+ markers: [...ident.markers.markers, marker]
514
+ }
515
+ });
516
+ }
517
+ }
518
+
519
+ return ident;
520
+ }
521
+
522
+ /**
523
+ * Propagates markers from element to RightPadded wrapper.
524
+ */
525
+ public override async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: undefined): Promise<J.RightPadded<T>> {
526
+ if (!isTree(right.element)) {
527
+ return right;
528
+ }
529
+
530
+ const visitedElement = await this.visit(right.element as J, p);
531
+ if (visitedElement && visitedElement !== right.element as Tree) {
532
+ return produceAsync<J.RightPadded<T>>(right, async (draft: any) => {
533
+ // Visit element first
534
+ if (right.element && (right.element as any).kind) {
535
+ // Check if element has a CaptureMarker
536
+ const elementMarker = PlaceholderUtils.getCaptureMarker(visitedElement);
537
+ if (elementMarker) {
538
+ draft.markers.markers.push(elementMarker);
539
+ } else {
540
+ draft.element = visitedElement;
541
+ }
542
+ }
543
+ });
544
+ }
545
+
546
+ return right;
547
+ }
548
+
549
+ /**
550
+ * Propagates markers from expression to ExpressionStatement.
551
+ */
552
+ protected override async visitExpressionStatement(expressionStatement: JS.ExpressionStatement, p: undefined): Promise<J | undefined> {
553
+ // Visit the expression
554
+ const visitedExpression = await this.visit(expressionStatement.expression, p);
555
+
556
+ // Check if expression has a CaptureMarker
557
+ const expressionMarker = PlaceholderUtils.getCaptureMarker(visitedExpression as any);
558
+ if (expressionMarker) {
559
+ return updateIfChanged(expressionStatement, {
560
+ markers: {
561
+ ...expressionStatement.markers,
562
+ markers: [...expressionStatement.markers.markers, expressionMarker]
563
+ },
564
+ });
565
+ }
566
+
567
+ // No marker to move, just update with visited expression
568
+ return updateIfChanged(expressionStatement, {
569
+ expression: visitedExpression
570
+ });
571
+ }
278
572
  }
279
573
 
280
574
  /**
@@ -299,11 +593,9 @@ export class TemplateApplier {
299
593
  // Apply the template based on the location and mode
300
594
  switch (loc || 'EXPRESSION_PREFIX') {
301
595
  case 'EXPRESSION_PREFIX':
302
- return this.applyToExpression();
303
596
  case 'STATEMENT_PREFIX':
304
- return this.applyToStatement();
305
597
  case 'BLOCK_END':
306
- return this.applyToBlock();
598
+ return this.applyInternal();
307
599
  default:
308
600
  throw new Error(`Unsupported location: ${loc}`);
309
601
  }
@@ -314,40 +606,82 @@ export class TemplateApplier {
314
606
  *
315
607
  * @returns A Promise resolving to the modified AST
316
608
  */
317
- private async applyToExpression(): Promise<J | undefined> {
609
+ private async applyInternal(): Promise<J | undefined> {
318
610
  const {tree} = this.coordinates;
319
611
 
320
- // Create a copy of the AST with the prefix from the target
321
- return tree ? produce(this.ast, draft => {
322
- draft.prefix = (tree as J).prefix;
323
- }) : this.ast;
324
- }
612
+ if (!tree) {
613
+ return this.ast;
614
+ }
325
615
 
326
- /**
327
- * Applies the template to a statement.
328
- *
329
- * @returns A Promise resolving to the modified AST
330
- */
331
- private async applyToStatement(): Promise<J | undefined> {
332
- const {tree} = this.coordinates;
616
+ const originalTree = tree as J;
617
+ const resultToUse = this.wrapTree(originalTree, this.ast);
618
+ return this.format(resultToUse, originalTree);
619
+ }
333
620
 
621
+ private async format(resultToUse: J, originalTree: J) {
334
622
  // Create a copy of the AST with the prefix from the target
335
- return produce(this.ast, draft => {
336
- draft.prefix = (tree as J).prefix;
337
- });
623
+ const result = {
624
+ ...resultToUse,
625
+ // We temporarily set the ID so that the formatter can identify the tree
626
+ id: originalTree.id,
627
+ prefix: originalTree.prefix
628
+ };
629
+
630
+ // Apply auto-formatting to the result
631
+ const formatted =
632
+ await maybeAutoFormat(originalTree, result, null, undefined, this.cursor?.parent);
633
+
634
+ // Restore the original ID
635
+ return {...formatted, id: resultToUse.id};
338
636
  }
339
637
 
340
- /**
341
- * Applies the template to a block.
342
- *
343
- * @returns A Promise resolving to the modified AST
344
- */
345
- private async applyToBlock(): Promise<J | undefined> {
346
- const {tree} = this.coordinates;
347
-
348
- // Create a copy of the AST with the prefix from the target
349
- return produce(this.ast, draft => {
350
- draft.prefix = (tree as J).prefix;
351
- });
638
+ private wrapTree(originalTree: J, resultToUse: J) {
639
+ const parentTree = this.cursor?.parentTree()?.value;
640
+
641
+ // Only apply wrapping logic if we have parent context
642
+ if (parentTree) {
643
+ // FIXME: This is a heuristic to determine if the parent expects a statement child
644
+ const parentExpectsStatement = parentTree.kind === J.Kind.Block ||
645
+ parentTree.kind === J.Kind.Case ||
646
+ parentTree.kind === J.Kind.DoWhileLoop ||
647
+ parentTree.kind === J.Kind.ForEachLoop ||
648
+ parentTree.kind === J.Kind.ForLoop ||
649
+ parentTree.kind === J.Kind.If ||
650
+ parentTree.kind === J.Kind.IfElse ||
651
+ parentTree.kind === J.Kind.WhileLoop ||
652
+ parentTree.kind === JS.Kind.CompilationUnit ||
653
+ parentTree.kind === JS.Kind.ForInLoop;
654
+ const originalIsStatement = isStatement(originalTree);
655
+
656
+ const resultIsStatement = isStatement(resultToUse);
657
+ const resultIsExpression = isExpression(resultToUse);
658
+
659
+ // Determine context and wrap if needed
660
+ if (parentExpectsStatement && originalIsStatement) {
661
+ // Statement context: wrap in ExpressionStatement if result is not a statement
662
+ if (!resultIsStatement && resultIsExpression) {
663
+ resultToUse = {
664
+ kind: JS.Kind.ExpressionStatement,
665
+ id: randomId(),
666
+ prefix: resultToUse.prefix,
667
+ markers: resultToUse.markers,
668
+ expression: { ...resultToUse, prefix: emptySpace }
669
+ } as JS.ExpressionStatement;
670
+ }
671
+ } else if (!parentExpectsStatement) {
672
+ // Expression context: wrap in StatementExpression if result is statement-only
673
+ if (resultIsStatement && !resultIsExpression) {
674
+ const stmt = resultToUse as Statement;
675
+ resultToUse = {
676
+ kind: JS.Kind.StatementExpression,
677
+ id: randomId(),
678
+ prefix: stmt.prefix,
679
+ markers: stmt.markers,
680
+ statement: { ...stmt, prefix: emptySpace }
681
+ } as JS.StatementExpression;
682
+ }
683
+ }
684
+ }
685
+ return resultToUse;
352
686
  }
353
687
  }