@openrewrite/rewrite 8.66.0 → 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 (113) 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 +217 -7
  16. package/dist/javascript/comparator.d.ts.map +1 -1
  17. package/dist/javascript/comparator.js +1020 -2848
  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 +87 -44
  22. package/dist/javascript/format.js.map +1 -1
  23. package/dist/javascript/index.d.ts +2 -1
  24. package/dist/javascript/index.d.ts.map +1 -1
  25. package/dist/javascript/index.js +2 -1
  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 +54 -43
  30. package/dist/javascript/parser.js.map +1 -1
  31. package/dist/javascript/templating/capture.d.ts +293 -0
  32. package/dist/javascript/templating/capture.d.ts.map +1 -0
  33. package/dist/javascript/templating/capture.js +461 -0
  34. package/dist/javascript/templating/capture.js.map +1 -0
  35. package/dist/javascript/templating/comparator.d.ts +171 -0
  36. package/dist/javascript/templating/comparator.d.ts.map +1 -0
  37. package/dist/javascript/templating/comparator.js +1221 -0
  38. package/dist/javascript/templating/comparator.js.map +1 -0
  39. package/dist/javascript/templating/engine.d.ts +108 -0
  40. package/dist/javascript/templating/engine.d.ts.map +1 -0
  41. package/dist/javascript/templating/engine.js +661 -0
  42. package/dist/javascript/templating/engine.js.map +1 -0
  43. package/dist/javascript/templating/index.d.ts +6 -0
  44. package/dist/javascript/templating/index.d.ts.map +1 -0
  45. package/dist/javascript/templating/index.js +44 -0
  46. package/dist/javascript/templating/index.js.map +1 -0
  47. package/dist/javascript/templating/pattern.d.ts +276 -0
  48. package/dist/javascript/templating/pattern.d.ts.map +1 -0
  49. package/dist/javascript/templating/pattern.js +952 -0
  50. package/dist/javascript/templating/pattern.js.map +1 -0
  51. package/dist/javascript/templating/placeholder-replacement.d.ts +83 -0
  52. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -0
  53. package/dist/javascript/templating/placeholder-replacement.js +467 -0
  54. package/dist/javascript/templating/placeholder-replacement.js.map +1 -0
  55. package/dist/javascript/templating/rewrite.d.ts +84 -0
  56. package/dist/javascript/templating/rewrite.d.ts.map +1 -0
  57. package/dist/javascript/templating/rewrite.js +208 -0
  58. package/dist/javascript/templating/rewrite.js.map +1 -0
  59. package/dist/javascript/templating/template.d.ts +230 -0
  60. package/dist/javascript/templating/template.d.ts.map +1 -0
  61. package/dist/javascript/templating/template.js +367 -0
  62. package/dist/javascript/templating/template.js.map +1 -0
  63. package/dist/javascript/templating/types.d.ts +610 -0
  64. package/dist/javascript/templating/types.d.ts.map +1 -0
  65. package/dist/javascript/templating/types.js +3 -0
  66. package/dist/javascript/templating/types.js.map +1 -0
  67. package/dist/javascript/templating/utils.d.ts +135 -0
  68. package/dist/javascript/templating/utils.d.ts.map +1 -0
  69. package/dist/javascript/templating/utils.js +251 -0
  70. package/dist/javascript/templating/utils.js.map +1 -0
  71. package/dist/javascript/type-mapping.d.ts.map +1 -1
  72. package/dist/javascript/type-mapping.js +21 -11
  73. package/dist/javascript/type-mapping.js.map +1 -1
  74. package/dist/json/rpc.js +2 -2
  75. package/dist/json/rpc.js.map +1 -1
  76. package/dist/recipe/order-imports.js.map +1 -1
  77. package/dist/test/rewrite-test.d.ts.map +1 -1
  78. package/dist/test/rewrite-test.js +10 -6
  79. package/dist/test/rewrite-test.js.map +1 -1
  80. package/dist/version.txt +1 -1
  81. package/dist/visitor.d.ts +4 -4
  82. package/dist/visitor.d.ts.map +1 -1
  83. package/dist/visitor.js +8 -3
  84. package/dist/visitor.js.map +1 -1
  85. package/package.json +5 -2
  86. package/src/java/tree.ts +10 -3
  87. package/src/java/type-visitor.ts +1 -1
  88. package/src/java/visitor.ts +11 -5
  89. package/src/javascript/assertions.ts +9 -3
  90. package/src/javascript/comparator.ts +1095 -3373
  91. package/src/javascript/format.ts +72 -33
  92. package/src/javascript/index.ts +2 -1
  93. package/src/javascript/parser.ts +67 -45
  94. package/src/javascript/templating/capture.ts +595 -0
  95. package/src/javascript/templating/comparator.ts +1383 -0
  96. package/src/javascript/templating/engine.ts +750 -0
  97. package/src/javascript/templating/index.ts +67 -0
  98. package/src/javascript/templating/pattern.ts +1101 -0
  99. package/src/javascript/templating/placeholder-replacement.ts +475 -0
  100. package/src/javascript/templating/rewrite.ts +229 -0
  101. package/src/javascript/templating/template.ts +414 -0
  102. package/src/javascript/templating/types.ts +674 -0
  103. package/src/javascript/templating/utils.ts +298 -0
  104. package/src/javascript/type-mapping.ts +20 -11
  105. package/src/json/rpc.ts +2 -2
  106. package/src/recipe/order-imports.ts +1 -1
  107. package/src/test/rewrite-test.ts +12 -7
  108. package/src/visitor.ts +14 -6
  109. package/dist/javascript/templating.d.ts +0 -265
  110. package/dist/javascript/templating.d.ts.map +0 -1
  111. package/dist/javascript/templating.js +0 -1027
  112. package/dist/javascript/templating.js.map +0 -1
  113. package/src/javascript/templating.ts +0 -1226
