@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.
- package/dist/java/tree.d.ts +10 -1
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js +21 -5
- package/dist/java/tree.js.map +1 -1
- package/dist/java/type-visitor.d.ts +1 -1
- package/dist/java/type-visitor.d.ts.map +1 -1
- package/dist/java/visitor.d.ts +2 -2
- package/dist/java/visitor.d.ts.map +1 -1
- package/dist/java/visitor.js +8 -2
- package/dist/java/visitor.js.map +1 -1
- package/dist/javascript/assertions.d.ts +6 -0
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +14 -6
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/comparator.d.ts +217 -7
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +1020 -2848
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/format.d.ts +5 -3
- package/dist/javascript/format.d.ts.map +1 -1
- package/dist/javascript/format.js +87 -44
- package/dist/javascript/format.js.map +1 -1
- package/dist/javascript/index.d.ts +2 -1
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +2 -1
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/parser.d.ts +2 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +54 -43
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/templating/capture.d.ts +293 -0
- package/dist/javascript/templating/capture.d.ts.map +1 -0
- package/dist/javascript/templating/capture.js +461 -0
- package/dist/javascript/templating/capture.js.map +1 -0
- package/dist/javascript/templating/comparator.d.ts +171 -0
- package/dist/javascript/templating/comparator.d.ts.map +1 -0
- package/dist/javascript/templating/comparator.js +1221 -0
- package/dist/javascript/templating/comparator.js.map +1 -0
- package/dist/javascript/templating/engine.d.ts +108 -0
- package/dist/javascript/templating/engine.d.ts.map +1 -0
- package/dist/javascript/templating/engine.js +661 -0
- package/dist/javascript/templating/engine.js.map +1 -0
- package/dist/javascript/templating/index.d.ts +6 -0
- package/dist/javascript/templating/index.d.ts.map +1 -0
- package/dist/javascript/templating/index.js +44 -0
- package/dist/javascript/templating/index.js.map +1 -0
- package/dist/javascript/templating/pattern.d.ts +276 -0
- package/dist/javascript/templating/pattern.d.ts.map +1 -0
- package/dist/javascript/templating/pattern.js +952 -0
- package/dist/javascript/templating/pattern.js.map +1 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts +83 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -0
- package/dist/javascript/templating/placeholder-replacement.js +467 -0
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -0
- package/dist/javascript/templating/rewrite.d.ts +84 -0
- package/dist/javascript/templating/rewrite.d.ts.map +1 -0
- package/dist/javascript/templating/rewrite.js +208 -0
- package/dist/javascript/templating/rewrite.js.map +1 -0
- package/dist/javascript/templating/template.d.ts +230 -0
- package/dist/javascript/templating/template.d.ts.map +1 -0
- package/dist/javascript/templating/template.js +367 -0
- package/dist/javascript/templating/template.js.map +1 -0
- package/dist/javascript/templating/types.d.ts +610 -0
- package/dist/javascript/templating/types.d.ts.map +1 -0
- package/dist/javascript/templating/types.js +3 -0
- package/dist/javascript/templating/types.js.map +1 -0
- package/dist/javascript/templating/utils.d.ts +135 -0
- package/dist/javascript/templating/utils.d.ts.map +1 -0
- package/dist/javascript/templating/utils.js +251 -0
- package/dist/javascript/templating/utils.js.map +1 -0
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +21 -11
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/json/rpc.js +2 -2
- package/dist/json/rpc.js.map +1 -1
- package/dist/recipe/order-imports.js.map +1 -1
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +10 -6
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/version.txt +1 -1
- package/dist/visitor.d.ts +4 -4
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +8 -3
- package/dist/visitor.js.map +1 -1
- package/package.json +5 -2
- package/src/java/tree.ts +10 -3
- package/src/java/type-visitor.ts +1 -1
- package/src/java/visitor.ts +11 -5
- package/src/javascript/assertions.ts +9 -3
- package/src/javascript/comparator.ts +1095 -3373
- package/src/javascript/format.ts +72 -33
- package/src/javascript/index.ts +2 -1
- package/src/javascript/parser.ts +67 -45
- package/src/javascript/templating/capture.ts +595 -0
- package/src/javascript/templating/comparator.ts +1383 -0
- package/src/javascript/templating/engine.ts +750 -0
- package/src/javascript/templating/index.ts +67 -0
- package/src/javascript/templating/pattern.ts +1101 -0
- package/src/javascript/templating/placeholder-replacement.ts +475 -0
- package/src/javascript/templating/rewrite.ts +229 -0
- package/src/javascript/templating/template.ts +414 -0
- package/src/javascript/templating/types.ts +674 -0
- package/src/javascript/templating/utils.ts +298 -0
- package/src/javascript/type-mapping.ts +20 -11
- package/src/json/rpc.ts +2 -2
- package/src/recipe/order-imports.ts +1 -1
- package/src/test/rewrite-test.ts +12 -7
- package/src/visitor.ts +14 -6
- package/dist/javascript/templating.d.ts +0 -265
- package/dist/javascript/templating.d.ts.map +0 -1
- package/dist/javascript/templating.js +0 -1027
- package/dist/javascript/templating.js.map +0 -1
- 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};
|