@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
|
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.Template = exports.MatchResult = exports.Pattern = void 0;
|
|
12
|
+
exports.Template = exports.MatchResult = exports.Pattern = exports._ = void 0;
|
|
13
13
|
exports.capture = capture;
|
|
14
14
|
exports.pattern = pattern;
|
|
15
15
|
exports.template = template;
|
|
@@ -36,6 +36,79 @@ const __1 = require("..");
|
|
|
36
36
|
const java_1 = require("../java");
|
|
37
37
|
const immer_1 = require("immer");
|
|
38
38
|
const comparator_1 = require("./comparator");
|
|
39
|
+
const dependency_workspace_1 = require("./dependency-workspace");
|
|
40
|
+
const uuid_1 = require("../uuid");
|
|
41
|
+
/**
|
|
42
|
+
* Cache for compiled templates and patterns.
|
|
43
|
+
* Stores parsed ASTs to avoid expensive re-parsing and dependency resolution.
|
|
44
|
+
*/
|
|
45
|
+
class TemplateCache {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.cache = new Map();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Generates a cache key from template string, captures, and options.
|
|
51
|
+
*/
|
|
52
|
+
generateKey(templateString, captures, imports, dependencies) {
|
|
53
|
+
// Use the actual template string (with placeholders) as the primary key
|
|
54
|
+
const templateKey = templateString;
|
|
55
|
+
// Capture names
|
|
56
|
+
const capturesKey = captures.map(c => c.name).join(',');
|
|
57
|
+
// Imports
|
|
58
|
+
const importsKey = imports.join(';');
|
|
59
|
+
// Dependencies
|
|
60
|
+
const depsKey = JSON.stringify(dependencies || {});
|
|
61
|
+
return `${templateKey}::${capturesKey}::${importsKey}::${depsKey}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Gets a cached compilation unit or creates and caches a new one.
|
|
65
|
+
*/
|
|
66
|
+
getOrParse(templateString, captures, imports, dependencies) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
const key = this.generateKey(templateString, captures, imports, dependencies);
|
|
69
|
+
let cu = this.cache.get(key);
|
|
70
|
+
if (cu) {
|
|
71
|
+
return cu;
|
|
72
|
+
}
|
|
73
|
+
// Create workspace if dependencies are provided
|
|
74
|
+
// DependencyWorkspace has its own cache, so multiple templates with
|
|
75
|
+
// the same dependencies will automatically share the same workspace
|
|
76
|
+
let workspaceDir;
|
|
77
|
+
if (dependencies && Object.keys(dependencies).length > 0) {
|
|
78
|
+
workspaceDir = yield dependency_workspace_1.DependencyWorkspace.getOrCreateWorkspace(dependencies);
|
|
79
|
+
}
|
|
80
|
+
// Prepend imports for type attribution context
|
|
81
|
+
const fullTemplateString = imports.length > 0
|
|
82
|
+
? imports.join('\n') + '\n' + templateString
|
|
83
|
+
: templateString;
|
|
84
|
+
// Parse and cache (workspace only needed during parsing)
|
|
85
|
+
const parser = new parser_1.JavaScriptParser({ relativeTo: workspaceDir });
|
|
86
|
+
const parseGenerator = parser.parse({ text: fullTemplateString, sourcePath: 'template.ts' });
|
|
87
|
+
cu = (yield parseGenerator.next()).value;
|
|
88
|
+
this.cache.set(key, cu);
|
|
89
|
+
return cu;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Clears the cache.
|
|
94
|
+
*/
|
|
95
|
+
clear() {
|
|
96
|
+
this.cache.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Global cache instance
|
|
100
|
+
const templateCache = new TemplateCache();
|
|
101
|
+
/**
|
|
102
|
+
* Marker that stores capture metadata on pattern AST nodes.
|
|
103
|
+
* This avoids the need to parse capture names from identifiers during matching.
|
|
104
|
+
*/
|
|
105
|
+
class CaptureMarker {
|
|
106
|
+
constructor(captureName) {
|
|
107
|
+
this.captureName = captureName;
|
|
108
|
+
this.kind = 'org.openrewrite.javascript.CaptureMarker';
|
|
109
|
+
this.id = (0, uuid_1.randomId)();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
39
112
|
class CaptureImpl {
|
|
40
113
|
constructor(name) {
|
|
41
114
|
this.name = name;
|
|
@@ -44,26 +117,52 @@ class CaptureImpl {
|
|
|
44
117
|
/**
|
|
45
118
|
* Creates a capture specification for use in template patterns.
|
|
46
119
|
*
|
|
120
|
+
* @param name Optional name for the capture. If not provided, an auto-generated name is used.
|
|
47
121
|
* @returns A Capture object
|
|
48
122
|
*
|
|
49
123
|
* @example
|
|
50
|
-
* //
|
|
124
|
+
* // Named inline captures
|
|
125
|
+
* const pattern = pattern`${capture('left')} + ${capture('right')}`;
|
|
126
|
+
*
|
|
127
|
+
* // Unnamed captures
|
|
51
128
|
* const {left, right} = {left: capture(), right: capture()};
|
|
52
129
|
* const pattern = pattern`${left} + ${right}`;
|
|
53
130
|
*
|
|
54
131
|
* // Repeated patterns using the same capture
|
|
55
|
-
* const expr = capture();
|
|
132
|
+
* const expr = capture('expr');
|
|
56
133
|
* const redundantOr = pattern`${expr} || ${expr}`;
|
|
57
134
|
*/
|
|
58
|
-
function capture() {
|
|
135
|
+
function capture(name) {
|
|
136
|
+
if (name) {
|
|
137
|
+
return new CaptureImpl(name);
|
|
138
|
+
}
|
|
59
139
|
return new CaptureImpl(`unnamed_${capture.nextUnnamedId++}`);
|
|
60
140
|
}
|
|
61
141
|
// Static counter for generating unique IDs for unnamed captures
|
|
62
142
|
capture.nextUnnamedId = 1;
|
|
143
|
+
/**
|
|
144
|
+
* Concise alias for `capture`. Works well for inline captures in patterns and templates.
|
|
145
|
+
*
|
|
146
|
+
* @param name Optional name for the capture. If not provided, an auto-generated name is used.
|
|
147
|
+
* @returns A Capture object
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* // Inline captures with _ alias
|
|
151
|
+
* pattern`isDate(${_('dateArg')})`
|
|
152
|
+
* template`${_('dateArg')} instanceof Date`
|
|
153
|
+
*/
|
|
154
|
+
exports._ = capture;
|
|
63
155
|
/**
|
|
64
156
|
* Represents a pattern that can be matched against AST nodes.
|
|
65
157
|
*/
|
|
66
158
|
class Pattern {
|
|
159
|
+
/**
|
|
160
|
+
* Gets the configuration options for this pattern.
|
|
161
|
+
* @readonly
|
|
162
|
+
*/
|
|
163
|
+
get options() {
|
|
164
|
+
return this._options;
|
|
165
|
+
}
|
|
67
166
|
/**
|
|
68
167
|
* Creates a new pattern from template parts and captures.
|
|
69
168
|
*
|
|
@@ -73,6 +172,24 @@ class Pattern {
|
|
|
73
172
|
constructor(templateParts, captures) {
|
|
74
173
|
this.templateParts = templateParts;
|
|
75
174
|
this.captures = captures;
|
|
175
|
+
this._options = {};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Configures this pattern with additional options.
|
|
179
|
+
*
|
|
180
|
+
* @param options Configuration options
|
|
181
|
+
* @returns This pattern for method chaining
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* pattern`isDate(${capture('date')})`
|
|
185
|
+
* .configure({
|
|
186
|
+
* imports: ['import { isDate } from "util"'],
|
|
187
|
+
* dependencies: { 'util': '^1.0.0' }
|
|
188
|
+
* })
|
|
189
|
+
*/
|
|
190
|
+
configure(options) {
|
|
191
|
+
this._options = Object.assign(Object.assign({}, this._options), options);
|
|
192
|
+
return this;
|
|
76
193
|
}
|
|
77
194
|
/**
|
|
78
195
|
* Creates a matcher for this pattern against a specific AST node.
|
|
@@ -99,6 +216,142 @@ class MatchResult {
|
|
|
99
216
|
}
|
|
100
217
|
}
|
|
101
218
|
exports.MatchResult = MatchResult;
|
|
219
|
+
/**
|
|
220
|
+
* A comparator visitor that checks semantic equality including type attribution.
|
|
221
|
+
* This ensures that patterns only match code with compatible types, not just
|
|
222
|
+
* structurally similar code.
|
|
223
|
+
*/
|
|
224
|
+
class JavaScriptTemplateSemanticallyEqualVisitor extends comparator_1.JavaScriptComparatorVisitor {
|
|
225
|
+
/**
|
|
226
|
+
* Checks if two types are semantically equal.
|
|
227
|
+
* For method types, this checks that the declaring type and method name match.
|
|
228
|
+
*/
|
|
229
|
+
isOfType(target, source) {
|
|
230
|
+
if (!target || !source) {
|
|
231
|
+
return target === source;
|
|
232
|
+
}
|
|
233
|
+
if (target.kind !== source.kind) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
// For method types, check declaring type
|
|
237
|
+
// Note: We don't check the name field because it might not be fully resolved in patterns
|
|
238
|
+
// The method invocation visitor already checks that simple names match
|
|
239
|
+
if (target.kind === java_1.Type.Kind.Method && source.kind === java_1.Type.Kind.Method) {
|
|
240
|
+
const targetMethod = target;
|
|
241
|
+
const sourceMethod = source;
|
|
242
|
+
// Only check that declaring types match
|
|
243
|
+
return this.isOfType(targetMethod.declaringType, sourceMethod.declaringType);
|
|
244
|
+
}
|
|
245
|
+
// For fully qualified types, check the fully qualified name
|
|
246
|
+
if (java_1.Type.isFullyQualified(target) && java_1.Type.isFullyQualified(source)) {
|
|
247
|
+
return java_1.Type.FullyQualified.getFullyQualifiedName(target) ===
|
|
248
|
+
java_1.Type.FullyQualified.getFullyQualifiedName(source);
|
|
249
|
+
}
|
|
250
|
+
// Default: types are equal if they're the same kind
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Override method invocation comparison to include type attribution checking.
|
|
255
|
+
* When types match semantically, we allow matching even if one has a receiver
|
|
256
|
+
* and the other doesn't (e.g., `isDate(x)` vs `util.isDate(x)`).
|
|
257
|
+
*/
|
|
258
|
+
visitMethodInvocation(method, other) {
|
|
259
|
+
const _super = Object.create(null, {
|
|
260
|
+
visitMethodInvocation: { get: () => super.visitMethodInvocation }
|
|
261
|
+
});
|
|
262
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
263
|
+
if (other.kind !== java_1.J.Kind.MethodInvocation) {
|
|
264
|
+
return method;
|
|
265
|
+
}
|
|
266
|
+
const otherMethod = other;
|
|
267
|
+
// Check basic structural equality first
|
|
268
|
+
if (method.name.simpleName !== otherMethod.name.simpleName ||
|
|
269
|
+
method.arguments.elements.length !== otherMethod.arguments.elements.length) {
|
|
270
|
+
this.abort();
|
|
271
|
+
return method;
|
|
272
|
+
}
|
|
273
|
+
// Check type attribution
|
|
274
|
+
// Both must have method types for semantic equality
|
|
275
|
+
if (!method.methodType || !otherMethod.methodType) {
|
|
276
|
+
// If template has type but target doesn't, they don't match
|
|
277
|
+
if (method.methodType || otherMethod.methodType) {
|
|
278
|
+
this.abort();
|
|
279
|
+
return method;
|
|
280
|
+
}
|
|
281
|
+
// If neither has type, fall through to structural comparison
|
|
282
|
+
return _super.visitMethodInvocation.call(this, method, other);
|
|
283
|
+
}
|
|
284
|
+
// Both have types - check they match semantically
|
|
285
|
+
const typesMatch = this.isOfType(method.methodType, otherMethod.methodType);
|
|
286
|
+
if (!typesMatch) {
|
|
287
|
+
// Types don't match - abort comparison
|
|
288
|
+
this.abort();
|
|
289
|
+
return method;
|
|
290
|
+
}
|
|
291
|
+
// Types match! Now we can ignore receiver differences and just compare arguments.
|
|
292
|
+
// This allows pattern `isDate(x)` to match both `isDate(x)` and `util.isDate(x)`
|
|
293
|
+
// when they have the same type attribution.
|
|
294
|
+
// Compare type parameters
|
|
295
|
+
if ((method.typeParameters === undefined) !== (otherMethod.typeParameters === undefined)) {
|
|
296
|
+
this.abort();
|
|
297
|
+
return method;
|
|
298
|
+
}
|
|
299
|
+
if (method.typeParameters && otherMethod.typeParameters) {
|
|
300
|
+
if (method.typeParameters.elements.length !== otherMethod.typeParameters.elements.length) {
|
|
301
|
+
this.abort();
|
|
302
|
+
return method;
|
|
303
|
+
}
|
|
304
|
+
for (let i = 0; i < method.typeParameters.elements.length; i++) {
|
|
305
|
+
yield this.visit(method.typeParameters.elements[i].element, otherMethod.typeParameters.elements[i].element);
|
|
306
|
+
if (!this.match)
|
|
307
|
+
return method;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Compare name (already checked simpleName above, but visit for markers/prefix)
|
|
311
|
+
yield this.visit(method.name, otherMethod.name);
|
|
312
|
+
if (!this.match)
|
|
313
|
+
return method;
|
|
314
|
+
// Compare arguments
|
|
315
|
+
for (let i = 0; i < method.arguments.elements.length; i++) {
|
|
316
|
+
yield this.visit(method.arguments.elements[i].element, otherMethod.arguments.elements[i].element);
|
|
317
|
+
if (!this.match)
|
|
318
|
+
return method;
|
|
319
|
+
}
|
|
320
|
+
return method;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Override identifier comparison to include type checking for field access.
|
|
325
|
+
*/
|
|
326
|
+
visitIdentifier(identifier, other) {
|
|
327
|
+
const _super = Object.create(null, {
|
|
328
|
+
visitIdentifier: { get: () => super.visitIdentifier }
|
|
329
|
+
});
|
|
330
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
331
|
+
if (other.kind !== java_1.J.Kind.Identifier) {
|
|
332
|
+
return identifier;
|
|
333
|
+
}
|
|
334
|
+
const otherIdentifier = other;
|
|
335
|
+
// Check name matches
|
|
336
|
+
if (identifier.simpleName !== otherIdentifier.simpleName) {
|
|
337
|
+
return identifier;
|
|
338
|
+
}
|
|
339
|
+
// For identifiers with field types, check type attribution
|
|
340
|
+
if (identifier.fieldType && otherIdentifier.fieldType) {
|
|
341
|
+
if (!this.isOfType(identifier.fieldType, otherIdentifier.fieldType)) {
|
|
342
|
+
this.abort();
|
|
343
|
+
return identifier;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else if (identifier.fieldType || otherIdentifier.fieldType) {
|
|
347
|
+
// If only one has a type, they don't match
|
|
348
|
+
this.abort();
|
|
349
|
+
return identifier;
|
|
350
|
+
}
|
|
351
|
+
return _super.visitIdentifier.call(this, identifier, other);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
102
355
|
/**
|
|
103
356
|
* Matcher for checking if a pattern matches an AST node and extracting captured nodes.
|
|
104
357
|
*/
|
|
@@ -122,8 +375,8 @@ class Matcher {
|
|
|
122
375
|
matches() {
|
|
123
376
|
return __awaiter(this, void 0, void 0, function* () {
|
|
124
377
|
if (!this.patternAst) {
|
|
125
|
-
|
|
126
|
-
this.patternAst = yield
|
|
378
|
+
const templateProcessor = new TemplateProcessor(this.pattern.templateParts, this.pattern.captures, this.pattern.options.imports || [], this.pattern.options.dependencies || {});
|
|
379
|
+
this.patternAst = yield templateProcessor.toAstPattern();
|
|
127
380
|
}
|
|
128
381
|
return this.matchNode(this.patternAst, this.ast);
|
|
129
382
|
});
|
|
@@ -154,7 +407,7 @@ class Matcher {
|
|
|
154
407
|
return false;
|
|
155
408
|
}
|
|
156
409
|
const matcher = this;
|
|
157
|
-
return yield ((new class extends
|
|
410
|
+
return yield ((new class extends JavaScriptTemplateSemanticallyEqualVisitor {
|
|
158
411
|
hasSameKind(j, other) {
|
|
159
412
|
return super.hasSameKind(j, other) || j.kind == java_1.J.Kind.Identifier && this.matchesParameter(j, other);
|
|
160
413
|
}
|
|
@@ -167,8 +420,7 @@ class Matcher {
|
|
|
167
420
|
});
|
|
168
421
|
}
|
|
169
422
|
matchesParameter(identifier, other) {
|
|
170
|
-
return PlaceholderUtils.isCapture(identifier) &&
|
|
171
|
-
matcher.handleCapture(identifier, other);
|
|
423
|
+
return PlaceholderUtils.isCapture(identifier) && matcher.handleCapture(identifier, other);
|
|
172
424
|
}
|
|
173
425
|
}).compare(pattern, target));
|
|
174
426
|
});
|
|
@@ -181,13 +433,12 @@ class Matcher {
|
|
|
181
433
|
* @returns true if the capture is successful, false otherwise
|
|
182
434
|
*/
|
|
183
435
|
handleCapture(pattern, target) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
if (!captureInfo) {
|
|
436
|
+
const captureName = PlaceholderUtils.getCaptureName(pattern);
|
|
437
|
+
if (!captureName) {
|
|
187
438
|
return false;
|
|
188
439
|
}
|
|
189
440
|
// Store the binding
|
|
190
|
-
this.bindings.set(
|
|
441
|
+
this.bindings.set(captureName, target);
|
|
191
442
|
return true;
|
|
192
443
|
}
|
|
193
444
|
}
|
|
@@ -243,6 +494,24 @@ class Template {
|
|
|
243
494
|
constructor(templateParts, parameters) {
|
|
244
495
|
this.templateParts = templateParts;
|
|
245
496
|
this.parameters = parameters;
|
|
497
|
+
this.options = {};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Configures this template with additional options.
|
|
501
|
+
*
|
|
502
|
+
* @param options Configuration options
|
|
503
|
+
* @returns This template for method chaining
|
|
504
|
+
*
|
|
505
|
+
* @example
|
|
506
|
+
* template`isDate(${capture('date')})`
|
|
507
|
+
* .configure({
|
|
508
|
+
* imports: ['import { isDate } from "util"'],
|
|
509
|
+
* dependencies: { 'util': '^1.0.0' }
|
|
510
|
+
* })
|
|
511
|
+
*/
|
|
512
|
+
configure(options) {
|
|
513
|
+
this.options = Object.assign(Object.assign({}, this.options), options);
|
|
514
|
+
return this;
|
|
246
515
|
}
|
|
247
516
|
/**
|
|
248
517
|
* Applies this template and returns the resulting tree.
|
|
@@ -257,7 +526,7 @@ class Template {
|
|
|
257
526
|
return TemplateEngine.applyTemplate(this.templateParts, this.parameters, cursor, {
|
|
258
527
|
tree,
|
|
259
528
|
mode: JavaCoordinates.Mode.Replace
|
|
260
|
-
}, values);
|
|
529
|
+
}, values, this.options.imports || [], this.options.dependencies || {});
|
|
261
530
|
});
|
|
262
531
|
}
|
|
263
532
|
}
|
|
@@ -283,34 +552,43 @@ class TemplateEngine {
|
|
|
283
552
|
* @param cursor The cursor pointing to the current location in the AST
|
|
284
553
|
* @param coordinates The coordinates specifying where and how to insert the generated AST
|
|
285
554
|
* @param values Map of capture names to values to replace the parameters with
|
|
555
|
+
* @param imports Import statements to prepend for type attribution
|
|
556
|
+
* @param dependencies NPM dependencies for type attribution
|
|
286
557
|
* @returns A Promise resolving to the generated AST node
|
|
287
558
|
*/
|
|
288
559
|
static applyTemplate(templateParts_1, parameters_1, cursor_1, coordinates_1) {
|
|
289
|
-
return __awaiter(this, arguments, void 0, function* (templateParts, parameters, cursor, coordinates, values = new Map()) {
|
|
560
|
+
return __awaiter(this, arguments, void 0, function* (templateParts, parameters, cursor, coordinates, values = new Map(), imports = [], dependencies = {}) {
|
|
290
561
|
// Build the template string with parameter placeholders
|
|
291
562
|
const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
|
|
292
563
|
// If the template string is empty, return undefined
|
|
293
564
|
if (!templateString.trim()) {
|
|
294
565
|
return undefined;
|
|
295
566
|
}
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
567
|
+
// Use cache to get or parse the compilation unit
|
|
568
|
+
// For templates, we don't have captures, so use empty array
|
|
569
|
+
const cu = yield templateCache.getOrParse(templateString, [], // templates don't have captures in the cache key
|
|
570
|
+
imports, dependencies);
|
|
300
571
|
// Check if there are any statements
|
|
301
572
|
if (!cu.statements || cu.statements.length === 0) {
|
|
302
573
|
return undefined;
|
|
303
574
|
}
|
|
575
|
+
// Skip import statements to get to the actual template code
|
|
576
|
+
const templateStatementIndex = imports.length;
|
|
577
|
+
if (templateStatementIndex >= cu.statements.length) {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
304
580
|
// Extract the relevant part of the AST
|
|
305
|
-
const firstStatement = cu.statements[
|
|
306
|
-
|
|
581
|
+
const firstStatement = cu.statements[templateStatementIndex].element;
|
|
582
|
+
let extracted = firstStatement.kind === _1.JS.Kind.ExpressionStatement ?
|
|
307
583
|
firstStatement.expression :
|
|
308
584
|
firstStatement;
|
|
585
|
+
// Create a copy to avoid sharing cached AST instances
|
|
586
|
+
const ast = (0, immer_1.produce)(extracted, draft => { });
|
|
309
587
|
// Create substitutions map for placeholders
|
|
310
588
|
const substitutions = new Map();
|
|
311
589
|
for (let i = 0; i < parameters.length; i++) {
|
|
312
590
|
const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
|
|
313
|
-
substitutions.set(placeholder,
|
|
591
|
+
substitutions.set(placeholder, parameters[i]);
|
|
314
592
|
}
|
|
315
593
|
// Unsubstitute placeholders with actual parameter values and match results
|
|
316
594
|
const visitor = new PlaceholderReplacementVisitor(substitutions, values);
|
|
@@ -331,12 +609,15 @@ class TemplateEngine {
|
|
|
331
609
|
for (let i = 0; i < templateParts.length; i++) {
|
|
332
610
|
result += templateParts[i];
|
|
333
611
|
if (i < parameters.length) {
|
|
334
|
-
|
|
612
|
+
const param = parameters[i].value;
|
|
613
|
+
// Use a placeholder for Captures and Tree nodes
|
|
614
|
+
// Inline everything else (strings, numbers, booleans) directly
|
|
615
|
+
if (param instanceof CaptureImpl || (0, __1.isTree)(param)) {
|
|
335
616
|
const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
|
|
336
617
|
result += placeholder;
|
|
337
618
|
}
|
|
338
619
|
else {
|
|
339
|
-
result +=
|
|
620
|
+
result += param;
|
|
340
621
|
}
|
|
341
622
|
}
|
|
342
623
|
}
|
|
@@ -355,12 +636,29 @@ class PlaceholderUtils {
|
|
|
355
636
|
* @returns true if the node is a capture placeholder, false otherwise
|
|
356
637
|
*/
|
|
357
638
|
static isCapture(node) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
639
|
+
// Check for CaptureMarker first (efficient)
|
|
640
|
+
for (const marker of node.markers.markers) {
|
|
641
|
+
if (marker instanceof CaptureMarker) {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
361
644
|
}
|
|
362
645
|
return false;
|
|
363
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Gets the capture name from a node with a CaptureMarker.
|
|
649
|
+
*
|
|
650
|
+
* @param node The node to extract capture name from
|
|
651
|
+
* @returns The capture name, or null if not a capture
|
|
652
|
+
*/
|
|
653
|
+
static getCaptureName(node) {
|
|
654
|
+
// Check for CaptureMarker
|
|
655
|
+
for (const marker of node.markers.markers) {
|
|
656
|
+
if (marker instanceof CaptureMarker) {
|
|
657
|
+
return marker.captureName;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
364
662
|
/**
|
|
365
663
|
* Parses a capture placeholder to extract name and type constraint.
|
|
366
664
|
*
|
|
@@ -582,10 +880,14 @@ class TemplateProcessor {
|
|
|
582
880
|
*
|
|
583
881
|
* @param templateParts The string parts of the template
|
|
584
882
|
* @param captures The captures between the string parts
|
|
883
|
+
* @param imports Import statements to prepend for type attribution
|
|
884
|
+
* @param dependencies NPM dependencies for type attribution
|
|
585
885
|
*/
|
|
586
|
-
constructor(templateParts, captures) {
|
|
886
|
+
constructor(templateParts, captures, imports = [], dependencies = {}) {
|
|
587
887
|
this.templateParts = templateParts;
|
|
588
888
|
this.captures = captures;
|
|
889
|
+
this.imports = imports;
|
|
890
|
+
this.dependencies = dependencies;
|
|
589
891
|
}
|
|
590
892
|
/**
|
|
591
893
|
* Converts the template to an AST pattern.
|
|
@@ -596,10 +898,8 @@ class TemplateProcessor {
|
|
|
596
898
|
return __awaiter(this, void 0, void 0, function* () {
|
|
597
899
|
// Combine template parts and placeholders
|
|
598
900
|
const templateString = this.buildTemplateString();
|
|
599
|
-
//
|
|
600
|
-
const
|
|
601
|
-
const parseGenerator = parser.parse({ text: templateString, sourcePath: 'template.ts' });
|
|
602
|
-
const cu = (yield parseGenerator.next()).value;
|
|
901
|
+
// Use cache to get or parse the compilation unit
|
|
902
|
+
const cu = yield templateCache.getOrParse(templateString, this.captures, this.imports, this.dependencies);
|
|
603
903
|
// Extract the relevant part of the AST
|
|
604
904
|
return this.extractPatternFromAst(cu);
|
|
605
905
|
});
|
|
@@ -627,14 +927,75 @@ class TemplateProcessor {
|
|
|
627
927
|
* @returns The extracted pattern
|
|
628
928
|
*/
|
|
629
929
|
extractPatternFromAst(cu) {
|
|
930
|
+
// Skip import statements to get to the actual pattern code
|
|
931
|
+
const patternStatementIndex = this.imports.length;
|
|
630
932
|
// Extract the relevant part of the AST based on the template content
|
|
631
|
-
const firstStatement = cu.statements[
|
|
933
|
+
const firstStatement = cu.statements[patternStatementIndex].element;
|
|
934
|
+
let extracted;
|
|
632
935
|
// If the first statement is an expression statement, extract the expression
|
|
633
936
|
if (firstStatement.kind === _1.JS.Kind.ExpressionStatement) {
|
|
634
|
-
|
|
937
|
+
extracted = firstStatement.expression;
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
// Otherwise, return the statement itself
|
|
941
|
+
extracted = firstStatement;
|
|
942
|
+
}
|
|
943
|
+
// Attach CaptureMarkers to capture identifiers
|
|
944
|
+
return this.attachCaptureMarkers(extracted);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Attaches CaptureMarkers to capture identifiers in the AST.
|
|
948
|
+
* This allows efficient capture detection without string parsing.
|
|
949
|
+
*
|
|
950
|
+
* @param ast The AST to process
|
|
951
|
+
* @returns The AST with CaptureMarkers attached
|
|
952
|
+
*/
|
|
953
|
+
attachCaptureMarkers(ast) {
|
|
954
|
+
const visited = new Set();
|
|
955
|
+
return (0, immer_1.produce)(ast, draft => {
|
|
956
|
+
this.visitAndAttachMarkers(draft, visited);
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Recursively visits AST nodes and attaches CaptureMarkers to capture identifiers.
|
|
961
|
+
*
|
|
962
|
+
* @param node The node to visit
|
|
963
|
+
* @param visited Set of already visited nodes to avoid cycles
|
|
964
|
+
*/
|
|
965
|
+
visitAndAttachMarkers(node, visited) {
|
|
966
|
+
var _a;
|
|
967
|
+
if (!node || typeof node !== 'object' || visited.has(node)) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
// Mark as visited to avoid cycles
|
|
971
|
+
visited.add(node);
|
|
972
|
+
// If this is an identifier that looks like a capture, attach a marker
|
|
973
|
+
if (node.kind === java_1.J.Kind.Identifier && ((_a = node.simpleName) === null || _a === void 0 ? void 0 : _a.startsWith(PlaceholderUtils.CAPTURE_PREFIX))) {
|
|
974
|
+
const captureInfo = PlaceholderUtils.parseCapture(node.simpleName);
|
|
975
|
+
if (captureInfo) {
|
|
976
|
+
// Initialize markers if needed
|
|
977
|
+
if (!node.markers) {
|
|
978
|
+
node.markers = { kind: 'org.openrewrite.marker.Markers', id: (0, uuid_1.randomId)(), markers: [] };
|
|
979
|
+
}
|
|
980
|
+
if (!node.markers.markers) {
|
|
981
|
+
node.markers.markers = [];
|
|
982
|
+
}
|
|
983
|
+
// Add CaptureMarker
|
|
984
|
+
node.markers.markers.push(new CaptureMarker(captureInfo.name));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
// Recursively visit all properties
|
|
988
|
+
for (const key in node) {
|
|
989
|
+
if (node.hasOwnProperty(key)) {
|
|
990
|
+
const value = node[key];
|
|
991
|
+
if (Array.isArray(value)) {
|
|
992
|
+
value.forEach(item => this.visitAndAttachMarkers(item, visited));
|
|
993
|
+
}
|
|
994
|
+
else if (typeof value === 'object' && value !== null) {
|
|
995
|
+
this.visitAndAttachMarkers(value, visited);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
635
998
|
}
|
|
636
|
-
// Otherwise, return the statement itself
|
|
637
|
-
return firstStatement;
|
|
638
999
|
}
|
|
639
1000
|
}
|
|
640
1001
|
/**
|
|
@@ -669,20 +1030,33 @@ class RewriteRuleImpl {
|
|
|
669
1030
|
*
|
|
670
1031
|
* @example
|
|
671
1032
|
* // Single pattern
|
|
672
|
-
* const swapOperands =
|
|
1033
|
+
* const swapOperands = rewrite(() => ({
|
|
673
1034
|
* before: pattern`${"left"} + ${"right"}`,
|
|
674
1035
|
* after: template`${"right"} + ${"left"}`
|
|
675
1036
|
* }));
|
|
676
1037
|
*
|
|
677
1038
|
* @example
|
|
678
1039
|
* // Multiple patterns
|
|
679
|
-
* const normalizeComparisons =
|
|
1040
|
+
* const normalizeComparisons = rewrite(() => ({
|
|
680
1041
|
* before: [
|
|
681
1042
|
* pattern`${"left"} == ${"right"}`,
|
|
682
1043
|
* pattern`${"left"} === ${"right"}`
|
|
683
1044
|
* ],
|
|
684
1045
|
* after: template`${"left"} === ${"right"}`
|
|
685
1046
|
* }));
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* // Using in a visitor - IMPORTANT: use `|| node` to handle undefined when no match
|
|
1050
|
+
* class MyVisitor extends JavaScriptVisitor<any> {
|
|
1051
|
+
* override async visitBinary(binary: J.Binary, p: any): Promise<J | undefined> {
|
|
1052
|
+
* const rule = rewrite(() => ({
|
|
1053
|
+
* before: pattern`${capture('a')} + ${capture('b')}`,
|
|
1054
|
+
* after: template`${capture('b')} + ${capture('a')}`
|
|
1055
|
+
* }));
|
|
1056
|
+
* // tryOn() returns undefined if no pattern matches, so always use || node
|
|
1057
|
+
* return await rule.tryOn(this.cursor, binary) || binary;
|
|
1058
|
+
* }
|
|
1059
|
+
* }
|
|
686
1060
|
*/
|
|
687
1061
|
function rewrite(builderFn) {
|
|
688
1062
|
const config = builderFn();
|