@openrewrite/rewrite 8.66.1 → 8.66.2

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 (106) hide show
  1. package/dist/java/tree.d.ts +10 -1
  2. package/dist/java/tree.d.ts.map +1 -1
  3. package/dist/java/tree.js +21 -5
  4. package/dist/java/tree.js.map +1 -1
  5. package/dist/java/type-visitor.d.ts +1 -1
  6. package/dist/java/type-visitor.d.ts.map +1 -1
  7. package/dist/java/visitor.d.ts +2 -2
  8. package/dist/java/visitor.d.ts.map +1 -1
  9. package/dist/java/visitor.js +8 -2
  10. package/dist/java/visitor.js.map +1 -1
  11. package/dist/javascript/assertions.d.ts +6 -0
  12. package/dist/javascript/assertions.d.ts.map +1 -1
  13. package/dist/javascript/assertions.js +14 -6
  14. package/dist/javascript/assertions.js.map +1 -1
  15. package/dist/javascript/comparator.d.ts +154 -7
  16. package/dist/javascript/comparator.d.ts.map +1 -1
  17. package/dist/javascript/comparator.js +623 -180
  18. package/dist/javascript/comparator.js.map +1 -1
  19. package/dist/javascript/format.d.ts +5 -3
  20. package/dist/javascript/format.d.ts.map +1 -1
  21. package/dist/javascript/format.js +85 -43
  22. package/dist/javascript/format.js.map +1 -1
  23. package/dist/javascript/index.d.ts +1 -0
  24. package/dist/javascript/index.d.ts.map +1 -1
  25. package/dist/javascript/index.js +1 -0
  26. package/dist/javascript/index.js.map +1 -1
  27. package/dist/javascript/parser.d.ts +2 -1
  28. package/dist/javascript/parser.d.ts.map +1 -1
  29. package/dist/javascript/parser.js +39 -30
  30. package/dist/javascript/parser.js.map +1 -1
  31. package/dist/javascript/templating/capture.d.ts +81 -14
  32. package/dist/javascript/templating/capture.d.ts.map +1 -1
  33. package/dist/javascript/templating/capture.js +98 -8
  34. package/dist/javascript/templating/capture.js.map +1 -1
  35. package/dist/javascript/templating/comparator.d.ts +125 -15
  36. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  37. package/dist/javascript/templating/comparator.js +946 -118
  38. package/dist/javascript/templating/comparator.js.map +1 -1
  39. package/dist/javascript/templating/engine.d.ts +58 -25
  40. package/dist/javascript/templating/engine.d.ts.map +1 -1
  41. package/dist/javascript/templating/engine.js +527 -94
  42. package/dist/javascript/templating/engine.js.map +1 -1
  43. package/dist/javascript/templating/index.d.ts +3 -3
  44. package/dist/javascript/templating/index.d.ts.map +1 -1
  45. package/dist/javascript/templating/index.js +3 -1
  46. package/dist/javascript/templating/index.js.map +1 -1
  47. package/dist/javascript/templating/pattern.d.ts +121 -16
  48. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  49. package/dist/javascript/templating/pattern.js +528 -257
  50. package/dist/javascript/templating/pattern.js.map +1 -1
  51. package/dist/javascript/templating/placeholder-replacement.d.ts +30 -5
  52. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  53. package/dist/javascript/templating/placeholder-replacement.js +183 -81
  54. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  55. package/dist/javascript/templating/rewrite.d.ts +56 -11
  56. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  57. package/dist/javascript/templating/rewrite.js +143 -16
  58. package/dist/javascript/templating/rewrite.js.map +1 -1
  59. package/dist/javascript/templating/template.d.ts +31 -5
  60. package/dist/javascript/templating/template.d.ts.map +1 -1
  61. package/dist/javascript/templating/template.js +89 -15
  62. package/dist/javascript/templating/template.js.map +1 -1
  63. package/dist/javascript/templating/types.d.ts +359 -12
  64. package/dist/javascript/templating/types.d.ts.map +1 -1
  65. package/dist/javascript/templating/utils.d.ts +52 -35
  66. package/dist/javascript/templating/utils.d.ts.map +1 -1
  67. package/dist/javascript/templating/utils.js +107 -109
  68. package/dist/javascript/templating/utils.js.map +1 -1
  69. package/dist/javascript/type-mapping.d.ts.map +1 -1
  70. package/dist/javascript/type-mapping.js +21 -11
  71. package/dist/javascript/type-mapping.js.map +1 -1
  72. package/dist/json/rpc.js +2 -2
  73. package/dist/json/rpc.js.map +1 -1
  74. package/dist/recipe/order-imports.js.map +1 -1
  75. package/dist/test/rewrite-test.d.ts.map +1 -1
  76. package/dist/test/rewrite-test.js +10 -6
  77. package/dist/test/rewrite-test.js.map +1 -1
  78. package/dist/version.txt +1 -1
  79. package/dist/visitor.d.ts +4 -4
  80. package/dist/visitor.d.ts.map +1 -1
  81. package/dist/visitor.js +8 -3
  82. package/dist/visitor.js.map +1 -1
  83. package/package.json +4 -2
  84. package/src/java/tree.ts +10 -3
  85. package/src/java/type-visitor.ts +1 -1
  86. package/src/java/visitor.ts +11 -5
  87. package/src/javascript/assertions.ts +9 -3
  88. package/src/javascript/comparator.ts +676 -185
  89. package/src/javascript/format.ts +72 -34
  90. package/src/javascript/index.ts +1 -0
  91. package/src/javascript/parser.ts +51 -31
  92. package/src/javascript/templating/capture.ts +107 -15
  93. package/src/javascript/templating/comparator.ts +1087 -134
  94. package/src/javascript/templating/engine.ts +601 -103
  95. package/src/javascript/templating/index.ts +9 -2
  96. package/src/javascript/templating/pattern.ts +655 -281
  97. package/src/javascript/templating/placeholder-replacement.ts +183 -80
  98. package/src/javascript/templating/rewrite.ts +152 -18
  99. package/src/javascript/templating/template.ts +110 -22
  100. package/src/javascript/templating/types.ts +386 -12
  101. package/src/javascript/templating/utils.ts +116 -102
  102. package/src/javascript/type-mapping.ts +20 -11
  103. package/src/json/rpc.ts +2 -2
  104. package/src/recipe/order-imports.ts +1 -1
  105. package/src/test/rewrite-test.ts +12 -7
  106. package/src/visitor.ts +14 -6