@@ -0,0 +1,229 @@
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, ExecutionContext, Recipe} from '../..';
17
+ import {J} from '../../java';
18
+ import {RewriteRule, RewriteConfig} from './types';
19
+ import {Pattern, MatchResult} from './pattern';
20
+ import {Template} from './template';
21
+
22
+ /**
23
+ * Implementation of a replacement rule.
24
+ */
25
+ class RewriteRuleImpl implements RewriteRule {
26
+ constructor(
27
+ private readonly before: Pattern[],
28
+ private readonly after: Template | ((match: MatchResult) => Template),
29
+ private readonly where?: (node: J, cursor: Cursor) => boolean | Promise<boolean>,
30
+ private readonly whereNot?: (node: J, cursor: Cursor) => boolean | Promise<boolean>
31
+ ) {
32
+ }
33
+
34
+ async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
35
+ for (const pattern of this.before) {
36
+ // Pass cursor to pattern.match() for context-aware capture constraints
37
+ const match = await pattern.match(node, cursor);
38
+ if (match) {
39
+ // Evaluate context predicates after structural match
40
+ if (this.where) {
41
+ const whereResult = await this.where(node, cursor);
42
+ if (!whereResult) {
43
+ continue; // Pattern matched but context doesn't, try next pattern
44
+ }
45
+ }
46
+
47
+ if (this.whereNot) {
48
+ const whereNotResult = await this.whereNot(node, cursor);
49
+ if (whereNotResult) {
50
+ continue; // Pattern matched but context is excluded, try next pattern
51
+ }
52
+ }
53
+
54
+ // Apply transformation
55
+ let result: J | undefined;
56
+
57
+ if (typeof this.after === 'function') {
58
+ // Call the function to get a template, then apply it
59
+ const template = this.after(match);
60
+ result = await template.apply(cursor, node, match);
61
+ } else {
62
+ // Use template.apply() as before
63
+ result = await this.after.apply(cursor, node, match);
64
+ }
65
+
66
+ if (result) {
67
+ return result;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Return undefined if no patterns match or all context checks failed
73
+ return undefined;
74
+ }
75
+
76
+ andThen(next: RewriteRule): RewriteRule {
77
+ const first = this;
78
+ return new (class extends RewriteRuleImpl {
79
+ constructor() {
80
+ // Pass empty patterns and a function that will never be called
81
+ // since we override tryOn
82
+ super([], () => undefined as unknown as Template);
83
+ }
84
+
85
+ async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
86
+ const firstResult = await first.tryOn(cursor, node);
87
+ if (firstResult !== undefined) {
88
+ const secondResult = await next.tryOn(cursor, firstResult);
89
+ return secondResult ?? firstResult;
90
+ }
91
+ return undefined;
92
+ }
93
+ })();
94
+ }
95
+
96
+ orElse(alternative: RewriteRule): RewriteRule {
97
+ const first = this;
98
+ return new (class extends RewriteRuleImpl {
99
+ constructor() {
100
+ // Pass empty patterns and a function that will never be called
101
+ // since we override tryOn
102
+ super([], () => undefined as unknown as Template);
103
+ }
104
+
105
+ async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
106
+ const firstResult = await first.tryOn(cursor, node);
107
+ if (firstResult !== undefined) {
108
+ return firstResult;
109
+ }
110
+ return await alternative.tryOn(cursor, node);
111
+ }
112
+ })();
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Creates a replacement rule using a capture context and configuration.
118
+ *
119
+ * @param builderFn Function that takes a capture context and returns before/after configuration
120
+ * @returns A replacement rule that can be applied to AST nodes
121
+ *
122
+ * @example
123
+ * // Single pattern
124
+ * const swapOperands = rewrite(() => {
125
+ * const { left, right } = { left: capture(), right: capture() };
126
+ * return {
127
+ * before: pattern`${left} + ${right}`,
128
+ * after: template`${right} + ${left}`
129
+ * };
130
+ * });
131
+ *
132
+ * @example
133
+ * // Multiple patterns
134
+ * const normalizeComparisons = rewrite(() => {
135
+ * const { left, right } = { left: capture(), right: capture() };
136
+ * return {
137
+ * before: [
138
+ * pattern`${left} == ${right}`,
139
+ * pattern`${left} === ${right}`
140
+ * ],
141
+ * after: template`${left} === ${right}`
142
+ * };
143
+ * });
144
+ *
145
+ * @example
146
+ * // Using in a visitor - IMPORTANT: use `|| node` to handle undefined when no match
147
+ * class MyVisitor extends JavaScriptVisitor<any> {
148
+ * override async visitBinary(binary: J.Binary, p: any): Promise<J | undefined> {
149
+ * const rule = rewrite(() => ({
150
+ * before: pattern`${capture('a')} + ${capture('b')}`,
151
+ * after: template`${capture('b')} + ${capture('a')}`
152
+ * }));
153
+ * // tryOn() returns undefined if no pattern matches, so always use || node
154
+ * return await rule.tryOn(this.cursor, binary) || binary;
155
+ * }
156
+ * }
157
+ */
158
+ export function rewrite(
159
+ builderFn: () => RewriteConfig
160
+ ): RewriteRule {
161
+ const config = builderFn();
162
+
163
+ // Ensure we have valid before and after properties
164
+ if (!config.before || !config.after) {
165
+ throw new Error('Builder function must return an object with before and after properties');
166
+ }
167
+
168
+ return new RewriteRuleImpl(
169
+ Array.isArray(config.before) ? config.before : [config.before],
170
+ config.after,
171
+ config.where,
172
+ config.whereNot
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Creates a RewriteRule from a Recipe by using its editor visitor.
178
+ *
179
+ * This allows recipes to be used in the same chaining pattern as other rewrite rules,
180
+ * enabling composition with `andThen()`.
181
+ *
182
+ * @param recipe The recipe whose editor will be used to transform nodes
183
+ * @param ctx The execution context to pass to the recipe's editor
184
+ * @returns A RewriteRule that applies the recipe's editor to nodes
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * class MyRecipe extends Recipe {
189
+ * name = "my.recipe";
190
+ * displayName = "My Recipe";
191
+ * description = "Transforms code.";
192
+ *
193
+ * async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
194
+ * return new MyVisitor();
195
+ * }
196
+ * }
197
+ *
198
+ * // In a visitor:
199
+ * override async visitBinary(binary: J.Binary, p: ExecutionContext): Promise<J | undefined> {
200
+ * const rule1 = rewrite(() => ({
201
+ * before: pattern`${capture('a')} + ${capture('b')}`,
202
+ * after: template`${capture('b')} + ${capture('a')}`
203
+ * }));
204
+ *
205
+ * const rule2 = fromRecipe(new MyRecipe(), p);
206
+ *
207
+ * // Chain the pattern-based rule with the recipe
208
+ * const combined = rule1.andThen(rule2);
209
+ * return await combined.tryOn(this.cursor, binary) || binary;
210
+ * }
211
+ * ```
212
+ */
213
+ export const fromRecipe = (recipe: Recipe, ctx: ExecutionContext): RewriteRule => {
214
+ return new (class extends RewriteRuleImpl {
215
+ constructor() {
216
+ // Pass empty patterns and a function that will never be called
217
+ // since we override tryOn
218
+ super([], () => undefined as unknown as Template);
219
+ }
220
+
221
+ async tryOn(cursor: Cursor, tree: J): Promise<J | undefined> {
222
+ const visitor = await recipe.editor();
223
+ const result = await visitor.visit<J>(tree, ctx, cursor);
224
+
225
+ // Return undefined if the visitor didn't change the node
226
+ return result !== tree ? result : undefined;
227
+ }
228
+ })();
229
+ }
@@ -0,0 +1,414 @@
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 {Capture, Parameter, TemplateOptions, TemplateParameter} from './types';
19
+ import {MatchResult} from './pattern';
20
+ import {generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
21
+ import {CAPTURE_NAME_SYMBOL, RAW_CODE_SYMBOL} from './capture';
22
+ import {TemplateEngine} from './engine';
23
+ import {JS} from '..';
24
+
25
+ /**
26
+ * Coordinates for template application.
27
+ */
28
+ type JavaCoordinates = {
29
+ tree?: Tree;
30
+ loc?: JavaCoordinates.Location;
31
+ mode?: JavaCoordinates.Mode;
32
+ };
33
+
34
+ namespace JavaCoordinates {
35
+ // FIXME need to come up with the equivalent of `Space.Location` support
36
+ export type Location = 'EXPRESSION_PREFIX' | 'STATEMENT_PREFIX' | 'BLOCK_END';
37
+
38
+ export enum Mode {
39
+ Before,
40
+ After,
41
+ Replace,
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Builder for creating templates programmatically.
47
+ * Use when template structure is not known at compile time.
48
+ *
49
+ * @example
50
+ * // Conditional construction
51
+ * const builder = Template.builder().code('function foo(x) {');
52
+ * if (needsValidation) {
53
+ * builder.code('if (typeof x !== "number") throw new Error("Invalid");');
54
+ * }
55
+ * builder.code('return x * 2; }');
56
+ * const tmpl = builder.build();
57
+ *
58
+ * @example
59
+ * // Composition from fragments
60
+ * function createWrapper(innerBody: Capture): Template {
61
+ * return Template.builder()
62
+ * .code('function wrapper() { try { ')
63
+ * .param(innerBody)
64
+ * .code(' } catch(e) { console.error(e); } }')
65
+ * .build();
66
+ * }
67
+ */
68
+ export class TemplateBuilder {
69
+ private parts: string[] = [];
70
+ private params: TemplateParameter[] = [];
71
+
72
+ /**
73
+ * Adds a static string part to the template.
74
+ *
75
+ * @param str The string to add
76
+ * @returns This builder for chaining
77
+ */
78
+ code(str: string): this {
79
+ // If there are already params, we need to add an empty string before this
80
+ if (this.params.length > this.parts.length) {
81
+ this.parts.push('');
82
+ }
83
+ // Append to the last part or start a new one
84
+ if (this.parts.length === 0) {
85
+ this.parts.push(str);
86
+ } else {
87
+ this.parts[this.parts.length - 1] += str;
88
+ }
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * Adds a parameter to the template.
94
+ *
95
+ * @param value The parameter value (Capture, Tree, or primitive)
96
+ * @returns This builder for chaining
97
+ */
98
+ param(value: TemplateParameter): this {
99
+ // Ensure we have a part for after this parameter
100
+ if (this.parts.length === 0) {
101
+ this.parts.push('');
102
+ }
103
+ this.params.push(value);
104
+ // Add an empty string for the next part
105
+ this.parts.push('');
106
+ return this;
107
+ }
108
+
109
+ /**
110
+ * Builds the template from accumulated parts and parameters.
111
+ *
112
+ * @returns A Template instance
113
+ */
114
+ build(): Template {
115
+ // Ensure parts array is one longer than params array
116
+ while (this.parts.length <= this.params.length) {
117
+ this.parts.push('');
118
+ }
119
+
120
+ // Create a synthetic TemplateStringsArray
121
+ const templateStrings = this.parts.slice() as any;
122
+ templateStrings.raw = this.parts.slice();
123
+ Object.defineProperty(templateStrings, 'raw', {
124
+ value: this.parts.slice(),
125
+ writable: false
126
+ });
127
+
128
+ // Delegate to the template() function
129
+ return template(templateStrings, ...this.params);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Template for creating AST nodes.
135
+ *
136
+ * This class provides the public API for template generation.
137
+ * The actual templating logic is handled by the internal TemplateEngine.
138
+ *
139
+ * Templates can reference captures from patterns, and you can access properties
140
+ * of captured nodes using dot notation. This allows you to extract and insert
141
+ * specific subtrees from matched AST nodes.
142
+ *
143
+ * @example
144
+ * // Generate a literal AST node
145
+ * const result = template`2`.apply(cursor, coordinates);
146
+ *
147
+ * @example
148
+ * // Generate an AST node with a parameter
149
+ * const result = template`${capture()}`.apply(cursor, coordinates);
150
+ *
151
+ * @example
152
+ * // Access properties of captured nodes in templates
153
+ * const method = capture<J.MethodInvocation>('method');
154
+ * const pat = pattern`foo(${method})`;
155
+ * const tmpl = template`bar(${method.name})`; // Access the 'name' property
156
+ *
157
+ * const match = await pat.match(someNode);
158
+ * if (match) {
159
+ * // The template will insert just the 'name' subtree from the captured method
160
+ * const result = await tmpl.apply(cursor, someNode, match);
161
+ * }
162
+ *
163
+ * @example
164
+ * // Deep property access chains
165
+ * const method = capture<J.MethodInvocation>('method');
166
+ * template`console.log(${method.name.simpleName})` // Navigate multiple properties
167
+ *
168
+ * @example
169
+ * // Array element access
170
+ * const invocation = capture<J.MethodInvocation>('invocation');
171
+ * template`bar(${invocation.arguments.elements[0].element})` // Access array elements
172
+ */
173
+ export class Template {
174
+ private options: TemplateOptions = {};
175
+ private _cachedTemplate?: J;
176
+
177
+ /**
178
+ * Creates a new builder for constructing templates programmatically.
179
+ *
180
+ * @returns A new TemplateBuilder instance
181
+ *
182
+ * @example
183
+ * const tmpl = Template.builder()
184
+ * .code('function foo() {')
185
+ * .code('return ')
186
+ * .param(capture('value'))
187
+ * .code('; }')
188
+ * .build();
189
+ */
190
+ static builder(): TemplateBuilder {
191
+ return new TemplateBuilder();
192
+ }
193
+
194
+ /**
195
+ * Creates a new template.
196
+ *
197
+ * @param templateParts The string parts of the template
198
+ * @param parameters The parameters between the string parts
199
+ */
200
+ constructor(
201
+ private readonly templateParts: TemplateStringsArray,
202
+ private readonly parameters: Parameter[]
203
+ ) {
204
+ }
205
+
206
+ /**
207
+ * Configures this template with additional options.
208
+ *
209
+ * @param options Configuration options
210
+ * @returns This template for method chaining
211
+ *
212
+ * @example
213
+ * template`isDate(${capture('date')})`
214
+ * .configure({
215
+ * context: ['import { isDate } from "util"'],
216
+ * dependencies: { 'util': '^1.0.0' }
217
+ * })
218
+ */
219
+ configure(options: TemplateOptions): Template {
220
+ this.options = { ...this.options, ...options };
221
+ // Invalidate cache when configuration changes
222
+ this._cachedTemplate = undefined;
223
+ return this;
224
+ }
225
+
226
+ /**
227
+ * Gets the template tree for this template, using two-level caching:
228
+ * - Level 1: Instance cache (this._cachedTemplate) - fastest, no lookup needed
229
+ * - Level 2: Global cache (globalAstCache) - fast, shared across all templates
230
+ * - Level 3: TemplateEngine - slow, parses and processes the template
231
+ *
232
+ * Most parameters use placeholders that are replaced during application, so templates
233
+ * with the same structure share cached ASTs. However, raw() parameters are spliced at
234
+ * construction time, so their values must be included in the cache key.
235
+ *
236
+ * @returns The cached or newly computed template tree
237
+ * @internal
238
+ */
239
+ async getTemplateTree(): Promise<JS.CompilationUnit> {
240
+ // Level 1: Instance cache (fastest path)
241
+ if (this._cachedTemplate) {
242
+ return this._cachedTemplate as JS.CompilationUnit;
243
+ }
244
+
245
+ // Generate cache key for global lookup
246
+ // For raw() parameters, we need to include their code values in the key
247
+ // since they're spliced at construction time, not application time
248
+ const contextStatements = this.options.context || this.options.imports || [];
249
+ const parametersKey = this.parameters.map((p, i) => {
250
+ const value = p.value;
251
+ // Include raw code values in the cache key using the symbol
252
+ if (value && typeof value === 'object' && value[RAW_CODE_SYMBOL]) {
253
+ return `raw:${value.code}`;
254
+ }
255
+ return i.toString();
256
+ }).join(',');
257
+ const cacheKey = generateCacheKey(
258
+ this.templateParts,
259
+ parametersKey,
260
+ contextStatements,
261
+ this.options.dependencies || {}
262
+ );
263
+
264
+ // Level 2: Global cache (fast path - shared with Pattern)
265
+ const cached = globalAstCache.get(cacheKey);
266
+ if (cached) {
267
+ this._cachedTemplate = cached as JS.CompilationUnit;
268
+ return cached as JS.CompilationUnit;
269
+ }
270
+
271
+ // Level 3: Compute via TemplateEngine (slow path)
272
+ const result = await TemplateEngine.getTemplateTree(
273
+ this.templateParts,
274
+ this.parameters,
275
+ contextStatements,
276
+ this.options.dependencies || {}
277
+ ) as JS.CompilationUnit;
278
+
279
+ // Cache in both levels
280
+ globalAstCache.set(cacheKey, result);
281
+ this._cachedTemplate = result;
282
+
283
+ return result;
284
+ }
285
+
286
+ /**
287
+ * Applies this template and returns the resulting tree.
288
+ *
289
+ * @param cursor The cursor pointing to the current location in the AST
290
+ * @param tree Input tree
291
+ * @param values values for parameters in template
292
+ * @returns A Promise resolving to the generated AST node
293
+ */
294
+ async apply(cursor: Cursor, tree: J, values?: Map<Capture | string, J> | Pick<Map<string, J>, 'get'> | Record<string, J>): Promise<J | undefined> {
295
+ // Normalize the values map: convert any Capture keys to string keys
296
+ let normalizedValues: Pick<Map<string, J>, 'get'> | undefined;
297
+ let wrappersMap: Map<string, J.RightPadded<J> | J.RightPadded<J>[]> = new Map();
298
+
299
+ if (values instanceof MatchResult) {
300
+ // MatchResult - extract both bindings and wrappersMap
301
+ normalizedValues = values;
302
+ wrappersMap = (values as any)[WRAPPERS_MAP_SYMBOL]();
303
+ } else if (values instanceof Map) {
304
+ const normalized = new Map<string, J>();
305
+ for (const [key, value] of values.entries()) {
306
+ const stringKey = typeof key === 'string'
307
+ ? key
308
+ : ((key as any)[CAPTURE_NAME_SYMBOL] || key.getName());
309
+ normalized.set(stringKey, value);
310
+ }
311
+ normalizedValues = normalized;
312
+ } else if (values && typeof values === 'object') {
313
+ // Check if it's a Map-like object with 'get' method, or a plain object literal
314
+ if ('get' in values && typeof values.get === 'function') {
315
+ // Map-like object with get method
316
+ normalizedValues = values as Pick<Map<string, J>, 'get'>;
317
+ } else {
318
+ // Plain object literal - convert to Map
319
+ // Keys may be strings or Capture objects (via computed properties {[x]: value})
320
+ const normalized = new Map<string, J>();
321
+ for (const [key, value] of Object.entries(values)) {
322
+ // If the key happens to be a stringified Capture (from computed properties),
323
+ // it's already been converted to a string by JavaScript
324
+ normalized.set(key, value);
325
+ }
326
+ normalizedValues = normalized;
327
+ }
328
+ }
329
+
330
+ // Use instance-level cache to get the template tree
331
+ const ast = await this.getTemplateTree();
332
+
333
+ // Delegate to TemplateEngine for placeholder substitution and application
334
+ return TemplateEngine.applyTemplateFromAst(
335
+ ast,
336
+ this.parameters,
337
+ cursor,
338
+ {
339
+ tree,
340
+ mode: JavaCoordinates.Mode.Replace
341
+ },
342
+ normalizedValues,
343
+ wrappersMap
344
+ );
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Tagged template function for creating templates that generate AST nodes.
350
+ *
351
+ * Templates support property access on captures from patterns, allowing you to
352
+ * extract and insert specific subtrees from matched AST nodes. Use dot notation
353
+ * to navigate properties (e.g., `method.name`) or array bracket notation to
354
+ * access array elements (e.g., `args.elements[0].element`).
355
+ *
356
+ * Templates can also accept AST wrapper types directly:
357
+ * - J.RightPadded<T>: The element will be extracted and inserted
358
+ * - J.RightPadded<T>[]: Elements will be expanded in place
359
+ * - J.Container<T>: Elements will be expanded in place
360
+ *
361
+ * @param strings The string parts of the template
362
+ * @param parameters The parameters between the string parts (Capture, CaptureValue, TemplateParam, Tree, Tree[], J.RightPadded, J.RightPadded[], or J.Container)
363
+ * @returns A Template object that can be applied to generate AST nodes
364
+ *
365
+ * @example
366
+ * // Simple template with literal
367
+ * const tmpl = template`console.log("hello")`;
368
+ * const result = await tmpl.apply(cursor, node);
369
+ *
370
+ * @example
371
+ * // Template with capture - matches captured value from pattern
372
+ * const expr = capture('expr');
373
+ * const pat = pattern`foo(${expr})`;
374
+ * const tmpl = template`bar(${expr})`;
375
+ *
376
+ * const match = await pat.match(node);
377
+ * if (match) {
378
+ * const result = await tmpl.apply(cursor, node, match);
379
+ * }
380
+ *
381
+ * @example
382
+ * // Property access on captures - extract subtrees
383
+ * const method = capture<J.MethodInvocation>('method');
384
+ * const pat = pattern`foo(${method})`;
385
+ * // Access the 'name' property of the captured method invocation
386
+ * const tmpl = template`bar(${method.name})`;
387
+ *
388
+ * @example
389
+ * // Deep property chains
390
+ * const method = capture<J.MethodInvocation>('method');
391
+ * template`console.log(${method.name.simpleName})`
392
+ *
393
+ * @example
394
+ * // Array element access
395
+ * const invocation = capture<J.MethodInvocation>('invocation');
396
+ * template`bar(${invocation.arguments.elements[0].element})`
397
+ *
398
+ * @example
399
+ * // Using J.RightPadded and J.Container directly
400
+ * const selectExpr = method.select; // J.RightPadded<Expression>
401
+ * const args = method.arguments; // J.Container<Expression>
402
+ * template`${selectExpr}.newMethod(${args})`
403
+ */
404
+ export function template(strings: TemplateStringsArray, ...parameters: TemplateParameter[]): Template {
405
+ // Convert parameters to Parameter objects (no longer need to check for mutable tree property)
406
+ const processedParameters = parameters.map(param => {
407
+ // Just wrap each parameter value in a Parameter object
408
+ return {value: param};
409
+ });
410
+
411
+ return new Template(strings, processedParameters);
412
+ }
413
+
414
+ export type {JavaCoordinates};