@openrewrite/rewrite 8.63.2 → 8.63.4
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/rpc.d.ts +2 -2
- package/dist/java/rpc.d.ts.map +1 -1
- package/dist/java/rpc.js +10 -4
- package/dist/java/rpc.js.map +1 -1
- package/dist/java/type.d.ts +1 -1
- package/dist/java/type.d.ts.map +1 -1
- package/dist/java/type.js +3 -3
- package/dist/java/type.js.map +1 -1
- package/dist/javascript/assertions.d.ts +1 -1
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +35 -65
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/comparator.d.ts +2 -2
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/dependency-workspace.d.ts +44 -0
- package/dist/javascript/dependency-workspace.d.ts.map +1 -0
- package/dist/javascript/dependency-workspace.js +335 -0
- package/dist/javascript/dependency-workspace.js.map +1 -0
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +5 -2
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/preconditions.js +2 -2
- package/dist/javascript/preconditions.js.map +1 -1
- package/dist/javascript/templating.d.ts +110 -5
- package/dist/javascript/templating.d.ts.map +1 -1
- package/dist/javascript/templating.js +412 -38
- package/dist/javascript/templating.js.map +1 -1
- package/dist/javascript/type-mapping.js +2 -2
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/rpc/queue.d.ts +1 -0
- package/dist/rpc/queue.d.ts.map +1 -1
- package/dist/rpc/queue.js +11 -1
- package/dist/rpc/queue.js.map +1 -1
- package/dist/rpc/request/install-recipes.d.ts.map +1 -1
- package/dist/rpc/request/install-recipes.js +116 -21
- package/dist/rpc/request/install-recipes.js.map +1 -1
- package/dist/rpc/server.d.ts.map +1 -1
- package/dist/rpc/server.js +5 -0
- package/dist/rpc/server.js.map +1 -1
- package/dist/test/rewrite-test.d.ts +1 -1
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +27 -5
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/java/rpc.ts +4 -4
- package/src/java/type.ts +3 -3
- package/src/javascript/assertions.ts +14 -21
- package/src/javascript/comparator.ts +2 -2
- package/src/javascript/dependency-workspace.ts +317 -0
- package/src/javascript/parser.ts +6 -3
- package/src/javascript/preconditions.ts +2 -2
- package/src/javascript/templating.ts +535 -44
- package/src/javascript/type-mapping.ts +2 -2
- package/src/rpc/queue.ts +11 -1
- package/src/rpc/request/install-recipes.ts +127 -24
- package/src/rpc/server.ts +5 -0
- package/src/test/rewrite-test.ts +11 -3
|
@@ -17,9 +17,106 @@ import {JS} from '.';
|
|
|
17
17
|
import {JavaScriptParser} from './parser';
|
|
18
18
|
import {JavaScriptVisitor} from './visitor';
|
|
19
19
|
import {Cursor, isTree, Tree} from '..';
|
|
20
|
-
import {J} from '../java';
|
|
20
|
+
import {J, Type} from '../java';
|
|
21
21
|
import {produce} from "immer";
|
|
22
22
|
import {JavaScriptComparatorVisitor} from "./comparator";
|
|
23
|
+
import {DependencyWorkspace} from './dependency-workspace';
|
|
24
|
+
import {Marker} from '../markers';
|
|
25
|
+
import {randomId} from '../uuid';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cache for compiled templates and patterns.
|
|
29
|
+
* Stores parsed ASTs to avoid expensive re-parsing and dependency resolution.
|
|
30
|
+
*/
|
|
31
|
+
class TemplateCache {
|
|
32
|
+
private cache = new Map<string, JS.CompilationUnit>();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generates a cache key from template string, captures, and options.
|
|
36
|
+
*/
|
|
37
|
+
private generateKey(
|
|
38
|
+
templateString: string,
|
|
39
|
+
captures: Capture[],
|
|
40
|
+
imports: string[],
|
|
41
|
+
dependencies: Record<string, string>
|
|
42
|
+
): string {
|
|
43
|
+
// Use the actual template string (with placeholders) as the primary key
|
|
44
|
+
const templateKey = templateString;
|
|
45
|
+
|
|
46
|
+
// Capture names
|
|
47
|
+
const capturesKey = captures.map(c => c.name).join(',');
|
|
48
|
+
|
|
49
|
+
// Imports
|
|
50
|
+
const importsKey = imports.join(';');
|
|
51
|
+
|
|
52
|
+
// Dependencies
|
|
53
|
+
const depsKey = JSON.stringify(dependencies || {});
|
|
54
|
+
|
|
55
|
+
return `${templateKey}::${capturesKey}::${importsKey}::${depsKey}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gets a cached compilation unit or creates and caches a new one.
|
|
60
|
+
*/
|
|
61
|
+
async getOrParse(
|
|
62
|
+
templateString: string,
|
|
63
|
+
captures: Capture[],
|
|
64
|
+
imports: string[],
|
|
65
|
+
dependencies: Record<string, string>
|
|
66
|
+
): Promise<JS.CompilationUnit> {
|
|
67
|
+
const key = this.generateKey(templateString, captures, imports, dependencies);
|
|
68
|
+
|
|
69
|
+
let cu = this.cache.get(key);
|
|
70
|
+
if (cu) {
|
|
71
|
+
return cu;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create workspace if dependencies are provided
|
|
75
|
+
// DependencyWorkspace has its own cache, so multiple templates with
|
|
76
|
+
// the same dependencies will automatically share the same workspace
|
|
77
|
+
let workspaceDir: string | undefined;
|
|
78
|
+
if (dependencies && Object.keys(dependencies).length > 0) {
|
|
79
|
+
workspaceDir = await DependencyWorkspace.getOrCreateWorkspace(dependencies);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Prepend imports for type attribution context
|
|
83
|
+
const fullTemplateString = imports.length > 0
|
|
84
|
+
? imports.join('\n') + '\n' + templateString
|
|
85
|
+
: templateString;
|
|
86
|
+
|
|
87
|
+
// Parse and cache (workspace only needed during parsing)
|
|
88
|
+
const parser = new JavaScriptParser({relativeTo: workspaceDir});
|
|
89
|
+
const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.ts'});
|
|
90
|
+
cu = (await parseGenerator.next()).value as JS.CompilationUnit;
|
|
91
|
+
|
|
92
|
+
this.cache.set(key, cu);
|
|
93
|
+
return cu;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Clears the cache.
|
|
98
|
+
*/
|
|
99
|
+
clear(): void {
|
|
100
|
+
this.cache.clear();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Global cache instance
|
|
105
|
+
const templateCache = new TemplateCache();
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Marker that stores capture metadata on pattern AST nodes.
|
|
109
|
+
* This avoids the need to parse capture names from identifiers during matching.
|
|
110
|
+
*/
|
|
111
|
+
class CaptureMarker implements Marker {
|
|
112
|
+
readonly kind = 'org.openrewrite.javascript.CaptureMarker';
|
|
113
|
+
readonly id = randomId();
|
|
114
|
+
|
|
115
|
+
constructor(
|
|
116
|
+
public readonly captureName: string
|
|
117
|
+
) {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
23
120
|
|
|
24
121
|
/**
|
|
25
122
|
* Capture specification for pattern matching.
|
|
@@ -42,28 +139,76 @@ class CaptureImpl implements Capture {
|
|
|
42
139
|
/**
|
|
43
140
|
* Creates a capture specification for use in template patterns.
|
|
44
141
|
*
|
|
142
|
+
* @param name Optional name for the capture. If not provided, an auto-generated name is used.
|
|
45
143
|
* @returns A Capture object
|
|
46
144
|
*
|
|
47
145
|
* @example
|
|
48
|
-
* //
|
|
146
|
+
* // Named inline captures
|
|
147
|
+
* const pattern = pattern`${capture('left')} + ${capture('right')}`;
|
|
148
|
+
*
|
|
149
|
+
* // Unnamed captures
|
|
49
150
|
* const {left, right} = {left: capture(), right: capture()};
|
|
50
151
|
* const pattern = pattern`${left} + ${right}`;
|
|
51
152
|
*
|
|
52
153
|
* // Repeated patterns using the same capture
|
|
53
|
-
* const expr = capture();
|
|
154
|
+
* const expr = capture('expr');
|
|
54
155
|
* const redundantOr = pattern`${expr} || ${expr}`;
|
|
55
156
|
*/
|
|
56
|
-
export function capture(): Capture {
|
|
157
|
+
export function capture(name?: string): Capture {
|
|
158
|
+
if (name) {
|
|
159
|
+
return new CaptureImpl(name);
|
|
160
|
+
}
|
|
57
161
|
return new CaptureImpl(`unnamed_${capture.nextUnnamedId++}`);
|
|
58
162
|
}
|
|
59
163
|
|
|
60
164
|
// Static counter for generating unique IDs for unnamed captures
|
|
61
165
|
capture.nextUnnamedId = 1;
|
|
62
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Concise alias for `capture`. Works well for inline captures in patterns and templates.
|
|
169
|
+
*
|
|
170
|
+
* @param name Optional name for the capture. If not provided, an auto-generated name is used.
|
|
171
|
+
* @returns A Capture object
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* // Inline captures with _ alias
|
|
175
|
+
* pattern`isDate(${_('dateArg')})`
|
|
176
|
+
* template`${_('dateArg')} instanceof Date`
|
|
177
|
+
*/
|
|
178
|
+
export const _ = capture;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Configuration options for patterns.
|
|
182
|
+
*/
|
|
183
|
+
export interface PatternOptions {
|
|
184
|
+
/**
|
|
185
|
+
* Import statements to provide type attribution context.
|
|
186
|
+
* These are prepended to the pattern when parsing to ensure proper type information.
|
|
187
|
+
*/
|
|
188
|
+
imports?: string[];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* NPM dependencies required for import resolution and type attribution.
|
|
192
|
+
* Maps package names to version specifiers (e.g., { 'util': '^1.0.0' }).
|
|
193
|
+
* The template engine will create a package.json with these dependencies.
|
|
194
|
+
*/
|
|
195
|
+
dependencies?: Record<string, string>;
|
|
196
|
+
}
|
|
197
|
+
|
|
63
198
|
/**
|
|
64
199
|
* Represents a pattern that can be matched against AST nodes.
|
|
65
200
|
*/
|
|
66
201
|
export class Pattern {
|
|
202
|
+
private _options: PatternOptions = {};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Gets the configuration options for this pattern.
|
|
206
|
+
* @readonly
|
|
207
|
+
*/
|
|
208
|
+
get options(): Readonly<PatternOptions> {
|
|
209
|
+
return this._options;
|
|
210
|
+
}
|
|
211
|
+
|
|
67
212
|
/**
|
|
68
213
|
* Creates a new pattern from template parts and captures.
|
|
69
214
|
*
|
|
@@ -76,6 +221,24 @@ export class Pattern {
|
|
|
76
221
|
) {
|
|
77
222
|
}
|
|
78
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Configures this pattern with additional options.
|
|
226
|
+
*
|
|
227
|
+
* @param options Configuration options
|
|
228
|
+
* @returns This pattern for method chaining
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* pattern`isDate(${capture('date')})`
|
|
232
|
+
* .configure({
|
|
233
|
+
* imports: ['import { isDate } from "util"'],
|
|
234
|
+
* dependencies: { 'util': '^1.0.0' }
|
|
235
|
+
* })
|
|
236
|
+
*/
|
|
237
|
+
configure(options: PatternOptions): Pattern {
|
|
238
|
+
this._options = { ...this._options, ...options };
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
79
242
|
/**
|
|
80
243
|
* Creates a matcher for this pattern against a specific AST node.
|
|
81
244
|
*
|
|
@@ -101,13 +264,156 @@ export class MatchResult implements Pick<Map<string, J>, "get"> {
|
|
|
101
264
|
}
|
|
102
265
|
}
|
|
103
266
|
|
|
267
|
+
/**
|
|
268
|
+
* A comparator visitor that checks semantic equality including type attribution.
|
|
269
|
+
* This ensures that patterns only match code with compatible types, not just
|
|
270
|
+
* structurally similar code.
|
|
271
|
+
*/
|
|
272
|
+
class JavaScriptTemplateSemanticallyEqualVisitor extends JavaScriptComparatorVisitor {
|
|
273
|
+
/**
|
|
274
|
+
* Checks if two types are semantically equal.
|
|
275
|
+
* For method types, this checks that the declaring type and method name match.
|
|
276
|
+
*/
|
|
277
|
+
private isOfType(target?: Type, source?: Type): boolean {
|
|
278
|
+
if (!target || !source) {
|
|
279
|
+
return target === source;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (target.kind !== source.kind) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// For method types, check declaring type
|
|
287
|
+
// Note: We don't check the name field because it might not be fully resolved in patterns
|
|
288
|
+
// The method invocation visitor already checks that simple names match
|
|
289
|
+
if (target.kind === Type.Kind.Method && source.kind === Type.Kind.Method) {
|
|
290
|
+
const targetMethod = target as Type.Method;
|
|
291
|
+
const sourceMethod = source as Type.Method;
|
|
292
|
+
|
|
293
|
+
// Only check that declaring types match
|
|
294
|
+
return this.isOfType(targetMethod.declaringType, sourceMethod.declaringType);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// For fully qualified types, check the fully qualified name
|
|
298
|
+
if (Type.isFullyQualified(target) && Type.isFullyQualified(source)) {
|
|
299
|
+
return Type.FullyQualified.getFullyQualifiedName(target) ===
|
|
300
|
+
Type.FullyQualified.getFullyQualifiedName(source);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Default: types are equal if they're the same kind
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Override method invocation comparison to include type attribution checking.
|
|
309
|
+
* When types match semantically, we allow matching even if one has a receiver
|
|
310
|
+
* and the other doesn't (e.g., `isDate(x)` vs `util.isDate(x)`).
|
|
311
|
+
*/
|
|
312
|
+
override async visitMethodInvocation(method: J.MethodInvocation, other: J): Promise<J | undefined> {
|
|
313
|
+
if (other.kind !== J.Kind.MethodInvocation) {
|
|
314
|
+
return method;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const otherMethod = other as J.MethodInvocation;
|
|
318
|
+
|
|
319
|
+
// Check basic structural equality first
|
|
320
|
+
if (method.name.simpleName !== otherMethod.name.simpleName ||
|
|
321
|
+
method.arguments.elements.length !== otherMethod.arguments.elements.length) {
|
|
322
|
+
this.abort();
|
|
323
|
+
return method;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check type attribution
|
|
327
|
+
// Both must have method types for semantic equality
|
|
328
|
+
if (!method.methodType || !otherMethod.methodType) {
|
|
329
|
+
// If template has type but target doesn't, they don't match
|
|
330
|
+
if (method.methodType || otherMethod.methodType) {
|
|
331
|
+
this.abort();
|
|
332
|
+
return method;
|
|
333
|
+
}
|
|
334
|
+
// If neither has type, fall through to structural comparison
|
|
335
|
+
return super.visitMethodInvocation(method, other);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Both have types - check they match semantically
|
|
339
|
+
const typesMatch = this.isOfType(method.methodType, otherMethod.methodType);
|
|
340
|
+
if (!typesMatch) {
|
|
341
|
+
// Types don't match - abort comparison
|
|
342
|
+
this.abort();
|
|
343
|
+
return method;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Types match! Now we can ignore receiver differences and just compare arguments.
|
|
347
|
+
// This allows pattern `isDate(x)` to match both `isDate(x)` and `util.isDate(x)`
|
|
348
|
+
// when they have the same type attribution.
|
|
349
|
+
|
|
350
|
+
// Compare type parameters
|
|
351
|
+
if ((method.typeParameters === undefined) !== (otherMethod.typeParameters === undefined)) {
|
|
352
|
+
this.abort();
|
|
353
|
+
return method;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (method.typeParameters && otherMethod.typeParameters) {
|
|
357
|
+
if (method.typeParameters.elements.length !== otherMethod.typeParameters.elements.length) {
|
|
358
|
+
this.abort();
|
|
359
|
+
return method;
|
|
360
|
+
}
|
|
361
|
+
for (let i = 0; i < method.typeParameters.elements.length; i++) {
|
|
362
|
+
await this.visit(method.typeParameters.elements[i].element, otherMethod.typeParameters.elements[i].element);
|
|
363
|
+
if (!this.match) return method;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Compare name (already checked simpleName above, but visit for markers/prefix)
|
|
368
|
+
await this.visit(method.name, otherMethod.name);
|
|
369
|
+
if (!this.match) return method;
|
|
370
|
+
|
|
371
|
+
// Compare arguments
|
|
372
|
+
for (let i = 0; i < method.arguments.elements.length; i++) {
|
|
373
|
+
await this.visit(method.arguments.elements[i].element, otherMethod.arguments.elements[i].element);
|
|
374
|
+
if (!this.match) return method;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return method;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Override identifier comparison to include type checking for field access.
|
|
382
|
+
*/
|
|
383
|
+
override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
|
|
384
|
+
if (other.kind !== J.Kind.Identifier) {
|
|
385
|
+
return identifier;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const otherIdentifier = other as J.Identifier;
|
|
389
|
+
|
|
390
|
+
// Check name matches
|
|
391
|
+
if (identifier.simpleName !== otherIdentifier.simpleName) {
|
|
392
|
+
return identifier;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// For identifiers with field types, check type attribution
|
|
396
|
+
if (identifier.fieldType && otherIdentifier.fieldType) {
|
|
397
|
+
if (!this.isOfType(identifier.fieldType, otherIdentifier.fieldType)) {
|
|
398
|
+
this.abort();
|
|
399
|
+
return identifier;
|
|
400
|
+
}
|
|
401
|
+
} else if (identifier.fieldType || otherIdentifier.fieldType) {
|
|
402
|
+
// If only one has a type, they don't match
|
|
403
|
+
this.abort();
|
|
404
|
+
return identifier;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return super.visitIdentifier(identifier, other);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
104
411
|
/**
|
|
105
412
|
* Matcher for checking if a pattern matches an AST node and extracting captured nodes.
|
|
106
413
|
*/
|
|
107
414
|
class Matcher {
|
|
108
415
|
private readonly bindings = new Map<string, J>();
|
|
109
416
|
private patternAst?: J;
|
|
110
|
-
private templateProcessor?: TemplateProcessor;
|
|
111
417
|
|
|
112
418
|
/**
|
|
113
419
|
* Creates a new matcher for a pattern against an AST node.
|
|
@@ -128,8 +434,13 @@ class Matcher {
|
|
|
128
434
|
*/
|
|
129
435
|
async matches(): Promise<boolean> {
|
|
130
436
|
if (!this.patternAst) {
|
|
131
|
-
|
|
132
|
-
|
|
437
|
+
const templateProcessor = new TemplateProcessor(
|
|
438
|
+
this.pattern.templateParts,
|
|
439
|
+
this.pattern.captures,
|
|
440
|
+
this.pattern.options.imports || [],
|
|
441
|
+
this.pattern.options.dependencies || {}
|
|
442
|
+
);
|
|
443
|
+
this.patternAst = await templateProcessor.toAstPattern();
|
|
133
444
|
}
|
|
134
445
|
|
|
135
446
|
return this.matchNode(this.patternAst, this.ast);
|
|
@@ -163,7 +474,7 @@ class Matcher {
|
|
|
163
474
|
}
|
|
164
475
|
|
|
165
476
|
const matcher = this;
|
|
166
|
-
return await ((new class extends
|
|
477
|
+
return await ((new class extends JavaScriptTemplateSemanticallyEqualVisitor {
|
|
167
478
|
protected hasSameKind(j: J, other: J): boolean {
|
|
168
479
|
return super.hasSameKind(j, other) || j.kind == J.Kind.Identifier && this.matchesParameter(j as J.Identifier, other);
|
|
169
480
|
}
|
|
@@ -173,8 +484,7 @@ class Matcher {
|
|
|
173
484
|
}
|
|
174
485
|
|
|
175
486
|
private matchesParameter(identifier: J.Identifier, other: J): boolean {
|
|
176
|
-
return PlaceholderUtils.isCapture(identifier) &&
|
|
177
|
-
matcher.handleCapture(identifier, other);
|
|
487
|
+
return PlaceholderUtils.isCapture(identifier) && matcher.handleCapture(identifier, other);
|
|
178
488
|
}
|
|
179
489
|
}).compare(pattern, target));
|
|
180
490
|
}
|
|
@@ -187,15 +497,14 @@ class Matcher {
|
|
|
187
497
|
* @returns true if the capture is successful, false otherwise
|
|
188
498
|
*/
|
|
189
499
|
private handleCapture(pattern: J, target: J): boolean {
|
|
190
|
-
const
|
|
191
|
-
const captureInfo = PlaceholderUtils.parseCapture(id.simpleName);
|
|
500
|
+
const captureName = PlaceholderUtils.getCaptureName(pattern);
|
|
192
501
|
|
|
193
|
-
if (!
|
|
502
|
+
if (!captureName) {
|
|
194
503
|
return false;
|
|
195
504
|
}
|
|
196
505
|
|
|
197
506
|
// Store the binding
|
|
198
|
-
this.bindings.set(
|
|
507
|
+
this.bindings.set(captureName, target);
|
|
199
508
|
return true;
|
|
200
509
|
}
|
|
201
510
|
}
|
|
@@ -245,6 +554,24 @@ namespace JavaCoordinates {
|
|
|
245
554
|
*/
|
|
246
555
|
export type TemplateParameter = Capture | Tree | string | number | boolean;
|
|
247
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Configuration options for templates.
|
|
559
|
+
*/
|
|
560
|
+
export interface TemplateOptions {
|
|
561
|
+
/**
|
|
562
|
+
* Import statements to provide type attribution context.
|
|
563
|
+
* These are prepended to the template when parsing to ensure proper type information.
|
|
564
|
+
*/
|
|
565
|
+
imports?: string[];
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* NPM dependencies required for import resolution and type attribution.
|
|
569
|
+
* Maps package names to version specifiers (e.g., { 'util': '^1.0.0' }).
|
|
570
|
+
* The template engine will create a package.json with these dependencies.
|
|
571
|
+
*/
|
|
572
|
+
dependencies?: Record<string, string>;
|
|
573
|
+
}
|
|
574
|
+
|
|
248
575
|
/**
|
|
249
576
|
* Template for creating AST nodes.
|
|
250
577
|
*
|
|
@@ -260,6 +587,8 @@ export type TemplateParameter = Capture | Tree | string | number | boolean;
|
|
|
260
587
|
* const result = template`${capture()}`.apply(cursor, coordinates);
|
|
261
588
|
*/
|
|
262
589
|
export class Template {
|
|
590
|
+
private options: TemplateOptions = {};
|
|
591
|
+
|
|
263
592
|
/**
|
|
264
593
|
* Creates a new template.
|
|
265
594
|
*
|
|
@@ -272,6 +601,24 @@ export class Template {
|
|
|
272
601
|
) {
|
|
273
602
|
}
|
|
274
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Configures this template with additional options.
|
|
606
|
+
*
|
|
607
|
+
* @param options Configuration options
|
|
608
|
+
* @returns This template for method chaining
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* template`isDate(${capture('date')})`
|
|
612
|
+
* .configure({
|
|
613
|
+
* imports: ['import { isDate } from "util"'],
|
|
614
|
+
* dependencies: { 'util': '^1.0.0' }
|
|
615
|
+
* })
|
|
616
|
+
*/
|
|
617
|
+
configure(options: TemplateOptions): Template {
|
|
618
|
+
this.options = { ...this.options, ...options };
|
|
619
|
+
return this;
|
|
620
|
+
}
|
|
621
|
+
|
|
275
622
|
/**
|
|
276
623
|
* Applies this template and returns the resulting tree.
|
|
277
624
|
*
|
|
@@ -281,10 +628,18 @@ export class Template {
|
|
|
281
628
|
* @returns A Promise resolving to the generated AST node
|
|
282
629
|
*/
|
|
283
630
|
async apply(cursor: Cursor, tree: J, values?: Pick<Map<string, J>, 'get'>): Promise<J | undefined> {
|
|
284
|
-
return TemplateEngine.applyTemplate(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
631
|
+
return TemplateEngine.applyTemplate(
|
|
632
|
+
this.templateParts,
|
|
633
|
+
this.parameters,
|
|
634
|
+
cursor,
|
|
635
|
+
{
|
|
636
|
+
tree,
|
|
637
|
+
mode: JavaCoordinates.Mode.Replace
|
|
638
|
+
},
|
|
639
|
+
values,
|
|
640
|
+
this.options.imports || [],
|
|
641
|
+
this.options.dependencies || {}
|
|
642
|
+
);
|
|
288
643
|
}
|
|
289
644
|
}
|
|
290
645
|
|
|
@@ -322,6 +677,8 @@ class TemplateEngine {
|
|
|
322
677
|
* @param cursor The cursor pointing to the current location in the AST
|
|
323
678
|
* @param coordinates The coordinates specifying where and how to insert the generated AST
|
|
324
679
|
* @param values Map of capture names to values to replace the parameters with
|
|
680
|
+
* @param imports Import statements to prepend for type attribution
|
|
681
|
+
* @param dependencies NPM dependencies for type attribution
|
|
325
682
|
* @returns A Promise resolving to the generated AST node
|
|
326
683
|
*/
|
|
327
684
|
static async applyTemplate(
|
|
@@ -329,7 +686,9 @@ class TemplateEngine {
|
|
|
329
686
|
parameters: Parameter[],
|
|
330
687
|
cursor: Cursor,
|
|
331
688
|
coordinates: JavaCoordinates,
|
|
332
|
-
values: Pick<Map<string, J>, 'get'> = new Map()
|
|
689
|
+
values: Pick<Map<string, J>, 'get'> = new Map(),
|
|
690
|
+
imports: string[] = [],
|
|
691
|
+
dependencies: Record<string, string> = {}
|
|
333
692
|
): Promise<J | undefined> {
|
|
334
693
|
// Build the template string with parameter placeholders
|
|
335
694
|
const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
|
|
@@ -339,27 +698,40 @@ class TemplateEngine {
|
|
|
339
698
|
return undefined;
|
|
340
699
|
}
|
|
341
700
|
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
701
|
+
// Use cache to get or parse the compilation unit
|
|
702
|
+
// For templates, we don't have captures, so use empty array
|
|
703
|
+
const cu = await templateCache.getOrParse(
|
|
704
|
+
templateString,
|
|
705
|
+
[], // templates don't have captures in the cache key
|
|
706
|
+
imports,
|
|
707
|
+
dependencies
|
|
708
|
+
);
|
|
346
709
|
|
|
347
710
|
// Check if there are any statements
|
|
348
711
|
if (!cu.statements || cu.statements.length === 0) {
|
|
349
712
|
return undefined;
|
|
350
713
|
}
|
|
351
714
|
|
|
715
|
+
// Skip import statements to get to the actual template code
|
|
716
|
+
const templateStatementIndex = imports.length;
|
|
717
|
+
if (templateStatementIndex >= cu.statements.length) {
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
|
|
352
721
|
// Extract the relevant part of the AST
|
|
353
|
-
const firstStatement = cu.statements[
|
|
354
|
-
|
|
722
|
+
const firstStatement = cu.statements[templateStatementIndex].element;
|
|
723
|
+
let extracted = firstStatement.kind === JS.Kind.ExpressionStatement ?
|
|
355
724
|
(firstStatement as JS.ExpressionStatement).expression :
|
|
356
725
|
firstStatement;
|
|
357
726
|
|
|
727
|
+
// Create a copy to avoid sharing cached AST instances
|
|
728
|
+
const ast = produce(extracted, draft => {});
|
|
729
|
+
|
|
358
730
|
// Create substitutions map for placeholders
|
|
359
731
|
const substitutions = new Map<string, Parameter>();
|
|
360
732
|
for (let i = 0; i < parameters.length; i++) {
|
|
361
733
|
const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
|
|
362
|
-
substitutions.set(placeholder,
|
|
734
|
+
substitutions.set(placeholder, parameters[i]);
|
|
363
735
|
}
|
|
364
736
|
|
|
365
737
|
// Unsubstitute placeholders with actual parameter values and match results
|
|
@@ -377,16 +749,22 @@ class TemplateEngine {
|
|
|
377
749
|
* @param parameters The parameters between the string parts
|
|
378
750
|
* @returns The template string
|
|
379
751
|
*/
|
|
380
|
-
private static buildTemplateString(
|
|
752
|
+
private static buildTemplateString(
|
|
753
|
+
templateParts: TemplateStringsArray,
|
|
754
|
+
parameters: Parameter[]
|
|
755
|
+
): string {
|
|
381
756
|
let result = '';
|
|
382
757
|
for (let i = 0; i < templateParts.length; i++) {
|
|
383
758
|
result += templateParts[i];
|
|
384
759
|
if (i < parameters.length) {
|
|
385
|
-
|
|
760
|
+
const param = parameters[i].value;
|
|
761
|
+
// Use a placeholder for Captures and Tree nodes
|
|
762
|
+
// Inline everything else (strings, numbers, booleans) directly
|
|
763
|
+
if (param instanceof CaptureImpl || isTree(param)) {
|
|
386
764
|
const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
|
|
387
765
|
result += placeholder;
|
|
388
766
|
} else {
|
|
389
|
-
result +=
|
|
767
|
+
result += param;
|
|
390
768
|
}
|
|
391
769
|
}
|
|
392
770
|
}
|
|
@@ -409,13 +787,32 @@ class PlaceholderUtils {
|
|
|
409
787
|
* @returns true if the node is a capture placeholder, false otherwise
|
|
410
788
|
*/
|
|
411
789
|
static isCapture(node: J): boolean {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
790
|
+
// Check for CaptureMarker first (efficient)
|
|
791
|
+
for (const marker of node.markers.markers) {
|
|
792
|
+
if (marker instanceof CaptureMarker) {
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
415
795
|
}
|
|
416
796
|
return false;
|
|
417
797
|
}
|
|
418
798
|
|
|
799
|
+
/**
|
|
800
|
+
* Gets the capture name from a node with a CaptureMarker.
|
|
801
|
+
*
|
|
802
|
+
* @param node The node to extract capture name from
|
|
803
|
+
* @returns The capture name, or null if not a capture
|
|
804
|
+
*/
|
|
805
|
+
static getCaptureName(node: J): string | undefined {
|
|
806
|
+
// Check for CaptureMarker
|
|
807
|
+
for (const marker of node.markers.markers) {
|
|
808
|
+
if (marker instanceof CaptureMarker) {
|
|
809
|
+
return marker.captureName;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
|
|
419
816
|
/**
|
|
420
817
|
* Parses a capture placeholder to extract name and type constraint.
|
|
421
818
|
*
|
|
@@ -646,10 +1043,14 @@ class TemplateProcessor {
|
|
|
646
1043
|
*
|
|
647
1044
|
* @param templateParts The string parts of the template
|
|
648
1045
|
* @param captures The captures between the string parts
|
|
1046
|
+
* @param imports Import statements to prepend for type attribution
|
|
1047
|
+
* @param dependencies NPM dependencies for type attribution
|
|
649
1048
|
*/
|
|
650
1049
|
constructor(
|
|
651
1050
|
private readonly templateParts: TemplateStringsArray,
|
|
652
|
-
private readonly captures: Capture[]
|
|
1051
|
+
private readonly captures: Capture[],
|
|
1052
|
+
private readonly imports: string[] = [],
|
|
1053
|
+
private readonly dependencies: Record<string, string> = {}
|
|
653
1054
|
) {
|
|
654
1055
|
}
|
|
655
1056
|
|
|
@@ -662,10 +1063,14 @@ class TemplateProcessor {
|
|
|
662
1063
|
// Combine template parts and placeholders
|
|
663
1064
|
const templateString = this.buildTemplateString();
|
|
664
1065
|
|
|
665
|
-
//
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
1066
|
+
// Use cache to get or parse the compilation unit
|
|
1067
|
+
const cu = await templateCache.getOrParse(
|
|
1068
|
+
templateString,
|
|
1069
|
+
this.captures,
|
|
1070
|
+
this.imports,
|
|
1071
|
+
this.dependencies
|
|
1072
|
+
);
|
|
1073
|
+
|
|
669
1074
|
// Extract the relevant part of the AST
|
|
670
1075
|
return this.extractPatternFromAst(cu);
|
|
671
1076
|
}
|
|
@@ -694,16 +1099,81 @@ class TemplateProcessor {
|
|
|
694
1099
|
* @returns The extracted pattern
|
|
695
1100
|
*/
|
|
696
1101
|
private extractPatternFromAst(cu: JS.CompilationUnit): J {
|
|
1102
|
+
// Skip import statements to get to the actual pattern code
|
|
1103
|
+
const patternStatementIndex = this.imports.length;
|
|
1104
|
+
|
|
697
1105
|
// Extract the relevant part of the AST based on the template content
|
|
698
|
-
const firstStatement = cu.statements[
|
|
1106
|
+
const firstStatement = cu.statements[patternStatementIndex].element;
|
|
699
1107
|
|
|
1108
|
+
let extracted: J;
|
|
700
1109
|
// If the first statement is an expression statement, extract the expression
|
|
701
1110
|
if (firstStatement.kind === JS.Kind.ExpressionStatement) {
|
|
702
|
-
|
|
1111
|
+
extracted = (firstStatement as JS.ExpressionStatement).expression;
|
|
1112
|
+
} else {
|
|
1113
|
+
// Otherwise, return the statement itself
|
|
1114
|
+
extracted = firstStatement;
|
|
703
1115
|
}
|
|
704
1116
|
|
|
705
|
-
//
|
|
706
|
-
return
|
|
1117
|
+
// Attach CaptureMarkers to capture identifiers
|
|
1118
|
+
return this.attachCaptureMarkers(extracted);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Attaches CaptureMarkers to capture identifiers in the AST.
|
|
1123
|
+
* This allows efficient capture detection without string parsing.
|
|
1124
|
+
*
|
|
1125
|
+
* @param ast The AST to process
|
|
1126
|
+
* @returns The AST with CaptureMarkers attached
|
|
1127
|
+
*/
|
|
1128
|
+
private attachCaptureMarkers(ast: J): J {
|
|
1129
|
+
const visited = new Set<any>();
|
|
1130
|
+
return produce(ast, draft => {
|
|
1131
|
+
this.visitAndAttachMarkers(draft, visited);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Recursively visits AST nodes and attaches CaptureMarkers to capture identifiers.
|
|
1137
|
+
*
|
|
1138
|
+
* @param node The node to visit
|
|
1139
|
+
* @param visited Set of already visited nodes to avoid cycles
|
|
1140
|
+
*/
|
|
1141
|
+
private visitAndAttachMarkers(node: any, visited: Set<any>): void {
|
|
1142
|
+
if (!node || typeof node !== 'object' || visited.has(node)) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Mark as visited to avoid cycles
|
|
1147
|
+
visited.add(node);
|
|
1148
|
+
|
|
1149
|
+
// If this is an identifier that looks like a capture, attach a marker
|
|
1150
|
+
if (node.kind === J.Kind.Identifier && node.simpleName?.startsWith(PlaceholderUtils.CAPTURE_PREFIX)) {
|
|
1151
|
+
const captureInfo = PlaceholderUtils.parseCapture(node.simpleName);
|
|
1152
|
+
if (captureInfo) {
|
|
1153
|
+
// Initialize markers if needed
|
|
1154
|
+
if (!node.markers) {
|
|
1155
|
+
node.markers = { kind: 'org.openrewrite.marker.Markers', id: randomId(), markers: [] };
|
|
1156
|
+
}
|
|
1157
|
+
if (!node.markers.markers) {
|
|
1158
|
+
node.markers.markers = [];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Add CaptureMarker
|
|
1162
|
+
node.markers.markers.push(new CaptureMarker(captureInfo.name));
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Recursively visit all properties
|
|
1167
|
+
for (const key in node) {
|
|
1168
|
+
if (node.hasOwnProperty(key)) {
|
|
1169
|
+
const value = node[key];
|
|
1170
|
+
if (Array.isArray(value)) {
|
|
1171
|
+
value.forEach(item => this.visitAndAttachMarkers(item, visited));
|
|
1172
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
1173
|
+
this.visitAndAttachMarkers(value, visited);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
707
1177
|
}
|
|
708
1178
|
}
|
|
709
1179
|
|
|
@@ -711,6 +1181,15 @@ class TemplateProcessor {
|
|
|
711
1181
|
* Represents a replacement rule that can match a pattern and apply a template.
|
|
712
1182
|
*/
|
|
713
1183
|
export interface RewriteRule {
|
|
1184
|
+
/**
|
|
1185
|
+
* Attempts to apply this rewrite rule to the given AST node.
|
|
1186
|
+
*
|
|
1187
|
+
* @param cursor The cursor context at the current position in the AST
|
|
1188
|
+
* @param node The AST node to try matching and transforming
|
|
1189
|
+
* @returns The transformed node if a pattern matched, or `undefined` if no pattern matched.
|
|
1190
|
+
* When using in a visitor, always use the `|| node` pattern to return the original
|
|
1191
|
+
* node when there's no match: `return await rule.tryOn(this.cursor, node) || node;`
|
|
1192
|
+
*/
|
|
714
1193
|
tryOn(cursor: Cursor, node: J): Promise<J | undefined>;
|
|
715
1194
|
}
|
|
716
1195
|
|
|
@@ -735,7 +1214,6 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
735
1214
|
async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
|
|
736
1215
|
for (const pattern of this.before) {
|
|
737
1216
|
const match = await pattern.match(node);
|
|
738
|
-
|
|
739
1217
|
if (match) {
|
|
740
1218
|
const result = await this.after.apply(cursor, node, match);
|
|
741
1219
|
if (result) {
|
|
@@ -757,20 +1235,33 @@ class RewriteRuleImpl implements RewriteRule {
|
|
|
757
1235
|
*
|
|
758
1236
|
* @example
|
|
759
1237
|
* // Single pattern
|
|
760
|
-
* const swapOperands =
|
|
1238
|
+
* const swapOperands = rewrite(() => ({
|
|
761
1239
|
* before: pattern`${"left"} + ${"right"}`,
|
|
762
1240
|
* after: template`${"right"} + ${"left"}`
|
|
763
1241
|
* }));
|
|
764
1242
|
*
|
|
765
1243
|
* @example
|
|
766
1244
|
* // Multiple patterns
|
|
767
|
-
* const normalizeComparisons =
|
|
1245
|
+
* const normalizeComparisons = rewrite(() => ({
|
|
768
1246
|
* before: [
|
|
769
1247
|
* pattern`${"left"} == ${"right"}`,
|
|
770
1248
|
* pattern`${"left"} === ${"right"}`
|
|
771
1249
|
* ],
|
|
772
1250
|
* after: template`${"left"} === ${"right"}`
|
|
773
1251
|
* }));
|
|
1252
|
+
*
|
|
1253
|
+
* @example
|
|
1254
|
+
* // Using in a visitor - IMPORTANT: use `|| node` to handle undefined when no match
|
|
1255
|
+
* class MyVisitor extends JavaScriptVisitor<any> {
|
|
1256
|
+
* override async visitBinary(binary: J.Binary, p: any): Promise<J | undefined> {
|
|
1257
|
+
* const rule = rewrite(() => ({
|
|
1258
|
+
* before: pattern`${capture('a')} + ${capture('b')}`,
|
|
1259
|
+
* after: template`${capture('b')} + ${capture('a')}`
|
|
1260
|
+
* }));
|
|
1261
|
+
* // tryOn() returns undefined if no pattern matches, so always use || node
|
|
1262
|
+
* return await rule.tryOn(this.cursor, binary) || binary;
|
|
1263
|
+
* }
|
|
1264
|
+
* }
|
|
774
1265
|
*/
|
|
775
1266
|
export function rewrite(
|
|
776
1267
|
builderFn: () => RewriteConfig
|