@@ -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} 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 {TemplateCache, PlaceholderUtils} from './utils';
21
- import {CaptureImpl, TemplateParamImpl, CaptureValue, CAPTURE_NAME_SYMBOL} from './capture';
20
+ import {CaptureMarker, PlaceholderUtils, WRAPPER_FUNCTION_NAME} from './utils';
21
+ import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, RAW_CODE_SYMBOL, RawCode} 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
+ }
29
71
 
30
72
  /**
31
- * Parameter specification for template generation.
32
- * Represents a placeholder in a template that will be replaced with a parameter value.
73
+ * Module-level TypeScript sourceFileCache for template parsing.
33
74
  */
34
- export interface Parameter {
75
+ let templateSourceFileCache: Map<string, ts.SourceFile> | undefined;
76
+
77
+ /**
78
+ * Configure the sourceFileCache used for template parsing.
79
+ *
80
+ * @param cache The sourceFileCache to use, or undefined to disable caching
81
+ */
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
+
94
+ /**
95
+ * Generates a cache key from template string, captures, and options.
96
+ */
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
+
35
118
  /**
36
- * The value to substitute into the template.
119
+ * Gets a cached compilation unit or creates and caches a new one.
37
120
  */
38
- value: any;
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,42 +185,37 @@ 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> {
203
+ // Generate type preamble for captures/parameters with types
204
+ const preamble = TemplateEngine.generateTypePreamble(parameters);
205
+
69
206
  // Build the template string with parameter placeholders
70
207
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
71
208
 
72
- // If the template string is empty, return undefined
73
- if (!templateString.trim()) {
74
- return undefined;
75
- }
209
+ // Add preamble to context statements (so they're skipped during extraction)
210
+ const contextWithPreamble = preamble.length > 0
211
+ ? [...contextStatements, ...preamble]
212
+ : contextStatements;
76
213
 
77
214
  // Use cache to get or parse the compilation unit
78
- // For templates, we don't have captures, so use empty array
79
215
  const cu = await templateCache.getOrParse(
80
216
  templateString,
81
- [], // templates don't have captures in the cache key
82
- contextStatements,
217
+ [],
218
+ contextWithPreamble,
83
219
  dependencies
84
220
  );
85
221
 
@@ -88,36 +224,35 @@ export class TemplateEngine {
88
224
  throw new Error(`Failed to parse template code (no statements):\n${templateString}`);
89
225
  }
90
226
 
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
- }
227
+ // The template code is always the last statement (after context + preamble)
228
+ const lastStatement = cu.statements[cu.statements.length - 1].element;
96
229
 
97
- // Extract the relevant part of the AST
98
- const firstStatement = cu.statements[templateStatementIndex].element;
99
- let extracted: J;
230
+ // Extract from wrapper using shared utility
231
+ const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Template');
100
232
 
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, _ => {});
233
+ return produce(extracted, _ => {});
234
+ }
120
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> {
121
256
  // Create substitutions map for placeholders
122
257
  const substitutions = new Map<string, Parameter>();
123
258
  for (let i = 0; i < parameters.length; i++) {
@@ -133,8 +268,71 @@ export class TemplateEngine {
133
268
  return new TemplateApplier(cursor, coordinates, unsubstitutedAst).apply();
134
269
  }
135
270
 
271
+ /**
272
+ * Generates type preamble declarations for captures/parameters with type annotations.
273
+ *
274
+ * @param parameters The parameters
275
+ * @returns Array of preamble statements
276
+ */
277
+ private static generateTypePreamble(parameters: Parameter[]): string[] {
278
+ const preamble: string[] = [];
279
+
280
+ for (let i = 0; i < parameters.length; i++) {
281
+ const param = parameters[i].value;
282
+ const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
283
+
284
+ // Check for Capture (could be a Proxy, so check for symbol property)
285
+ const isCapture = param instanceof CaptureImpl ||
286
+ (param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL]);
287
+ const isCaptureValue = param instanceof CaptureValue;
288
+ const isTreeArray = Array.isArray(param) && param.length > 0 && isTree(param[0]);
289
+
290
+ if (isCapture) {
291
+ const captureType = param[CAPTURE_TYPE_SYMBOL];
292
+ if (captureType) {
293
+ const typeString = typeof captureType === 'string'
294
+ ? captureType
295
+ : this.typeToString(captureType);
296
+ // Only add preamble if we have a concrete type (not 'any')
297
+ if (typeString !== 'any') {
298
+ preamble.push(`let ${placeholder}: ${typeString};`);
299
+ }
300
+ }
301
+ } else if (isCaptureValue) {
302
+ // For CaptureValue, check if the root capture has a type
303
+ const rootCapture = param.rootCapture;
304
+ if (rootCapture) {
305
+ const captureType = (rootCapture as any)[CAPTURE_TYPE_SYMBOL];
306
+ if (captureType) {
307
+ const typeString = typeof captureType === 'string'
308
+ ? captureType
309
+ : this.typeToString(captureType);
310
+ // Only add preamble if we have a concrete type (not 'any')
311
+ if (typeString !== 'any') {
312
+ preamble.push(`let ${placeholder}: ${typeString};`);
313
+ }
314
+ }
315
+ }
316
+ } else if (isTree(param) && !isTreeArray) {
317
+ // For J elements, derive type from the element's type property if it exists
318
+ const jElement = param as J;
319
+ if ((jElement as any).type) {
320
+ const typeString = this.typeToString((jElement as any).type);
321
+ // Only add preamble if we have a concrete type (not 'any')
322
+ if (typeString !== 'any') {
323
+ preamble.push(`let ${placeholder}: ${typeString};`);
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ return preamble;
330
+ }
331
+
136
332
  /**
137
333
  * Builds a template string with parameter placeholders.
334
+ * RawCode parameters are spliced directly into the template at construction time.
335
+ * Other parameters use placeholders that are replaced during application.
138
336
  *
139
337
  * @param templateParts The string parts of the template
140
338
  * @param parameters The parameters between the string parts
@@ -149,30 +347,290 @@ export class TemplateEngine {
149
347
  result += templateParts[i];
150
348
  if (i < parameters.length) {
151
349
  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) {
350
+
351
+ // Check if this is a RawCode instance - splice directly
352
+ if (param instanceof RawCode || (param && typeof param === 'object' && param[RAW_CODE_SYMBOL])) {
353
+ result += (param as RawCode).code;
354
+ } else {
355
+ // All other parameters use placeholders
356
+ // This ensures templates with the same structure always produce the same AST
161
357
  const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
162
358
  result += placeholder;
359
+ }
360
+ }
361
+ }
362
+
363
+ // Always wrap in function body - let the parser decide what it is,
364
+ // then we'll extract intelligently based on what was parsed
365
+ return `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
366
+ }
367
+
368
+ /**
369
+ * Converts a Type instance to a TypeScript type string.
370
+ *
371
+ * @param type The Type instance
372
+ * @returns A TypeScript type string
373
+ */
374
+ private static typeToString(type: Type): string {
375
+ // Handle Type.Class and Type.ShallowClass - return their fully qualified names
376
+ if (type.kind === Type.Kind.Class || type.kind === Type.Kind.ShallowClass) {
377
+ const classType = type as Type.Class;
378
+ return classType.fullyQualifiedName;
379
+ }
380
+
381
+ // Handle Type.Primitive - map to TypeScript primitive types
382
+ if (type.kind === Type.Kind.Primitive) {
383
+ const primitiveType = type as Type.Primitive;
384
+ switch (primitiveType.keyword) {
385
+ case 'String':
386
+ return 'string';
387
+ case 'boolean':
388
+ return 'boolean';
389
+ case 'double':
390
+ case 'float':
391
+ case 'int':
392
+ case 'long':
393
+ case 'short':
394
+ case 'byte':
395
+ return 'number';
396
+ case 'void':
397
+ return 'void';
398
+ default:
399
+ return 'any';
400
+ }
401
+ }
402
+
403
+ // Handle Type.Array - render component type plus []
404
+ if (type.kind === Type.Kind.Array) {
405
+ const arrayType = type as Type.Array;
406
+ const componentTypeString = this.typeToString(arrayType.elemType);
407
+ return `${componentTypeString}[]`;
408
+ }
409
+
410
+ // For other types, return 'any' as a fallback
411
+ // TODO: Implement proper Type to string conversion for other Type.Kind values
412
+ return 'any';
413
+ }
414
+
415
+ /**
416
+ * Gets the parsed and extracted pattern tree with capture markers attached.
417
+ * This is the entry point for pattern processing, providing pattern-specific
418
+ * functionality on top of the shared template tree generation.
419
+ *
420
+ * @param templateParts The string parts of the template
421
+ * @param captures The captures between the string parts (can include RawCode)
422
+ * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
423
+ * @param dependencies NPM dependencies for type attribution
424
+ * @returns A Promise resolving to the extracted pattern AST with capture markers
425
+ */
426
+ static async getPatternTree(
427
+ templateParts: TemplateStringsArray,
428
+ captures: (Capture | Any | RawCode)[],
429
+ contextStatements: string[] = [],
430
+ dependencies: Record<string, string> = {}
431
+ ): Promise<J> {
432
+ // Generate type preamble for captures with types (skip RawCode)
433
+ const preamble: string[] = [];
434
+ for (const capture of captures) {
435
+ // Skip raw code - it's not a capture
436
+ if (capture instanceof RawCode || (capture && typeof capture === 'object' && (capture as any)[RAW_CODE_SYMBOL])) {
437
+ continue;
438
+ }
439
+
440
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
441
+ const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
442
+ if (captureType) {
443
+ // Convert Type to string if needed
444
+ const typeString = typeof captureType === 'string'
445
+ ? captureType
446
+ : this.typeToString(captureType);
447
+ // Only add preamble if we have a concrete type (not 'any')
448
+ if (typeString !== 'any') {
449
+ const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
450
+ preamble.push(`let ${placeholder}: ${typeString};`);
451
+ }
452
+ }
453
+ // Don't add preamble declarations without types - they don't provide type attribution
454
+ }
455
+
456
+ // Build the template string with placeholders for captures and raw code
457
+ let result = '';
458
+ for (let i = 0; i < templateParts.length; i++) {
459
+ result += templateParts[i];
460
+ if (i < captures.length) {
461
+ const capture = captures[i];
462
+
463
+ // Check if this is a RawCode instance - splice directly
464
+ if (capture instanceof RawCode || (capture && typeof capture === 'object' && (capture as any)[RAW_CODE_SYMBOL])) {
465
+ result += (capture as RawCode).code;
163
466
  } else {
164
- result += param;
467
+ // Use symbol to access capture name without triggering Proxy
468
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
469
+ result += PlaceholderUtils.createCapture(captureName, undefined);
165
470
  }
166
471
  }
167
472
  }
168
473
 
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}`;
474
+ // Always wrap in function body - let the parser decide what it is,
475
+ // then we'll extract intelligently based on what was parsed
476
+ const templateString = `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
477
+
478
+ // Add preamble to context statements (so they're skipped during extraction)
479
+ const contextWithPreamble = preamble.length > 0
480
+ ? [...contextStatements, ...preamble]
481
+ : contextStatements;
482
+
483
+ // Filter out RawCode from captures for cache and marker attachment
484
+ const actualCaptures = captures.filter(c =>
485
+ !(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL]))
486
+ ) as (Capture | Any)[];
487
+
488
+ // Use cache to get or parse the compilation unit
489
+ const cu = await templateCache.getOrParse(
490
+ templateString,
491
+ actualCaptures,
492
+ contextWithPreamble,
493
+ dependencies
494
+ );
495
+
496
+ // Check if there are any statements
497
+ if (!cu.statements || cu.statements.length === 0) {
498
+ throw new Error(`Failed to parse pattern code (no statements):\n${templateString}`);
173
499
  }
174
500
 
175
- return result;
501
+ // The pattern code is always the last statement (after context + preamble)
502
+ const lastStatement = cu.statements[cu.statements.length - 1].element;
503
+
504
+ // Extract from wrapper using shared utility
505
+ const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
506
+
507
+ // Attach CaptureMarkers to capture identifiers (only for actual captures, not raw code)
508
+ const visitor = new MarkerAttachmentVisitor(actualCaptures);
509
+ return (await visitor.visit(extracted, undefined))!;
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Visitor that attaches CaptureMarkers to capture identifiers in pattern ASTs.
515
+ * This allows efficient capture detection without string parsing during matching.
516
+ * Used by TemplateEngine.getPatternTree() for pattern-specific processing.
517
+ */
518
+ class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
519
+ constructor(private readonly captures: (Capture | Any)[]) {
520
+ super();
521
+ }
522
+
523
+ /**
524
+ * Attaches CaptureMarker to capture identifiers.
525
+ */
526
+ protected override async visitIdentifier(ident: J.Identifier, p: undefined): Promise<J | undefined> {
527
+ // First call parent to handle standard visitation
528
+ const visited = await super.visitIdentifier(ident, p);
529
+ if (!visited || visited.kind !== J.Kind.Identifier) {
530
+ return visited;
531
+ }
532
+ ident = visited as J.Identifier;
533
+
534
+ // Check if this is a capture placeholder
535
+ if (ident.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
536
+ const captureInfo = PlaceholderUtils.parseCapture(ident.simpleName);
537
+ if (captureInfo) {
538
+ // Find the original capture object to get variadic options and constraint
539
+ const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
540
+ const variadicOptions = captureObj?.getVariadicOptions();
541
+ const constraint = captureObj?.getConstraint?.();
542
+
543
+ // Add CaptureMarker to the Identifier with constraint
544
+ const marker = new CaptureMarker(captureInfo.name, variadicOptions, constraint);
545
+ return updateIfChanged(ident, {
546
+ markers: {
547
+ ...ident.markers,
548
+ markers: [...ident.markers.markers, marker]
549
+ }
550
+ });
551
+ }
552
+ }
553
+
554
+ return ident;
555
+ }
556
+
557
+ /**
558
+ * Propagates markers from element to RightPadded wrapper.
559
+ */
560
+ public override async visitRightPadded<T extends J | boolean>(right: J.RightPadded<T>, p: undefined): Promise<J.RightPadded<T> | undefined> {
561
+ if (!isTree(right.element)) {
562
+ return right;
563
+ }
564
+
565
+ const visitedElement = await this.visit(right.element as J, p);
566
+ if (visitedElement && visitedElement !== right.element as Tree) {
567
+ const result = await produceAsync<J.RightPadded<T>>(right, async (draft: any) => {
568
+ // Visit element first
569
+ if (right.element && (right.element as any).kind) {
570
+ // Check if element has a CaptureMarker
571
+ const elementMarker = PlaceholderUtils.getCaptureMarker(visitedElement);
572
+ if (elementMarker) {
573
+ draft.markers.markers.push(elementMarker);
574
+ } else {
575
+ draft.element = visitedElement;
576
+ }
577
+ }
578
+ });
579
+ return result!;
580
+ }
581
+
582
+ return right;
583
+ }
584
+
585
+ /**
586
+ * Propagates markers from expression to ExpressionStatement.
587
+ */
588
+ protected override async visitExpressionStatement(expressionStatement: JS.ExpressionStatement, p: undefined): Promise<J | undefined> {
589
+ // Visit the expression
590
+ const visitedExpression = await this.visit(expressionStatement.expression, p);
591
+
592
+ // Check if expression has a CaptureMarker
593
+ const expressionMarker = PlaceholderUtils.getCaptureMarker(visitedExpression as any);
594
+ if (expressionMarker) {
595
+ return updateIfChanged(expressionStatement, {
596
+ markers: {
597
+ ...expressionStatement.markers,
598
+ markers: [...expressionStatement.markers.markers, expressionMarker]
599
+ },
600
+ });
601
+ }
602
+
603
+ // No marker to move, just update with visited expression
604
+ return updateIfChanged(expressionStatement, {
605
+ expression: visitedExpression
606
+ });
607
+ }
608
+
609
+ /**
610
+ * Propagates markers from name identifier to BindingElement.
611
+ * This handles destructuring patterns like {${props}} where the capture marker
612
+ * is on the identifier but needs to be on the BindingElement for container matching.
613
+ */
614
+ protected override async visitBindingElement(bindingElement: JS.BindingElement, p: undefined): Promise<J | undefined> {
615
+ // Visit the name
616
+ const visitedName = await this.visit(bindingElement.name, p);
617
+
618
+ // Check if name has a CaptureMarker
619
+ const nameMarker = PlaceholderUtils.getCaptureMarker(visitedName as any);
620
+ if (nameMarker) {
621
+ return updateIfChanged(bindingElement, {
622
+ name: visitedName,
623
+ markers: {
624
+ ...bindingElement.markers,
625
+ markers: [...bindingElement.markers.markers, nameMarker]
626
+ },
627
+ });
628
+ }
629
+
630
+ // No marker to move, just update with visited name
631
+ return updateIfChanged(bindingElement, {
632
+ name: visitedName
633
+ });
176
634
  }
177
635
  }
178
636
 
@@ -198,11 +656,9 @@ export class TemplateApplier {
198
656
  // Apply the template based on the location and mode
199
657
  switch (loc || 'EXPRESSION_PREFIX') {
200
658
  case 'EXPRESSION_PREFIX':
201
- return this.applyToExpression();
202
659
  case 'STATEMENT_PREFIX':
203
- return this.applyToStatement();
204
660
  case 'BLOCK_END':
205
- return this.applyToBlock();
661
+ return this.applyInternal();
206
662
  default:
207
663
  throw new Error(`Unsupported location: ${loc}`);
208
664
  }
@@ -213,40 +669,82 @@ export class TemplateApplier {
213
669
  *
214
670
  * @returns A Promise resolving to the modified AST
215
671
  */
216
- private async applyToExpression(): Promise<J | undefined> {
672
+ private async applyInternal(): Promise<J | undefined> {
217
673
  const {tree} = this.coordinates;
218
674
 
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
- }
675
+ if (!tree) {
676
+ return this.ast;
677
+ }
224
678
 
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;
679
+ const originalTree = tree as J;
680
+ const resultToUse = this.wrapTree(originalTree, this.ast);
681
+ return this.format(resultToUse, originalTree);
682
+ }
232
683
 
684
+ private async format(resultToUse: J, originalTree: J) {
233
685
  // 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
- });
686
+ const result = {
687
+ ...resultToUse,
688
+ // We temporarily set the ID so that the formatter can identify the tree
689
+ id: originalTree.id,
690
+ prefix: originalTree.prefix
691
+ };
692
+
693
+ // Apply auto-formatting to the result
694
+ const formatted =
695
+ await maybeAutoFormat(originalTree, result, null, undefined, this.cursor?.parent);
696
+
697
+ // Restore the original ID
698
+ return {...formatted, id: resultToUse.id};
237
699
  }
238
700
 
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;
701
+ private wrapTree(originalTree: J, resultToUse: J) {
702
+ const parentTree = this.cursor?.parentTree()?.value;
246
703
 
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
- });
704
+ // Only apply wrapping logic if we have parent context
705
+ if (parentTree) {
706
+ // FIXME: This is a heuristic to determine if the parent expects a statement child
707
+ const parentExpectsStatement = parentTree.kind === J.Kind.Block ||
708
+ parentTree.kind === J.Kind.Case ||
709
+ parentTree.kind === J.Kind.DoWhileLoop ||
710
+ parentTree.kind === J.Kind.ForEachLoop ||
711
+ parentTree.kind === J.Kind.ForLoop ||
712
+ parentTree.kind === J.Kind.If ||
713
+ parentTree.kind === J.Kind.IfElse ||
714
+ parentTree.kind === J.Kind.WhileLoop ||
715
+ parentTree.kind === JS.Kind.CompilationUnit ||
716
+ parentTree.kind === JS.Kind.ForInLoop;
717
+ const originalIsStatement = isStatement(originalTree);
718
+
719
+ const resultIsStatement = isStatement(resultToUse);
720
+ const resultIsExpression = isExpression(resultToUse);
721
+
722
+ // Determine context and wrap if needed
723
+ if (parentExpectsStatement && originalIsStatement) {
724
+ // Statement context: wrap in ExpressionStatement if result is not a statement
725
+ if (!resultIsStatement && resultIsExpression) {
726
+ resultToUse = {
727
+ kind: JS.Kind.ExpressionStatement,
728
+ id: randomId(),
729
+ prefix: resultToUse.prefix,
730
+ markers: resultToUse.markers,
731
+ expression: { ...resultToUse, prefix: emptySpace }
732
+ } as JS.ExpressionStatement;
733
+ }
734
+ } else if (!parentExpectsStatement) {
735
+ // Expression context: wrap in StatementExpression if result is statement-only
736
+ if (resultIsStatement && !resultIsExpression) {
737
+ const stmt = resultToUse as Statement;
738
+ resultToUse = {
739
+ kind: JS.Kind.StatementExpression,
740
+ id: randomId(),
741
+ prefix: stmt.prefix,
742
+ markers: stmt.markers,
743
+ statement: { ...stmt, prefix: emptySpace }
744
+ } as JS.StatementExpression;
745
+ }
746
+ }
747
+ }
748
+ return resultToUse;
251
749
  }
252
750
  }