@openrewrite/rewrite 8.67.0-20251104-084009 → 8.67.0-20251104-114312
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 +3 -2
- 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/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 +2 -1
- package/src/javascript/index.ts +1 -1
- 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,727 @@
|
|
|
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 {produce} from 'immer';
|
|
17
|
+
import {J} from '../../java';
|
|
18
|
+
import {JS} from '../index';
|
|
19
|
+
import {randomId} from '../../uuid';
|
|
20
|
+
import {Capture, Any, PatternOptions} from './types';
|
|
21
|
+
import {CaptureImpl, CAPTURE_NAME_SYMBOL, CAPTURE_CAPTURING_SYMBOL} from './capture';
|
|
22
|
+
import {PatternMatchingComparator} from './comparator';
|
|
23
|
+
import {PlaceholderUtils, templateCache, CaptureMarker, CaptureStorageValue, WRAPPERS_MAP_SYMBOL} from './utils';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builder for creating patterns programmatically.
|
|
27
|
+
* Use when pattern structure is not known at compile time.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Loop-based pattern generation
|
|
31
|
+
* const builder = Pattern.builder().code('myFunction(');
|
|
32
|
+
* for (let i = 0; i < argCount; i++) {
|
|
33
|
+
* if (i > 0) builder.code(', ');
|
|
34
|
+
* builder.capture(capture(`arg${i}`));
|
|
35
|
+
* }
|
|
36
|
+
* builder.code(')');
|
|
37
|
+
* const pat = builder.build();
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Conditional pattern construction
|
|
41
|
+
* const builder = Pattern.builder().code('foo(');
|
|
42
|
+
* builder.capture(capture('first'));
|
|
43
|
+
* if (needsSecondArg) {
|
|
44
|
+
* builder.code(', ').capture(capture('second'));
|
|
45
|
+
* }
|
|
46
|
+
* builder.code(')');
|
|
47
|
+
* const pat = builder.build();
|
|
48
|
+
*/
|
|
49
|
+
export class PatternBuilder {
|
|
50
|
+
private parts: string[] = [];
|
|
51
|
+
private captures: (Capture | Any<any>)[] = [];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Adds a static string part to the pattern.
|
|
55
|
+
*
|
|
56
|
+
* @param str The string to add
|
|
57
|
+
* @returns This builder for chaining
|
|
58
|
+
*/
|
|
59
|
+
code(str: string): this {
|
|
60
|
+
// If there are already captures, we need to add an empty string before this
|
|
61
|
+
if (this.captures.length > this.parts.length) {
|
|
62
|
+
this.parts.push('');
|
|
63
|
+
}
|
|
64
|
+
// Append to the last part or start a new one
|
|
65
|
+
if (this.parts.length === 0) {
|
|
66
|
+
this.parts.push(str);
|
|
67
|
+
} else {
|
|
68
|
+
this.parts[this.parts.length - 1] += str;
|
|
69
|
+
}
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Adds a capture to the pattern.
|
|
75
|
+
*
|
|
76
|
+
* @param value The capture object (Capture or Any) or string name
|
|
77
|
+
* @returns This builder for chaining
|
|
78
|
+
*/
|
|
79
|
+
capture(value: Capture | Any<any> | string): this {
|
|
80
|
+
// Ensure we have a part for after this capture
|
|
81
|
+
if (this.parts.length === 0) {
|
|
82
|
+
this.parts.push('');
|
|
83
|
+
}
|
|
84
|
+
// Convert string to Capture if needed
|
|
85
|
+
const captureObj = typeof value === 'string' ? new CaptureImpl(value) : value;
|
|
86
|
+
this.captures.push(captureObj as any);
|
|
87
|
+
// Add an empty string for the next part
|
|
88
|
+
this.parts.push('');
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds the pattern from accumulated parts and captures.
|
|
94
|
+
*
|
|
95
|
+
* @returns A Pattern instance
|
|
96
|
+
*/
|
|
97
|
+
build(): Pattern {
|
|
98
|
+
// Ensure parts array is one longer than captures array
|
|
99
|
+
while (this.parts.length <= this.captures.length) {
|
|
100
|
+
this.parts.push('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create a synthetic TemplateStringsArray
|
|
104
|
+
const templateStrings = this.parts.slice() as any;
|
|
105
|
+
templateStrings.raw = this.parts.slice();
|
|
106
|
+
Object.defineProperty(templateStrings, 'raw', {
|
|
107
|
+
value: this.parts.slice(),
|
|
108
|
+
writable: false
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Delegate to the pattern() function
|
|
112
|
+
return pattern(templateStrings, ...this.captures);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Represents a pattern that can be matched against AST nodes.
|
|
118
|
+
*/
|
|
119
|
+
export class Pattern {
|
|
120
|
+
private _options: PatternOptions = {};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Gets the configuration options for this pattern.
|
|
124
|
+
* @readonly
|
|
125
|
+
*/
|
|
126
|
+
get options(): Readonly<PatternOptions> {
|
|
127
|
+
return this._options;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Creates a new builder for constructing patterns programmatically.
|
|
132
|
+
*
|
|
133
|
+
* @returns A new PatternBuilder instance
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const pat = Pattern.builder()
|
|
137
|
+
* .code('function ')
|
|
138
|
+
* .capture(capture('name'))
|
|
139
|
+
* .code('() { return ')
|
|
140
|
+
* .capture(capture('value'))
|
|
141
|
+
* .code('; }')
|
|
142
|
+
* .build();
|
|
143
|
+
*/
|
|
144
|
+
static builder(): PatternBuilder {
|
|
145
|
+
return new PatternBuilder();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Creates a new pattern from template parts and captures.
|
|
150
|
+
*
|
|
151
|
+
* @param templateParts The string parts of the template
|
|
152
|
+
* @param captures The captures between the string parts (can be Capture or Any)
|
|
153
|
+
*/
|
|
154
|
+
constructor(
|
|
155
|
+
public readonly templateParts: TemplateStringsArray,
|
|
156
|
+
public readonly captures: (Capture | Any<any>)[]
|
|
157
|
+
) {
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Configures this pattern with additional options.
|
|
162
|
+
*
|
|
163
|
+
* @param options Configuration options
|
|
164
|
+
* @returns This pattern for method chaining
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* pattern`isDate(${capture('date')})`
|
|
168
|
+
* .configure({
|
|
169
|
+
* imports: ['import { isDate } from \"util\"'],
|
|
170
|
+
* dependencies: { 'util': '^1.0.0' }
|
|
171
|
+
* })
|
|
172
|
+
*/
|
|
173
|
+
configure(options: PatternOptions): Pattern {
|
|
174
|
+
this._options = { ...this._options, ...options };
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Creates a matcher for this pattern against a specific AST node.
|
|
180
|
+
*
|
|
181
|
+
* @param ast The AST node to match against
|
|
182
|
+
* @returns A Matcher object
|
|
183
|
+
*/
|
|
184
|
+
async match(ast: J): Promise<MatchResult | undefined> {
|
|
185
|
+
const matcher = new Matcher(this, ast);
|
|
186
|
+
const success = await matcher.matches();
|
|
187
|
+
if (!success) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
// Create MatchResult with unified storage
|
|
191
|
+
const storage = (matcher as any).storage;
|
|
192
|
+
return new MatchResult(new Map(storage));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Result of a successful pattern match containing captured values.
|
|
198
|
+
*
|
|
199
|
+
* Provides access to captured AST nodes from pattern matching operations.
|
|
200
|
+
* Use the `get()` method to retrieve captured values by name or by Capture object.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* const x = capture('x');
|
|
204
|
+
* const pat = pattern`foo(${x})`;
|
|
205
|
+
* const match = await pat.match(someNode);
|
|
206
|
+
* if (match) {
|
|
207
|
+
* const captured = match.get('x'); // Get by name
|
|
208
|
+
* // or
|
|
209
|
+
* const captured = match.get(x); // Get by Capture object
|
|
210
|
+
* }
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* // Variadic captures return arrays
|
|
214
|
+
* const args = capture({ variadic: true });
|
|
215
|
+
* const pat = pattern`foo(${args})`;
|
|
216
|
+
* const match = await pat.match(methodInvocation);
|
|
217
|
+
* if (match) {
|
|
218
|
+
* const capturedArgs = match.get(args); // Returns J[] for variadic captures
|
|
219
|
+
* }
|
|
220
|
+
*/
|
|
221
|
+
export class MatchResult implements Pick<Map<string, J>, "get"> {
|
|
222
|
+
constructor(
|
|
223
|
+
private readonly storage: Map<string, CaptureStorageValue> = new Map()
|
|
224
|
+
) {
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Overload: get with variadic Capture (array type) returns array
|
|
228
|
+
get<T>(capture: Capture<T[]>): T[] | undefined;
|
|
229
|
+
// Overload: get with regular Capture returns single value
|
|
230
|
+
get<T>(capture: Capture<T>): T | undefined;
|
|
231
|
+
// Overload: get with string returns J
|
|
232
|
+
get(capture: string): J | undefined;
|
|
233
|
+
// Implementation
|
|
234
|
+
get(capture: Capture<any> | string): J | J[] | undefined {
|
|
235
|
+
// Use symbol to get internal name without triggering Proxy
|
|
236
|
+
const name = typeof capture === "string" ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
|
|
237
|
+
const value = this.storage.get(name);
|
|
238
|
+
if (value === undefined) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
return this.extractElements(value);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Extracts semantic elements from storage value.
|
|
246
|
+
* For wrappers, extracts the .element; for arrays, returns array of elements.
|
|
247
|
+
*
|
|
248
|
+
* @param value The storage value
|
|
249
|
+
* @returns The semantic element(s)
|
|
250
|
+
*/
|
|
251
|
+
private extractElements(value: CaptureStorageValue): J {
|
|
252
|
+
if (Array.isArray(value)) {
|
|
253
|
+
// Check if it's an array of wrappers
|
|
254
|
+
if (value.length > 0 && (value[0] as any).element !== undefined) {
|
|
255
|
+
// Array of J.RightPadded - extract elements
|
|
256
|
+
return (value as J.RightPadded<J>[]).map(w => w.element) as any;
|
|
257
|
+
}
|
|
258
|
+
// Already an array of elements
|
|
259
|
+
return value as any;
|
|
260
|
+
}
|
|
261
|
+
// Check if it's a scalar wrapper
|
|
262
|
+
if ((value as any).element !== undefined) {
|
|
263
|
+
return (value as J.RightPadded<J>).element;
|
|
264
|
+
}
|
|
265
|
+
// Scalar element
|
|
266
|
+
return value as J;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Internal method to get wrappers (used by template expansion).
|
|
271
|
+
* Returns both scalar and variadic wrappers.
|
|
272
|
+
* @internal
|
|
273
|
+
*/
|
|
274
|
+
[WRAPPERS_MAP_SYMBOL](): Map<string, J.RightPadded<J> | J.RightPadded<J>[]> {
|
|
275
|
+
const result = new Map<string, J.RightPadded<J> | J.RightPadded<J>[]>();
|
|
276
|
+
for (const [name, value] of this.storage) {
|
|
277
|
+
if (Array.isArray(value) && value.length > 0 && (value[0] as any).element !== undefined) {
|
|
278
|
+
// This is an array of wrappers (variadic)
|
|
279
|
+
result.set(name, value as J.RightPadded<J>[]);
|
|
280
|
+
} else if (!Array.isArray(value) && (value as any).element !== undefined) {
|
|
281
|
+
// This is a scalar wrapper
|
|
282
|
+
result.set(name, value as J.RightPadded<J>);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Matcher for checking if a pattern matches an AST node and extracting captured nodes.
|
|
291
|
+
*/
|
|
292
|
+
class Matcher {
|
|
293
|
+
// Unified storage: holds J for scalar captures, J.RightPadded<J>[] or J[] for variadic captures
|
|
294
|
+
private readonly storage = new Map<string, CaptureStorageValue>();
|
|
295
|
+
private patternAst?: J;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Creates a new matcher for a pattern against an AST node.
|
|
299
|
+
*
|
|
300
|
+
* @param pattern The pattern to match
|
|
301
|
+
* @param ast The AST node to match against
|
|
302
|
+
*/
|
|
303
|
+
constructor(
|
|
304
|
+
private readonly pattern: Pattern,
|
|
305
|
+
private readonly ast: J
|
|
306
|
+
) {
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Checks if the pattern matches the AST node.
|
|
311
|
+
*
|
|
312
|
+
* @returns true if the pattern matches, false otherwise
|
|
313
|
+
*/
|
|
314
|
+
async matches(): Promise<boolean> {
|
|
315
|
+
if (!this.patternAst) {
|
|
316
|
+
// Prefer 'context' over deprecated 'imports'
|
|
317
|
+
const contextStatements = this.pattern.options.context || this.pattern.options.imports || [];
|
|
318
|
+
const templateProcessor = new TemplateProcessor(
|
|
319
|
+
this.pattern.templateParts,
|
|
320
|
+
this.pattern.captures,
|
|
321
|
+
contextStatements,
|
|
322
|
+
this.pattern.options.dependencies || {}
|
|
323
|
+
);
|
|
324
|
+
this.patternAst = await templateProcessor.toAstPattern();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return this.matchNode(this.patternAst, this.ast);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Gets all captured nodes (projected view: extracts elements from wrappers).
|
|
332
|
+
*
|
|
333
|
+
* @returns A map of capture names to captured nodes
|
|
334
|
+
*/
|
|
335
|
+
getAll(): Map<string, J> {
|
|
336
|
+
const result = new Map<string, J>();
|
|
337
|
+
for (const [name, value] of this.storage) {
|
|
338
|
+
result.set(name, this.extractElements(value));
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Extracts semantic elements from storage value.
|
|
345
|
+
* For wrappers, extracts the .element; for arrays, returns array of elements.
|
|
346
|
+
*
|
|
347
|
+
* @param value The storage value
|
|
348
|
+
* @returns The semantic element(s)
|
|
349
|
+
*/
|
|
350
|
+
private extractElements(value: CaptureStorageValue): J {
|
|
351
|
+
if (Array.isArray(value)) {
|
|
352
|
+
// Check if it's an array of wrappers
|
|
353
|
+
if (value.length > 0 && (value[0] as any).element !== undefined) {
|
|
354
|
+
// Array of J.RightPadded - extract elements
|
|
355
|
+
return (value as J.RightPadded<J>[]).map(w => w.element) as any;
|
|
356
|
+
}
|
|
357
|
+
// Already an array of elements
|
|
358
|
+
return value as any;
|
|
359
|
+
}
|
|
360
|
+
// Check if it's a scalar wrapper
|
|
361
|
+
if ((value as any).element !== undefined) {
|
|
362
|
+
return (value as J.RightPadded<J>).element;
|
|
363
|
+
}
|
|
364
|
+
// Scalar element
|
|
365
|
+
return value as J;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Matches a pattern node against a target node.
|
|
370
|
+
*
|
|
371
|
+
* @param pattern The pattern node
|
|
372
|
+
* @param target The target node
|
|
373
|
+
* @returns true if the pattern matches the target, false otherwise
|
|
374
|
+
*/
|
|
375
|
+
private async matchNode(pattern: J, target: J): Promise<boolean> {
|
|
376
|
+
// Check if pattern is a capture placeholder
|
|
377
|
+
if (PlaceholderUtils.isCapture(pattern)) {
|
|
378
|
+
return this.handleCapture(pattern, target);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check if nodes have the same kind
|
|
382
|
+
if (pattern.kind !== target.kind) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Use the pattern matching comparator with configured lenient type matching
|
|
387
|
+
// Default to true for backward compatibility with existing patterns
|
|
388
|
+
const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
|
|
389
|
+
const comparator = new PatternMatchingComparator({
|
|
390
|
+
handleCapture: (p, t) => this.handleCapture(p, t),
|
|
391
|
+
handleVariadicCapture: (p, ts, ws) => this.handleVariadicCapture(p, ts, ws),
|
|
392
|
+
saveState: () => this.saveState(),
|
|
393
|
+
restoreState: (state) => this.restoreState(state)
|
|
394
|
+
}, lenientTypeMatching);
|
|
395
|
+
return await comparator.compare(pattern, target);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Saves the current state of storage for backtracking.
|
|
400
|
+
*
|
|
401
|
+
* @returns A snapshot of the current state
|
|
402
|
+
*/
|
|
403
|
+
private saveState(): Map<string, CaptureStorageValue> {
|
|
404
|
+
return new Map(this.storage);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Restores a previously saved state for backtracking.
|
|
409
|
+
*
|
|
410
|
+
* @param state The state to restore
|
|
411
|
+
*/
|
|
412
|
+
private restoreState(state: Map<string, CaptureStorageValue>): void {
|
|
413
|
+
this.storage.clear();
|
|
414
|
+
state.forEach((value, key) => this.storage.set(key, value));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Handles a capture placeholder.
|
|
419
|
+
*
|
|
420
|
+
* @param pattern The pattern node
|
|
421
|
+
* @param target The target node
|
|
422
|
+
* @param wrapper Optional wrapper containing the target (for preserving markers)
|
|
423
|
+
* @returns true if the capture is successful, false otherwise
|
|
424
|
+
*/
|
|
425
|
+
private handleCapture(pattern: J, target: J, wrapper?: J.RightPadded<J>): boolean {
|
|
426
|
+
const captureName = PlaceholderUtils.getCaptureName(pattern);
|
|
427
|
+
|
|
428
|
+
if (!captureName) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Find the original capture object to get constraint and capturing flag
|
|
433
|
+
const captureObj = this.pattern.captures.find(c => c.getName() === captureName);
|
|
434
|
+
const constraint = captureObj?.getConstraint?.();
|
|
435
|
+
|
|
436
|
+
// Apply constraint if present
|
|
437
|
+
if (constraint && !constraint(target as any)) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Only store the binding if this is a capturing placeholder
|
|
442
|
+
const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
|
|
443
|
+
if (capturing) {
|
|
444
|
+
// Store wrapper if available (preserves markers), otherwise store element
|
|
445
|
+
this.storage.set(captureName, wrapper ?? target);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Handles a variadic capture placeholder.
|
|
453
|
+
*
|
|
454
|
+
* @param pattern The pattern node (the variadic capture)
|
|
455
|
+
* @param targets The target nodes that were matched
|
|
456
|
+
* @param wrappers Optional wrappers to preserve markers
|
|
457
|
+
* @returns true if the capture is successful, false otherwise
|
|
458
|
+
*/
|
|
459
|
+
private handleVariadicCapture(pattern: J, targets: J[], wrappers?: J.RightPadded<J>[]): boolean {
|
|
460
|
+
const captureName = PlaceholderUtils.getCaptureName(pattern);
|
|
461
|
+
|
|
462
|
+
if (!captureName) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Find the original capture object to get constraint and capturing flag
|
|
467
|
+
const captureObj = this.pattern.captures.find(c => c.getName() === captureName);
|
|
468
|
+
const constraint = captureObj?.getConstraint?.();
|
|
469
|
+
|
|
470
|
+
// Apply constraint if present - for variadic captures, constraint receives the array of elements
|
|
471
|
+
if (constraint && !constraint(targets as any)) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Only store the binding if this is a capturing placeholder
|
|
476
|
+
const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
|
|
477
|
+
if (capturing) {
|
|
478
|
+
// Store the richest representation: wrappers if available, otherwise elements
|
|
479
|
+
if (wrappers && wrappers.length > 0) {
|
|
480
|
+
this.storage.set(captureName, wrappers);
|
|
481
|
+
} else {
|
|
482
|
+
this.storage.set(captureName, targets);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Processor for template strings.
|
|
492
|
+
* Converts a template string with captures into an AST pattern.
|
|
493
|
+
*/
|
|
494
|
+
class TemplateProcessor {
|
|
495
|
+
/**
|
|
496
|
+
* Creates a new template processor.
|
|
497
|
+
*
|
|
498
|
+
* @param templateParts The string parts of the template
|
|
499
|
+
* @param captures The captures between the string parts (can be Capture or Any)
|
|
500
|
+
* @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
|
|
501
|
+
* @param dependencies NPM dependencies for type attribution
|
|
502
|
+
*/
|
|
503
|
+
constructor(
|
|
504
|
+
private readonly templateParts: TemplateStringsArray,
|
|
505
|
+
private readonly captures: (Capture | Any<any>)[],
|
|
506
|
+
private readonly contextStatements: string[] = [],
|
|
507
|
+
private readonly dependencies: Record<string, string> = {}
|
|
508
|
+
) {
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Converts the template to an AST pattern.
|
|
513
|
+
*
|
|
514
|
+
* @returns A Promise resolving to the AST pattern
|
|
515
|
+
*/
|
|
516
|
+
async toAstPattern(): Promise<J> {
|
|
517
|
+
// Combine template parts and placeholders
|
|
518
|
+
const templateString = this.buildTemplateString();
|
|
519
|
+
|
|
520
|
+
// Use cache to get or parse the compilation unit
|
|
521
|
+
const cu = await templateCache.getOrParse(
|
|
522
|
+
templateString,
|
|
523
|
+
this.captures,
|
|
524
|
+
this.contextStatements,
|
|
525
|
+
this.dependencies
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// Extract the relevant part of the AST
|
|
529
|
+
return this.extractPatternFromAst(cu);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Builds a template string with placeholders for captures.
|
|
534
|
+
* If the template looks like a block pattern, wraps it in a function.
|
|
535
|
+
*
|
|
536
|
+
* @returns The template string
|
|
537
|
+
*/
|
|
538
|
+
private buildTemplateString(): string {
|
|
539
|
+
let result = '';
|
|
540
|
+
for (let i = 0; i < this.templateParts.length; i++) {
|
|
541
|
+
result += this.templateParts[i];
|
|
542
|
+
if (i < this.captures.length) {
|
|
543
|
+
const capture = this.captures[i];
|
|
544
|
+
// Use symbol to access capture name without triggering Proxy
|
|
545
|
+
const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
|
|
546
|
+
result += PlaceholderUtils.createCapture(captureName, undefined);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Check if this looks like a block pattern (starts with { and contains statement keywords)
|
|
551
|
+
const trimmed = result.trim();
|
|
552
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
553
|
+
// Check for statement keywords that indicate this is a block, not an object literal
|
|
554
|
+
const hasStatementKeywords = /\b(return|if|for|while|do|switch|try|throw|break|continue|const|let|var|function|class)\b/.test(result);
|
|
555
|
+
if (hasStatementKeywords) {
|
|
556
|
+
// Wrap in a function to ensure it parses as a block
|
|
557
|
+
return `function __PATTERN__() ${result}`;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Extracts the pattern from the parsed AST.
|
|
566
|
+
*
|
|
567
|
+
* @param cu The compilation unit
|
|
568
|
+
* @returns The extracted pattern
|
|
569
|
+
*/
|
|
570
|
+
private extractPatternFromAst(cu: JS.CompilationUnit): J {
|
|
571
|
+
// Skip context statements to get to the actual pattern code
|
|
572
|
+
const patternStatementIndex = this.contextStatements.length;
|
|
573
|
+
|
|
574
|
+
// Check if we have any statements at the pattern index
|
|
575
|
+
if (!cu.statements || patternStatementIndex >= cu.statements.length) {
|
|
576
|
+
// If there's no statement at the index, but we have exactly one statement
|
|
577
|
+
// and it's a block, it might be the pattern itself (e.g., pattern`{ ... }`)
|
|
578
|
+
if (cu.statements && cu.statements.length === 1 && cu.statements[0].element.kind === J.Kind.Block) {
|
|
579
|
+
return this.attachCaptureMarkers(cu.statements[0].element);
|
|
580
|
+
}
|
|
581
|
+
throw new Error(`No statement found at index ${patternStatementIndex} in compilation unit with ${cu.statements?.length || 0} statements`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Extract the relevant part of the AST based on the template content
|
|
585
|
+
const firstStatement = cu.statements[patternStatementIndex].element;
|
|
586
|
+
|
|
587
|
+
let extracted: J;
|
|
588
|
+
|
|
589
|
+
// Check if this is our wrapper function for block patterns
|
|
590
|
+
if (firstStatement.kind === J.Kind.MethodDeclaration) {
|
|
591
|
+
const method = firstStatement as J.MethodDeclaration;
|
|
592
|
+
if (method.name?.simpleName === '__PATTERN__' && method.body) {
|
|
593
|
+
// Extract the block from the wrapper function
|
|
594
|
+
extracted = method.body;
|
|
595
|
+
} else {
|
|
596
|
+
extracted = firstStatement;
|
|
597
|
+
}
|
|
598
|
+
} else if (firstStatement.kind === JS.Kind.ExpressionStatement) {
|
|
599
|
+
// If the first statement is an expression statement, extract the expression
|
|
600
|
+
extracted = (firstStatement as JS.ExpressionStatement).expression;
|
|
601
|
+
} else {
|
|
602
|
+
// Otherwise, return the statement itself
|
|
603
|
+
extracted = firstStatement;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Attach CaptureMarkers to capture identifiers
|
|
607
|
+
return this.attachCaptureMarkers(extracted);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Attaches CaptureMarkers to capture identifiers in the AST.
|
|
612
|
+
* This allows efficient capture detection without string parsing.
|
|
613
|
+
*
|
|
614
|
+
* @param ast The AST to process
|
|
615
|
+
* @returns The AST with CaptureMarkers attached
|
|
616
|
+
*/
|
|
617
|
+
private attachCaptureMarkers(ast: J): J {
|
|
618
|
+
const visited = new Set<J | object>();
|
|
619
|
+
return produce(ast, draft => {
|
|
620
|
+
this.visitAndAttachMarkers(draft, visited);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Recursively visits AST nodes and attaches CaptureMarkers to capture identifiers.
|
|
626
|
+
* For statement-level captures (identifiers in ExpressionStatement), the marker
|
|
627
|
+
* is attached to the ExpressionStatement itself rather than the nested identifier.
|
|
628
|
+
*
|
|
629
|
+
* @param node The node to visit
|
|
630
|
+
* @param visited Set of already visited nodes to avoid cycles
|
|
631
|
+
*/
|
|
632
|
+
private visitAndAttachMarkers(node: any, visited: Set<J | object>): void {
|
|
633
|
+
if (!node || typeof node !== 'object' || visited.has(node)) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Mark as visited to avoid cycles
|
|
638
|
+
visited.add(node);
|
|
639
|
+
|
|
640
|
+
// Check if this is an ExpressionStatement containing a capture identifier
|
|
641
|
+
// For statement-level captures, we attach the marker to the ExpressionStatement itself
|
|
642
|
+
if (node.kind === JS.Kind.ExpressionStatement &&
|
|
643
|
+
node.expression?.kind === J.Kind.Identifier &&
|
|
644
|
+
node.expression.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
|
|
645
|
+
|
|
646
|
+
const captureInfo = PlaceholderUtils.parseCapture(node.expression.simpleName);
|
|
647
|
+
if (captureInfo) {
|
|
648
|
+
// Initialize markers on the ExpressionStatement
|
|
649
|
+
if (!node.markers) {
|
|
650
|
+
node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
|
|
651
|
+
}
|
|
652
|
+
if (!node.markers.markers) {
|
|
653
|
+
node.markers.markers = [];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Find the original capture object to get variadic options
|
|
657
|
+
const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
|
|
658
|
+
const variadicOptions = captureObj?.getVariadicOptions();
|
|
659
|
+
|
|
660
|
+
// Add CaptureMarker to the ExpressionStatement
|
|
661
|
+
node.markers.markers.push(new CaptureMarker(captureInfo.name, variadicOptions));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// For non-statement captures (expressions), attach marker to the identifier
|
|
665
|
+
else if (node.kind === J.Kind.Identifier && node.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
|
|
666
|
+
const captureInfo = PlaceholderUtils.parseCapture(node.simpleName);
|
|
667
|
+
if (captureInfo) {
|
|
668
|
+
// Initialize markers if needed
|
|
669
|
+
if (!node.markers) {
|
|
670
|
+
node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
|
|
671
|
+
}
|
|
672
|
+
if (!node.markers.markers) {
|
|
673
|
+
node.markers.markers = [];
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Find the original capture object to get variadic options
|
|
677
|
+
const captureObj = this.captures.find(c => c.getName() === captureInfo.name);
|
|
678
|
+
const variadicOptions = captureObj?.getVariadicOptions();
|
|
679
|
+
|
|
680
|
+
// Add CaptureMarker with variadic options if available
|
|
681
|
+
node.markers.markers.push(new CaptureMarker(captureInfo.name, variadicOptions));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Recursively visit all properties
|
|
686
|
+
for (const key in node) {
|
|
687
|
+
if (node.hasOwnProperty(key)) {
|
|
688
|
+
const value = node[key];
|
|
689
|
+
if (Array.isArray(value)) {
|
|
690
|
+
value.forEach(item => this.visitAndAttachMarkers(item, visited));
|
|
691
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
692
|
+
this.visitAndAttachMarkers(value, visited);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Tagged template function for creating patterns.
|
|
701
|
+
*
|
|
702
|
+
* @param strings The string parts of the template
|
|
703
|
+
* @param captures The captures between the string parts (Capture, Any, or string names)
|
|
704
|
+
* @returns A Pattern object
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* // Using the same capture multiple times for repeated patterns
|
|
708
|
+
* const expr = capture('expr');
|
|
709
|
+
* const redundantOr = pattern`${expr} || ${expr}`;
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* // Using any() for non-capturing matches
|
|
713
|
+
* const pat = pattern`foo(${any()})`;
|
|
714
|
+
*/
|
|
715
|
+
export function pattern(strings: TemplateStringsArray, ...captures: (Capture | Any<any> | string)[]): Pattern {
|
|
716
|
+
const capturesByName = captures.reduce((map, c) => {
|
|
717
|
+
const capture = typeof c === "string" ? new CaptureImpl(c) : c;
|
|
718
|
+
// Use symbol to get internal name without triggering Proxy
|
|
719
|
+
const name = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
|
|
720
|
+
return map.set(name, capture);
|
|
721
|
+
}, new Map<string, Capture | Any<any>>());
|
|
722
|
+
return new Pattern(strings, captures.map(c => {
|
|
723
|
+
// Use symbol to get internal name without triggering Proxy
|
|
724
|
+
const name = typeof c === "string" ? c : ((c as any)[CAPTURE_NAME_SYMBOL] || c.getName());
|
|
725
|
+
return capturesByName.get(name)!;
|
|
726
|
+
}));
|
|
727
|
+
}
|