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