@openrewrite/rewrite 8.66.0 → 8.66.1
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/javascript/comparator.d.ts +67 -4
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +523 -2794
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/format.d.ts.map +1 -1
- package/dist/javascript/format.js +4 -3
- package/dist/javascript/format.js.map +1 -1
- package/dist/javascript/index.d.ts +1 -1
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +1 -1
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +18 -16
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/templating/capture.d.ts +226 -0
- package/dist/javascript/templating/capture.d.ts.map +1 -0
- package/dist/javascript/templating/capture.js +371 -0
- package/dist/javascript/templating/capture.js.map +1 -0
- package/dist/javascript/templating/comparator.d.ts +61 -0
- package/dist/javascript/templating/comparator.d.ts.map +1 -0
- package/dist/javascript/templating/comparator.js +393 -0
- package/dist/javascript/templating/comparator.js.map +1 -0
- package/dist/javascript/templating/engine.d.ts +75 -0
- package/dist/javascript/templating/engine.d.ts.map +1 -0
- package/dist/javascript/templating/engine.js +228 -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 +42 -0
- package/dist/javascript/templating/index.js.map +1 -0
- package/dist/javascript/templating/pattern.d.ts +171 -0
- package/dist/javascript/templating/pattern.d.ts.map +1 -0
- package/dist/javascript/templating/pattern.js +681 -0
- package/dist/javascript/templating/pattern.js.map +1 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts +58 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -0
- package/dist/javascript/templating/placeholder-replacement.js +365 -0
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -0
- package/dist/javascript/templating/rewrite.d.ts +39 -0
- package/dist/javascript/templating/rewrite.d.ts.map +1 -0
- package/dist/javascript/templating/rewrite.js +81 -0
- package/dist/javascript/templating/rewrite.js.map +1 -0
- package/dist/javascript/templating/template.d.ts +204 -0
- package/dist/javascript/templating/template.d.ts.map +1 -0
- package/dist/javascript/templating/template.js +293 -0
- package/dist/javascript/templating/template.js.map +1 -0
- package/dist/javascript/templating/types.d.ts +263 -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 +118 -0
- package/dist/javascript/templating/utils.d.ts.map +1 -0
- package/dist/javascript/templating/utils.js +253 -0
- package/dist/javascript/templating/utils.js.map +1 -0
- package/dist/version.txt +1 -1
- package/package.json +2 -1
- package/src/javascript/comparator.ts +554 -3323
- package/src/javascript/format.ts +3 -2
- package/src/javascript/index.ts +1 -1
- package/src/javascript/parser.ts +19 -17
- package/src/javascript/templating/capture.ts +503 -0
- package/src/javascript/templating/comparator.ts +430 -0
- package/src/javascript/templating/engine.ts +252 -0
- package/src/javascript/templating/index.ts +60 -0
- package/src/javascript/templating/pattern.ts +727 -0
- package/src/javascript/templating/placeholder-replacement.ts +372 -0
- package/src/javascript/templating/rewrite.ts +95 -0
- package/src/javascript/templating/template.ts +326 -0
- package/src/javascript/templating/types.ts +300 -0
- package/src/javascript/templating/utils.ts +284 -0
- 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,430 @@
|
|
|
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 {JS} from '../index';
|
|
19
|
+
import {JavaScriptSemanticComparatorVisitor} from '../comparator';
|
|
20
|
+
import {PlaceholderUtils, CaptureStorageValue} from './utils';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A comparator for pattern matching that is lenient about optional properties.
|
|
24
|
+
* Allows patterns without type annotations to match actual code with type annotations.
|
|
25
|
+
* Uses semantic comparison to match semantically equivalent code (e.g., isDate() and util.isDate()).
|
|
26
|
+
*/
|
|
27
|
+
export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisitor {
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly matcher: {
|
|
30
|
+
handleCapture: (pattern: J, target: J, wrapper?: J.RightPadded<J>) => boolean;
|
|
31
|
+
handleVariadicCapture: (pattern: J, targets: J[], wrappers?: J.RightPadded<J>[]) => boolean;
|
|
32
|
+
saveState: () => Map<string, CaptureStorageValue>;
|
|
33
|
+
restoreState: (state: Map<string, CaptureStorageValue>) => void;
|
|
34
|
+
},
|
|
35
|
+
lenientTypeMatching: boolean = true
|
|
36
|
+
) {
|
|
37
|
+
// Enable lenient type matching based on pattern configuration (default: true for backward compatibility)
|
|
38
|
+
super(lenientTypeMatching);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extracts the wrapper from the cursor if the parent is a RightPadded.
|
|
43
|
+
*/
|
|
44
|
+
private getWrapperFromCursor(cursor: Cursor): J.RightPadded<J> | undefined {
|
|
45
|
+
if (!cursor.parent) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const parent = cursor.parent.value;
|
|
49
|
+
// Check if parent is a RightPadded by checking its kind
|
|
50
|
+
if ((parent as any).kind === J.Kind.RightPadded) {
|
|
51
|
+
return parent as J.RightPadded<J>;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
|
|
57
|
+
// Check if the pattern node is a capture - this handles captures anywhere in the tree
|
|
58
|
+
if (PlaceholderUtils.isCapture(j as J)) {
|
|
59
|
+
const wrapper = this.getWrapperFromCursor(this.cursor);
|
|
60
|
+
const success = this.matcher.handleCapture(j as J, p, wrapper);
|
|
61
|
+
if (!success) {
|
|
62
|
+
return this.abort(j) as R;
|
|
63
|
+
}
|
|
64
|
+
return j as R;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!this.match) {
|
|
68
|
+
return j as R;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return super.visit(j, p, parent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected hasSameKind(j: J, other: J): boolean {
|
|
75
|
+
return super.hasSameKind(j, other) ||
|
|
76
|
+
(j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
|
|
80
|
+
if (PlaceholderUtils.isCapture(identifier)) {
|
|
81
|
+
const wrapper = this.getWrapperFromCursor(this.cursor);
|
|
82
|
+
const success = this.matcher.handleCapture(identifier, other, wrapper);
|
|
83
|
+
return success ? identifier : this.abort(identifier);
|
|
84
|
+
}
|
|
85
|
+
return super.visitIdentifier(identifier, other);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override async visitMethodInvocation(methodInvocation: J.MethodInvocation, other: J): Promise<J | undefined> {
|
|
89
|
+
// Check if any arguments are variadic captures
|
|
90
|
+
const hasVariadicCapture = methodInvocation.arguments.elements.some(arg =>
|
|
91
|
+
PlaceholderUtils.isVariadicCapture(arg.element)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// If no variadic captures, use parent implementation (which includes semantic/type-aware matching)
|
|
95
|
+
if (!hasVariadicCapture) {
|
|
96
|
+
return super.visitMethodInvocation(methodInvocation, other);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Otherwise, handle variadic captures ourselves
|
|
100
|
+
if (!this.match || other.kind !== J.Kind.MethodInvocation) {
|
|
101
|
+
return this.abort(methodInvocation);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const otherMethodInvocation = other as J.MethodInvocation;
|
|
105
|
+
|
|
106
|
+
// Compare select
|
|
107
|
+
if ((methodInvocation.select === undefined) !== (otherMethodInvocation.select === undefined)) {
|
|
108
|
+
return this.abort(methodInvocation);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Visit select if present
|
|
112
|
+
if (methodInvocation.select && otherMethodInvocation.select) {
|
|
113
|
+
await this.visit(methodInvocation.select.element, otherMethodInvocation.select.element);
|
|
114
|
+
if (!this.match) return methodInvocation;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Compare typeParameters
|
|
118
|
+
if ((methodInvocation.typeParameters === undefined) !== (otherMethodInvocation.typeParameters === undefined)) {
|
|
119
|
+
return this.abort(methodInvocation);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Visit typeParameters if present
|
|
123
|
+
if (methodInvocation.typeParameters && otherMethodInvocation.typeParameters) {
|
|
124
|
+
if (methodInvocation.typeParameters.elements.length !== otherMethodInvocation.typeParameters.elements.length) {
|
|
125
|
+
return this.abort(methodInvocation);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Visit each type parameter in lock step
|
|
129
|
+
for (let i = 0; i < methodInvocation.typeParameters.elements.length; i++) {
|
|
130
|
+
await this.visit(methodInvocation.typeParameters.elements[i].element, otherMethodInvocation.typeParameters.elements[i].element);
|
|
131
|
+
if (!this.match) return methodInvocation;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Visit name
|
|
136
|
+
await this.visit(methodInvocation.name, otherMethodInvocation.name);
|
|
137
|
+
if (!this.match) return methodInvocation;
|
|
138
|
+
|
|
139
|
+
// Special handling for variadic captures in arguments
|
|
140
|
+
if (!await this.matchArguments(methodInvocation.arguments.elements, otherMethodInvocation.arguments.elements)) {
|
|
141
|
+
return this.abort(methodInvocation);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return methodInvocation;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override async visitBlock(block: J.Block, other: J): Promise<J | undefined> {
|
|
148
|
+
// Check if any statements have CaptureMarker indicating they're variadic
|
|
149
|
+
const hasVariadicCapture = block.statements.some(stmt => {
|
|
150
|
+
const captureMarker = PlaceholderUtils.getCaptureMarker(stmt.element);
|
|
151
|
+
return captureMarker?.variadicOptions !== undefined;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// If no variadic captures, use parent implementation
|
|
155
|
+
if (!hasVariadicCapture) {
|
|
156
|
+
return super.visitBlock(block, other);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Otherwise, handle variadic captures ourselves
|
|
160
|
+
if (!this.match || other.kind !== J.Kind.Block) {
|
|
161
|
+
return this.abort(block);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const otherBlock = other as J.Block;
|
|
165
|
+
|
|
166
|
+
// Special handling for variadic captures in statements
|
|
167
|
+
if (!await this.matchSequence(block.statements, otherBlock.statements, false)) {
|
|
168
|
+
return this.abort(block);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return block;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, other: J): Promise<J | undefined> {
|
|
175
|
+
// Check if any statements are variadic captures (unwrap ExpressionStatement wrappers first)
|
|
176
|
+
const hasVariadicCapture = compilationUnit.statements.some(stmt => {
|
|
177
|
+
const unwrapped = PlaceholderUtils.unwrapStatementCapture(stmt.element);
|
|
178
|
+
return PlaceholderUtils.isVariadicCapture(unwrapped);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// If no variadic captures, use parent implementation
|
|
182
|
+
if (!hasVariadicCapture) {
|
|
183
|
+
return super.visitJsCompilationUnit(compilationUnit, other);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Otherwise, handle variadic captures ourselves
|
|
187
|
+
if (!this.match || other.kind !== JS.Kind.CompilationUnit) {
|
|
188
|
+
return this.abort(compilationUnit);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const otherCompilationUnit = other as JS.CompilationUnit;
|
|
192
|
+
|
|
193
|
+
// Special handling for variadic captures in top-level statements
|
|
194
|
+
if (!await this.matchSequence(compilationUnit.statements, otherCompilationUnit.statements, false)) {
|
|
195
|
+
return this.abort(compilationUnit);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return compilationUnit;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Matches argument lists, with special handling for variadic captures.
|
|
203
|
+
* A variadic capture can match zero or more consecutive arguments.
|
|
204
|
+
*/
|
|
205
|
+
private async matchArguments(patternArgs: J.RightPadded<J>[], targetArgs: J.RightPadded<J>[]): Promise<boolean> {
|
|
206
|
+
return this.matchSequence(patternArgs, targetArgs, true);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Generic sequence matching with variadic capture support.
|
|
211
|
+
* Works for any sequence of JRightPadded elements (arguments, statements, etc.).
|
|
212
|
+
* A variadic capture can match zero or more consecutive elements.
|
|
213
|
+
*
|
|
214
|
+
* Uses pivot detection to optimize matching, with backtracking as fallback.
|
|
215
|
+
*
|
|
216
|
+
* @param patternElements The pattern elements (JRightPadded)
|
|
217
|
+
* @param targetElements The target elements to match against (JRightPadded)
|
|
218
|
+
* @param filterEmpty Whether to filter out J.Empty elements when capturing (true for arguments, false for statements)
|
|
219
|
+
* @returns true if the sequence matches, false otherwise
|
|
220
|
+
*/
|
|
221
|
+
private async matchSequence(patternElements: J.RightPadded<J>[], targetElements: J.RightPadded<J>[], filterEmpty: boolean): Promise<boolean> {
|
|
222
|
+
return await this.matchSequenceOptimized(patternElements, targetElements, 0, 0, filterEmpty);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Optimized sequence matcher with pivot detection and backtracking.
|
|
227
|
+
* For variadic patterns, tries to detect pivots (where next pattern matches) to avoid
|
|
228
|
+
* unnecessary backtracking. Falls back to full backtracking when pivots are ambiguous.
|
|
229
|
+
*
|
|
230
|
+
* @param patternElements The pattern elements (JRightPadded)
|
|
231
|
+
* @param targetElements The target elements to match against (JRightPadded)
|
|
232
|
+
* @param patternIdx Current position in pattern
|
|
233
|
+
* @param targetIdx Current position in target
|
|
234
|
+
* @param filterEmpty Whether to filter out J.Empty elements when capturing
|
|
235
|
+
* @returns true if the remaining sequence matches, false otherwise
|
|
236
|
+
*/
|
|
237
|
+
private async matchSequenceOptimized(
|
|
238
|
+
patternElements: J.RightPadded<J>[],
|
|
239
|
+
targetElements: J.RightPadded<J>[],
|
|
240
|
+
patternIdx: number,
|
|
241
|
+
targetIdx: number,
|
|
242
|
+
filterEmpty: boolean
|
|
243
|
+
): Promise<boolean> {
|
|
244
|
+
// Base case: all patterns matched
|
|
245
|
+
if (patternIdx >= patternElements.length) {
|
|
246
|
+
return targetIdx >= targetElements.length; // Success if all targets consumed
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const patternElement = patternElements[patternIdx].element;
|
|
250
|
+
const captureMarker = PlaceholderUtils.getCaptureMarker(patternElement);
|
|
251
|
+
const isVariadic = captureMarker?.variadicOptions !== undefined;
|
|
252
|
+
|
|
253
|
+
if (isVariadic) {
|
|
254
|
+
// Variadic pattern: try different consumption amounts with backtracking
|
|
255
|
+
const variadicOptions = captureMarker!.variadicOptions;
|
|
256
|
+
const min = variadicOptions?.min ?? 0;
|
|
257
|
+
const max = variadicOptions?.max ?? Infinity;
|
|
258
|
+
|
|
259
|
+
// Calculate maximum possible consumption
|
|
260
|
+
let nonVariadicRemainingPatterns = 0;
|
|
261
|
+
for (let i = patternIdx + 1; i < patternElements.length; i++) {
|
|
262
|
+
const nextPatternElement = patternElements[i].element;
|
|
263
|
+
const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(nextPatternElement);
|
|
264
|
+
const nextIsVariadic = nextCaptureMarker?.variadicOptions !== undefined;
|
|
265
|
+
if (!nextIsVariadic) {
|
|
266
|
+
nonVariadicRemainingPatterns++;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const remainingTargetElements = targetElements.length - targetIdx;
|
|
270
|
+
const maxPossible = Math.min(remainingTargetElements - nonVariadicRemainingPatterns, max);
|
|
271
|
+
|
|
272
|
+
// Pivot detection optimization: try to find where next pattern matches
|
|
273
|
+
// This avoids unnecessary backtracking when constraints make the split point obvious
|
|
274
|
+
let pivotDetected = false;
|
|
275
|
+
let pivotAt = -1;
|
|
276
|
+
|
|
277
|
+
if (patternIdx + 1 < patternElements.length && min <= maxPossible) {
|
|
278
|
+
const nextPattern = patternElements[patternIdx + 1].element;
|
|
279
|
+
|
|
280
|
+
// Scan through possible consumption amounts starting from min
|
|
281
|
+
for (let tryConsume = min; tryConsume <= maxPossible; tryConsume++) {
|
|
282
|
+
// Check if element after our consumption would match next pattern
|
|
283
|
+
if (targetIdx + tryConsume < targetElements.length) {
|
|
284
|
+
const candidateElement = targetElements[targetIdx + tryConsume].element;
|
|
285
|
+
|
|
286
|
+
// Skip J.Empty for arguments
|
|
287
|
+
if (filterEmpty && candidateElement.kind === J.Kind.Empty) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Test if next pattern matches this element
|
|
292
|
+
const savedMatch = this.match;
|
|
293
|
+
const savedState = this.matcher.saveState();
|
|
294
|
+
|
|
295
|
+
await this.visit(nextPattern, candidateElement);
|
|
296
|
+
const matchesNext = this.match;
|
|
297
|
+
|
|
298
|
+
this.match = savedMatch;
|
|
299
|
+
this.matcher.restoreState(savedState);
|
|
300
|
+
|
|
301
|
+
if (matchesNext) {
|
|
302
|
+
// Found pivot! Try this consumption amount first
|
|
303
|
+
pivotDetected = true;
|
|
304
|
+
pivotAt = tryConsume;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Try different consumption amounts
|
|
312
|
+
// If pivot detected, try that first; otherwise use greedy approach (max to min)
|
|
313
|
+
const consumptionOrder: number[] = [];
|
|
314
|
+
if (pivotDetected && pivotAt >= 0) {
|
|
315
|
+
// Try pivot first, then others as fallback
|
|
316
|
+
consumptionOrder.push(pivotAt);
|
|
317
|
+
for (let c = maxPossible; c >= min; c--) {
|
|
318
|
+
if (c !== pivotAt) {
|
|
319
|
+
consumptionOrder.push(c);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
// Greedy approach: max to min
|
|
324
|
+
for (let c = maxPossible; c >= min; c--) {
|
|
325
|
+
consumptionOrder.push(c);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const consume of consumptionOrder) {
|
|
330
|
+
// Capture elements for this consumption amount
|
|
331
|
+
const capturedWrappers: J.RightPadded<J>[] = [];
|
|
332
|
+
for (let i = 0; i < consume; i++) {
|
|
333
|
+
const wrapped = targetElements[targetIdx + i];
|
|
334
|
+
const element = wrapped.element;
|
|
335
|
+
// For arguments, filter out J.Empty as it represents an empty argument list
|
|
336
|
+
// For statements, include all elements
|
|
337
|
+
if (!filterEmpty || element.kind !== J.Kind.Empty) {
|
|
338
|
+
capturedWrappers.push(wrapped);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Extract just the elements for the constraint check
|
|
343
|
+
const capturedElements: J[] = capturedWrappers.map(w => w.element);
|
|
344
|
+
|
|
345
|
+
// Re-check min/max constraints against actual captured elements (after filtering if applicable)
|
|
346
|
+
if (capturedElements.length < min || capturedElements.length > max) {
|
|
347
|
+
continue; // Try next consumption amount
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Save current state for backtracking
|
|
351
|
+
const savedState = this.matcher.saveState();
|
|
352
|
+
|
|
353
|
+
// Handle the variadic capture
|
|
354
|
+
const success = this.matcher.handleVariadicCapture(patternElement, capturedElements, capturedWrappers);
|
|
355
|
+
if (!success) {
|
|
356
|
+
// Restore state and try next amount
|
|
357
|
+
this.matcher.restoreState(savedState);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Try to match the rest of the pattern
|
|
362
|
+
const restMatches = await this.matchSequenceOptimized(
|
|
363
|
+
patternElements,
|
|
364
|
+
targetElements,
|
|
365
|
+
patternIdx + 1,
|
|
366
|
+
targetIdx + consume,
|
|
367
|
+
filterEmpty
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (restMatches) {
|
|
371
|
+
return true; // Found a valid matching
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Backtrack: restore state and try next amount
|
|
375
|
+
this.matcher.restoreState(savedState);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return false; // No consumption amount worked
|
|
379
|
+
} else {
|
|
380
|
+
// Regular non-variadic element - must match exactly one target element
|
|
381
|
+
if (targetIdx >= targetElements.length) {
|
|
382
|
+
return false; // Pattern has more elements than target
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const targetWrapper = targetElements[targetIdx];
|
|
386
|
+
const targetElement = targetWrapper.element;
|
|
387
|
+
|
|
388
|
+
// For arguments, J.Empty represents no argument, so regular captures should not match it
|
|
389
|
+
if (filterEmpty && targetElement.kind === J.Kind.Empty) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Save current state for backtracking (both match state and capture bindings)
|
|
394
|
+
const savedMatch = this.match;
|
|
395
|
+
const savedState = this.matcher.saveState();
|
|
396
|
+
|
|
397
|
+
// Push wrapper onto cursor so captures can access it
|
|
398
|
+
const savedCursor = this.cursor;
|
|
399
|
+
this.cursor = new Cursor(targetWrapper, this.cursor);
|
|
400
|
+
try {
|
|
401
|
+
await this.visit(patternElement, targetElement);
|
|
402
|
+
} finally {
|
|
403
|
+
this.cursor = savedCursor;
|
|
404
|
+
}
|
|
405
|
+
if (!this.match) {
|
|
406
|
+
// Restore state on match failure
|
|
407
|
+
this.match = savedMatch;
|
|
408
|
+
this.matcher.restoreState(savedState);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Continue matching the rest
|
|
413
|
+
const restMatches = await this.matchSequenceOptimized(
|
|
414
|
+
patternElements,
|
|
415
|
+
targetElements,
|
|
416
|
+
patternIdx + 1,
|
|
417
|
+
targetIdx + 1,
|
|
418
|
+
filterEmpty
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
if (!restMatches) {
|
|
422
|
+
// Restore full state on backtracking failure
|
|
423
|
+
this.match = savedMatch;
|
|
424
|
+
this.matcher.restoreState(savedState);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return restMatches;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
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, isTree} from '../..';
|
|
17
|
+
import {J} from '../../java';
|
|
18
|
+
import {JS} from '..';
|
|
19
|
+
import {produce} from 'immer';
|
|
20
|
+
import {TemplateCache, PlaceholderUtils} from './utils';
|
|
21
|
+
import {CaptureImpl, TemplateParamImpl, CaptureValue, CAPTURE_NAME_SYMBOL} from './capture';
|
|
22
|
+
import {PlaceholderReplacementVisitor} from './placeholder-replacement';
|
|
23
|
+
import {JavaCoordinates} from './template';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Cache for compiled templates.
|
|
27
|
+
*/
|
|
28
|
+
const templateCache = new TemplateCache();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parameter specification for template generation.
|
|
32
|
+
* Represents a placeholder in a template that will be replaced with a parameter value.
|
|
33
|
+
*/
|
|
34
|
+
export interface Parameter {
|
|
35
|
+
/**
|
|
36
|
+
* The value to substitute into the template.
|
|
37
|
+
*/
|
|
38
|
+
value: any;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Internal template engine - handles the core templating logic.
|
|
43
|
+
* Not exported from index, so only visible within the templating module.
|
|
44
|
+
*/
|
|
45
|
+
export class TemplateEngine {
|
|
46
|
+
/**
|
|
47
|
+
* Applies a template with optional match results from pattern matching.
|
|
48
|
+
*
|
|
49
|
+
* @param templateParts The string parts of the template
|
|
50
|
+
* @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
|
+
* @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
|
|
56
|
+
* @param dependencies NPM dependencies for type attribution
|
|
57
|
+
* @returns A Promise resolving to the generated AST node
|
|
58
|
+
*/
|
|
59
|
+
static async applyTemplate(
|
|
60
|
+
templateParts: TemplateStringsArray,
|
|
61
|
+
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
|
+
contextStatements: string[] = [],
|
|
67
|
+
dependencies: Record<string, string> = {}
|
|
68
|
+
): Promise<J | undefined> {
|
|
69
|
+
// Build the template string with parameter placeholders
|
|
70
|
+
const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
|
|
71
|
+
|
|
72
|
+
// If the template string is empty, return undefined
|
|
73
|
+
if (!templateString.trim()) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Use cache to get or parse the compilation unit
|
|
78
|
+
// For templates, we don't have captures, so use empty array
|
|
79
|
+
const cu = await templateCache.getOrParse(
|
|
80
|
+
templateString,
|
|
81
|
+
[], // templates don't have captures in the cache key
|
|
82
|
+
contextStatements,
|
|
83
|
+
dependencies
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Check if there are any statements
|
|
87
|
+
if (!cu.statements || cu.statements.length === 0) {
|
|
88
|
+
throw new Error(`Failed to parse template code (no statements):\n${templateString}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Skip context statements to get to the actual template code
|
|
92
|
+
const templateStatementIndex = contextStatements.length;
|
|
93
|
+
if (templateStatementIndex >= cu.statements.length) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Extract the relevant part of the AST
|
|
98
|
+
const firstStatement = cu.statements[templateStatementIndex].element;
|
|
99
|
+
let extracted: J;
|
|
100
|
+
|
|
101
|
+
// Check if this is a wrapped template (function __TEMPLATE__() { ... })
|
|
102
|
+
if (firstStatement.kind === J.Kind.MethodDeclaration) {
|
|
103
|
+
const func = firstStatement as J.MethodDeclaration;
|
|
104
|
+
if (func.name.simpleName === '__TEMPLATE__' && func.body) {
|
|
105
|
+
// __TEMPLATE__ wrapper indicates the original template was a block.
|
|
106
|
+
// Always return the block to preserve the block structure.
|
|
107
|
+
extracted = func.body;
|
|
108
|
+
} else {
|
|
109
|
+
// Not a __TEMPLATE__ wrapper
|
|
110
|
+
extracted = firstStatement;
|
|
111
|
+
}
|
|
112
|
+
} else if (firstStatement.kind === JS.Kind.ExpressionStatement) {
|
|
113
|
+
extracted = (firstStatement as JS.ExpressionStatement).expression;
|
|
114
|
+
} else {
|
|
115
|
+
extracted = firstStatement;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create a copy to avoid sharing cached AST instances
|
|
119
|
+
const ast = produce(extracted, _ => {});
|
|
120
|
+
|
|
121
|
+
// Create substitutions map for placeholders
|
|
122
|
+
const substitutions = new Map<string, Parameter>();
|
|
123
|
+
for (let i = 0; i < parameters.length; i++) {
|
|
124
|
+
const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
|
|
125
|
+
substitutions.set(placeholder, parameters[i]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Unsubstitute placeholders with actual parameter values and match results
|
|
129
|
+
const visitor = new PlaceholderReplacementVisitor(substitutions, values, wrappersMap);
|
|
130
|
+
const unsubstitutedAst = (await visitor.visit(ast, null))!;
|
|
131
|
+
|
|
132
|
+
// Apply the template to the current AST
|
|
133
|
+
return new TemplateApplier(cursor, coordinates, unsubstitutedAst).apply();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Builds a template string with parameter placeholders.
|
|
138
|
+
*
|
|
139
|
+
* @param templateParts The string parts of the template
|
|
140
|
+
* @param parameters The parameters between the string parts
|
|
141
|
+
* @returns The template string
|
|
142
|
+
*/
|
|
143
|
+
private static buildTemplateString(
|
|
144
|
+
templateParts: TemplateStringsArray,
|
|
145
|
+
parameters: Parameter[]
|
|
146
|
+
): string {
|
|
147
|
+
let result = '';
|
|
148
|
+
for (let i = 0; i < templateParts.length; i++) {
|
|
149
|
+
result += templateParts[i];
|
|
150
|
+
if (i < parameters.length) {
|
|
151
|
+
const param = parameters[i].value;
|
|
152
|
+
// Use a placeholder for Captures, TemplateParams, CaptureValues, Tree nodes, and Tree arrays
|
|
153
|
+
// Inline everything else (strings, numbers, booleans) directly
|
|
154
|
+
// Check for Capture (could be a Proxy, so check for symbol property)
|
|
155
|
+
const isCapture = param instanceof CaptureImpl ||
|
|
156
|
+
(param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL]);
|
|
157
|
+
const isTemplateParam = param instanceof TemplateParamImpl;
|
|
158
|
+
const isCaptureValue = param instanceof CaptureValue;
|
|
159
|
+
const isTreeArray = Array.isArray(param) && param.length > 0 && isTree(param[0]);
|
|
160
|
+
if (isCapture || isTemplateParam || isCaptureValue || isTree(param) || isTreeArray) {
|
|
161
|
+
const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
|
|
162
|
+
result += placeholder;
|
|
163
|
+
} else {
|
|
164
|
+
result += param;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Detect if this is a block template that needs wrapping
|
|
170
|
+
const trimmed = result.trim();
|
|
171
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
172
|
+
result = `function __TEMPLATE__() ${result}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Helper class for applying a template to an AST.
|
|
181
|
+
*/
|
|
182
|
+
export class TemplateApplier {
|
|
183
|
+
constructor(
|
|
184
|
+
private readonly cursor: Cursor,
|
|
185
|
+
private readonly coordinates: JavaCoordinates,
|
|
186
|
+
private readonly ast: J
|
|
187
|
+
) {
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Applies the template to the current AST.
|
|
192
|
+
*
|
|
193
|
+
* @returns A Promise resolving to the modified AST
|
|
194
|
+
*/
|
|
195
|
+
async apply(): Promise<J | undefined> {
|
|
196
|
+
const {loc} = this.coordinates;
|
|
197
|
+
|
|
198
|
+
// Apply the template based on the location and mode
|
|
199
|
+
switch (loc || 'EXPRESSION_PREFIX') {
|
|
200
|
+
case 'EXPRESSION_PREFIX':
|
|
201
|
+
return this.applyToExpression();
|
|
202
|
+
case 'STATEMENT_PREFIX':
|
|
203
|
+
return this.applyToStatement();
|
|
204
|
+
case 'BLOCK_END':
|
|
205
|
+
return this.applyToBlock();
|
|
206
|
+
default:
|
|
207
|
+
throw new Error(`Unsupported location: ${loc}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Applies the template to an expression.
|
|
213
|
+
*
|
|
214
|
+
* @returns A Promise resolving to the modified AST
|
|
215
|
+
*/
|
|
216
|
+
private async applyToExpression(): Promise<J | undefined> {
|
|
217
|
+
const {tree} = this.coordinates;
|
|
218
|
+
|
|
219
|
+
// Create a copy of the AST with the prefix from the target
|
|
220
|
+
return tree ? produce(this.ast, draft => {
|
|
221
|
+
draft.prefix = (tree as J).prefix;
|
|
222
|
+
}) : this.ast;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Applies the template to a statement.
|
|
227
|
+
*
|
|
228
|
+
* @returns A Promise resolving to the modified AST
|
|
229
|
+
*/
|
|
230
|
+
private async applyToStatement(): Promise<J | undefined> {
|
|
231
|
+
const {tree} = this.coordinates;
|
|
232
|
+
|
|
233
|
+
// Create a copy of the AST with the prefix from the target
|
|
234
|
+
return produce(this.ast, draft => {
|
|
235
|
+
draft.prefix = (tree as J).prefix;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Applies the template to a block.
|
|
241
|
+
*
|
|
242
|
+
* @returns A Promise resolving to the modified AST
|
|
243
|
+
*/
|
|
244
|
+
private async applyToBlock(): Promise<J | undefined> {
|
|
245
|
+
const {tree} = this.coordinates;
|
|
246
|
+
|
|
247
|
+
// Create a copy of the AST with the prefix from the target
|
|
248
|
+
return produce(this.ast, draft => {
|
|
249
|
+
draft.prefix = (tree as J).prefix;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|