@openrewrite/rewrite 8.66.0-SNAPSHOT → 8.66.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/javascript/comparator.d.ts +91 -5
  2. package/dist/javascript/comparator.d.ts.map +1 -1
  3. package/dist/javascript/comparator.js +679 -3091
  4. package/dist/javascript/comparator.js.map +1 -1
  5. package/dist/javascript/format.d.ts.map +1 -1
  6. package/dist/javascript/format.js +4 -3
  7. package/dist/javascript/format.js.map +1 -1
  8. package/dist/javascript/index.d.ts +1 -1
  9. package/dist/javascript/index.d.ts.map +1 -1
  10. package/dist/javascript/index.js +1 -1
  11. package/dist/javascript/index.js.map +1 -1
  12. package/dist/javascript/parser.d.ts.map +1 -1
  13. package/dist/javascript/parser.js +22 -21
  14. package/dist/javascript/parser.js.map +1 -1
  15. package/dist/javascript/print.d.ts +2 -2
  16. package/dist/javascript/print.d.ts.map +1 -1
  17. package/dist/javascript/print.js +4 -4
  18. package/dist/javascript/print.js.map +1 -1
  19. package/dist/javascript/templating/capture.d.ts +226 -0
  20. package/dist/javascript/templating/capture.d.ts.map +1 -0
  21. package/dist/javascript/templating/capture.js +371 -0
  22. package/dist/javascript/templating/capture.js.map +1 -0
  23. package/dist/javascript/templating/comparator.d.ts +61 -0
  24. package/dist/javascript/templating/comparator.d.ts.map +1 -0
  25. package/dist/javascript/templating/comparator.js +393 -0
  26. package/dist/javascript/templating/comparator.js.map +1 -0
  27. package/dist/javascript/templating/engine.d.ts +75 -0
  28. package/dist/javascript/templating/engine.d.ts.map +1 -0
  29. package/dist/javascript/templating/engine.js +228 -0
  30. package/dist/javascript/templating/engine.js.map +1 -0
  31. package/dist/javascript/templating/index.d.ts +6 -0
  32. package/dist/javascript/templating/index.d.ts.map +1 -0
  33. package/dist/javascript/templating/index.js +42 -0
  34. package/dist/javascript/templating/index.js.map +1 -0
  35. package/dist/javascript/templating/pattern.d.ts +171 -0
  36. package/dist/javascript/templating/pattern.d.ts.map +1 -0
  37. package/dist/javascript/templating/pattern.js +681 -0
  38. package/dist/javascript/templating/pattern.js.map +1 -0
  39. package/dist/javascript/templating/placeholder-replacement.d.ts +58 -0
  40. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -0
  41. package/dist/javascript/templating/placeholder-replacement.js +365 -0
  42. package/dist/javascript/templating/placeholder-replacement.js.map +1 -0
  43. package/dist/javascript/templating/rewrite.d.ts +39 -0
  44. package/dist/javascript/templating/rewrite.d.ts.map +1 -0
  45. package/dist/javascript/templating/rewrite.js +81 -0
  46. package/dist/javascript/templating/rewrite.js.map +1 -0
  47. package/dist/javascript/templating/template.d.ts +204 -0
  48. package/dist/javascript/templating/template.d.ts.map +1 -0
  49. package/dist/javascript/templating/template.js +293 -0
  50. package/dist/javascript/templating/template.js.map +1 -0
  51. package/dist/javascript/templating/types.d.ts +263 -0
  52. package/dist/javascript/templating/types.d.ts.map +1 -0
  53. package/dist/javascript/templating/types.js +3 -0
  54. package/dist/javascript/templating/types.js.map +1 -0
  55. package/dist/javascript/templating/utils.d.ts +118 -0
  56. package/dist/javascript/templating/utils.d.ts.map +1 -0
  57. package/dist/javascript/templating/utils.js +253 -0
  58. package/dist/javascript/templating/utils.js.map +1 -0
  59. package/dist/test/rewrite-test.d.ts.map +1 -1
  60. package/dist/test/rewrite-test.js +65 -9
  61. package/dist/test/rewrite-test.js.map +1 -1
  62. package/dist/version.txt +1 -1
  63. package/package.json +2 -2
  64. package/src/javascript/comparator.ts +721 -3607
  65. package/src/javascript/format.ts +3 -2
  66. package/src/javascript/index.ts +1 -1
  67. package/src/javascript/parser.ts +23 -22
  68. package/src/javascript/print.ts +6 -6
  69. package/src/javascript/templating/capture.ts +503 -0
  70. package/src/javascript/templating/comparator.ts +430 -0
  71. package/src/javascript/templating/engine.ts +252 -0
  72. package/src/javascript/templating/index.ts +60 -0
  73. package/src/javascript/templating/pattern.ts +727 -0
  74. package/src/javascript/templating/placeholder-replacement.ts +372 -0
  75. package/src/javascript/templating/rewrite.ts +95 -0
  76. package/src/javascript/templating/template.ts +326 -0
  77. package/src/javascript/templating/types.ts +300 -0
  78. package/src/javascript/templating/utils.ts +284 -0
  79. package/src/test/rewrite-test.ts +65 -1
  80. package/dist/javascript/templating.d.ts +0 -265
  81. package/dist/javascript/templating.d.ts.map +0 -1
  82. package/dist/javascript/templating.js +0 -1069
  83. package/dist/javascript/templating.js.map +0 -1
  84. package/src/javascript/templating.ts +0 -1277
@@ -1,1277 +0,0 @@
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 {JS} from '.';
17
- import {JavaScriptParser} from './parser';
18
- import {JavaScriptVisitor} from './visitor';
19
- import {Cursor, isTree, Tree} from '..';
20
- import {J, Type} from '../java';
21
- import {produce} from "immer";
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
- }
120
-
121
- /**
122
- * Capture specification for pattern matching.
123
- * Represents a placeholder in a template pattern that can capture a part of the AST.
124
- */
125
- export interface Capture {
126
- /**
127
- * The name of the capture, used to retrieve the captured node later.
128
- */
129
- name: string;
130
- }
131
-
132
- class CaptureImpl implements Capture {
133
- constructor(
134
- public readonly name: string
135
- ) {
136
- }
137
- }
138
-
139
- /**
140
- * Creates a capture specification for use in template patterns.
141
- *
142
- * @param name Optional name for the capture. If not provided, an auto-generated name is used.
143
- * @returns A Capture object
144
- *
145
- * @example
146
- * // Named inline captures
147
- * const pattern = pattern`${capture('left')} + ${capture('right')}`;
148
- *
149
- * // Unnamed captures
150
- * const {left, right} = {left: capture(), right: capture()};
151
- * const pattern = pattern`${left} + ${right}`;
152
- *
153
- * // Repeated patterns using the same capture
154
- * const expr = capture('expr');
155
- * const redundantOr = pattern`${expr} || ${expr}`;
156
- */
157
- export function capture(name?: string): Capture {
158
- if (name) {
159
- return new CaptureImpl(name);
160
- }
161
- return new CaptureImpl(`unnamed_${capture.nextUnnamedId++}`);
162
- }
163
-
164
- // Static counter for generating unique IDs for unnamed captures
165
- capture.nextUnnamedId = 1;
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
-
198
- /**
199
- * Represents a pattern that can be matched against AST nodes.
200
- */
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
-
212
- /**
213
- * Creates a new pattern from template parts and captures.
214
- *
215
- * @param templateParts The string parts of the template
216
- * @param captures The captures between the string parts
217
- */
218
- constructor(
219
- public readonly templateParts: TemplateStringsArray,
220
- public readonly captures: Capture[]
221
- ) {
222
- }
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
-
242
- /**
243
- * Creates a matcher for this pattern against a specific AST node.
244
- *
245
- * @param ast The AST node to match against
246
- * @returns A Matcher object
247
- */
248
- async match(ast: J): Promise<MatchResult | undefined> {
249
- const matcher = new Matcher(this, ast);
250
- const success = await matcher.matches();
251
- return success ? new MatchResult(matcher.getAll()) : undefined;
252
- }
253
- }
254
-
255
- export class MatchResult implements Pick<Map<string, J>, "get"> {
256
- constructor(
257
- private readonly bindings: Map<string, J> = new Map()
258
- ) {
259
- }
260
-
261
- get(capture: Capture | string): J | undefined {
262
- const name = typeof capture === "string" ? capture : capture.name;
263
- return this.bindings.get(name);
264
- }
265
- }
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
-
411
- /**
412
- * Matcher for checking if a pattern matches an AST node and extracting captured nodes.
413
- */
414
- class Matcher {
415
- private readonly bindings = new Map<string, J>();
416
- private patternAst?: J;
417
-
418
- /**
419
- * Creates a new matcher for a pattern against an AST node.
420
- *
421
- * @param pattern The pattern to match
422
- * @param ast The AST node to match against
423
- */
424
- constructor(
425
- private readonly pattern: Pattern,
426
- private readonly ast: J
427
- ) {
428
- }
429
-
430
- /**
431
- * Checks if the pattern matches the AST node.
432
- *
433
- * @returns true if the pattern matches, false otherwise
434
- */
435
- async matches(): Promise<boolean> {
436
- if (!this.patternAst) {
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();
444
- }
445
-
446
- return this.matchNode(this.patternAst, this.ast);
447
- }
448
-
449
- /**
450
- * Gets all captured nodes.
451
- *
452
- * @returns A map of capture names to captured nodes
453
- */
454
- getAll(): Map<string, J> {
455
- return new Map(this.bindings);
456
- }
457
-
458
- /**
459
- * Matches a pattern node against a target node.
460
- *
461
- * @param pattern The pattern node
462
- * @param target The target node
463
- * @returns true if the pattern matches the target, false otherwise
464
- */
465
- private async matchNode(pattern: J, target: J): Promise<boolean> {
466
- // Check if pattern is a capture placeholder
467
- if (PlaceholderUtils.isCapture(pattern)) {
468
- return this.handleCapture(pattern, target);
469
- }
470
-
471
- // Check if nodes have the same kind
472
- if (pattern.kind !== target.kind) {
473
- return false;
474
- }
475
-
476
- const matcher = this;
477
- return await ((new class extends JavaScriptTemplateSemanticallyEqualVisitor {
478
- protected hasSameKind(j: J, other: J): boolean {
479
- return super.hasSameKind(j, other) || j.kind == J.Kind.Identifier && this.matchesParameter(j as J.Identifier, other);
480
- }
481
-
482
- override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
483
- return this.matchesParameter(identifier, other) ? identifier : await super.visitIdentifier(identifier, other);
484
- }
485
-
486
- private matchesParameter(identifier: J.Identifier, other: J): boolean {
487
- return PlaceholderUtils.isCapture(identifier) && matcher.handleCapture(identifier, other);
488
- }
489
- }).compare(pattern, target));
490
- }
491
-
492
- /**
493
- * Handles a capture placeholder.
494
- *
495
- * @param pattern The pattern node
496
- * @param target The target node
497
- * @returns true if the capture is successful, false otherwise
498
- */
499
- private handleCapture(pattern: J, target: J): boolean {
500
- const captureName = PlaceholderUtils.getCaptureName(pattern);
501
-
502
- if (!captureName) {
503
- return false;
504
- }
505
-
506
- // Store the binding
507
- this.bindings.set(captureName, target);
508
- return true;
509
- }
510
- }
511
-
512
- /**
513
- * Tagged template function for creating patterns.
514
- *
515
- * @param strings The string parts of the template
516
- * @param captures The captures between the string parts
517
- * @returns A Pattern object
518
- *
519
- * @example
520
- * // Using the same capture multiple times for repeated patterns
521
- * const expr = capture('expr');
522
- * const redundantOr = pattern`${expr} || ${expr}`;
523
- */
524
- export function pattern(strings: TemplateStringsArray, ...captures: (Capture | string)[]): Pattern {
525
- const capturesByName = captures.reduce((map, c) => {
526
- const capture = typeof c === "string" ? new CaptureImpl(c) : c;
527
- return map.set(capture.name, capture);
528
- }, new Map<string, Capture>());
529
- return new Pattern(strings, captures.map(c => capturesByName.get(typeof c === "string" ? c : c.name)!));
530
- }
531
-
532
- type JavaCoordinates = {
533
- tree?: Tree;
534
- loc?: JavaCoordinates.Location;
535
- mode?: JavaCoordinates.Mode;
536
- };
537
-
538
- namespace JavaCoordinates {
539
- // FIXME need to come up with the equivalent of `Space.Location` support
540
- export type Location = 'EXPRESSION_PREFIX' | 'STATEMENT_PREFIX' | 'BLOCK_END';
541
-
542
- export enum Mode {
543
- Before,
544
- After,
545
- Replace,
546
- }
547
- }
548
-
549
- /**
550
- * Valid parameter types for template literals.
551
- * - Capture: For pattern matching and reuse
552
- * - Tree: AST nodes to be inserted directly
553
- * - Primitives: Values to be converted to literals
554
- */
555
- export type TemplateParameter = Capture | Tree | string | number | boolean;
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
-
575
- /**
576
- * Template for creating AST nodes.
577
- *
578
- * This class provides the public API for template generation.
579
- * The actual templating logic is handled by the internal TemplateEngine.
580
- *
581
- * @example
582
- * // Generate a literal AST node
583
- * const result = template`2`.apply(cursor, coordinates);
584
- *
585
- * @example
586
- * // Generate an AST node with a parameter
587
- * const result = template`${capture()}`.apply(cursor, coordinates);
588
- */
589
- export class Template {
590
- private options: TemplateOptions = {};
591
-
592
- /**
593
- * Creates a new template.
594
- *
595
- * @param templateParts The string parts of the template
596
- * @param parameters The parameters between the string parts
597
- */
598
- constructor(
599
- private readonly templateParts: TemplateStringsArray,
600
- private readonly parameters: Parameter[]
601
- ) {
602
- }
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
-
622
- /**
623
- * Applies this template and returns the resulting tree.
624
- *
625
- * @param cursor The cursor pointing to the current location in the AST
626
- * @param tree Input tree
627
- * @param values values for parameters in template
628
- * @returns A Promise resolving to the generated AST node
629
- */
630
- async apply(cursor: Cursor, tree: J, values?: Pick<Map<string, J>, 'get'>): Promise<J | undefined> {
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
- );
643
- }
644
- }
645
-
646
- export function template(strings: TemplateStringsArray, ...parameters: TemplateParameter[]): Template {
647
- // Convert parameters to Parameter objects (no longer need to check for mutable tree property)
648
- const processedParameters = parameters.map(param => {
649
- // Just wrap each parameter value in a Parameter object
650
- return {value: param};
651
- });
652
-
653
- return new Template(strings, processedParameters);
654
- }
655
-
656
- /**
657
- * Parameter specification for template generation.
658
- * Represents a placeholder in a template that will be replaced with a parameter value.
659
- */
660
- interface Parameter {
661
- /**
662
- * The value to substitute into the template.
663
- */
664
- value: any;
665
- }
666
-
667
- /**
668
- * Internal template engine - handles the core templating logic.
669
- * Not exported, so only visible within this module.
670
- */
671
- class TemplateEngine {
672
- /**
673
- * Applies a template with optional match results from pattern matching.
674
- *
675
- * @param templateParts The string parts of the template
676
- * @param parameters The parameters between the string parts
677
- * @param cursor The cursor pointing to the current location in the AST
678
- * @param coordinates The coordinates specifying where and how to insert the generated AST
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
682
- * @returns A Promise resolving to the generated AST node
683
- */
684
- static async applyTemplate(
685
- templateParts: TemplateStringsArray,
686
- parameters: Parameter[],
687
- cursor: Cursor,
688
- coordinates: JavaCoordinates,
689
- values: Pick<Map<string, J>, 'get'> = new Map(),
690
- imports: string[] = [],
691
- dependencies: Record<string, string> = {}
692
- ): Promise<J | undefined> {
693
- // Build the template string with parameter placeholders
694
- const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
695
-
696
- // If the template string is empty, return undefined
697
- if (!templateString.trim()) {
698
- return undefined;
699
- }
700
-
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
- );
709
-
710
- // Check if there are any statements
711
- if (!cu.statements || cu.statements.length === 0) {
712
- return undefined;
713
- }
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
-
721
- // Extract the relevant part of the AST
722
- const firstStatement = cu.statements[templateStatementIndex].element;
723
- let extracted = firstStatement.kind === JS.Kind.ExpressionStatement ?
724
- (firstStatement as JS.ExpressionStatement).expression :
725
- firstStatement;
726
-
727
- // Create a copy to avoid sharing cached AST instances
728
- const ast = produce(extracted, draft => {});
729
-
730
- // Create substitutions map for placeholders
731
- const substitutions = new Map<string, Parameter>();
732
- for (let i = 0; i < parameters.length; i++) {
733
- const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
734
- substitutions.set(placeholder, parameters[i]);
735
- }
736
-
737
- // Unsubstitute placeholders with actual parameter values and match results
738
- const visitor = new PlaceholderReplacementVisitor(substitutions, values);
739
- const unsubstitutedAst = (await visitor.visit(ast, null))!;
740
-
741
- // Apply the template to the current AST
742
- return new TemplateApplier(cursor, coordinates, unsubstitutedAst, parameters).apply();
743
- }
744
-
745
- /**
746
- * Builds a template string with parameter placeholders.
747
- *
748
- * @param templateParts The string parts of the template
749
- * @param parameters The parameters between the string parts
750
- * @returns The template string
751
- */
752
- private static buildTemplateString(
753
- templateParts: TemplateStringsArray,
754
- parameters: Parameter[]
755
- ): string {
756
- let result = '';
757
- for (let i = 0; i < templateParts.length; i++) {
758
- result += templateParts[i];
759
- if (i < parameters.length) {
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)) {
764
- const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
765
- result += placeholder;
766
- } else {
767
- result += param;
768
- }
769
- }
770
- }
771
- return result;
772
- }
773
- }
774
-
775
- /**
776
- * Utility class for managing placeholder naming and parsing.
777
- * Centralizes all logic related to capture placeholders.
778
- */
779
- class PlaceholderUtils {
780
- static readonly CAPTURE_PREFIX = '__capture_';
781
- static readonly PLACEHOLDER_PREFIX = '__PLACEHOLDER_';
782
-
783
- /**
784
- * Checks if a node is a capture placeholder.
785
- *
786
- * @param node The node to check
787
- * @returns true if the node is a capture placeholder, false otherwise
788
- */
789
- static isCapture(node: J): boolean {
790
- // Check for CaptureMarker first (efficient)
791
- for (const marker of node.markers.markers) {
792
- if (marker instanceof CaptureMarker) {
793
- return true;
794
- }
795
- }
796
- return false;
797
- }
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
-
816
- /**
817
- * Parses a capture placeholder to extract name and type constraint.
818
- *
819
- * @param identifier The identifier string to parse
820
- * @returns Object with name and optional type constraint, or null if not a valid capture
821
- */
822
- static parseCapture(identifier: string): { name: string; typeConstraint?: string } | null {
823
- if (!identifier.startsWith(this.CAPTURE_PREFIX)) {
824
- return null;
825
- }
826
-
827
- // Handle unnamed captures: "__capture_unnamed_N__"
828
- if (identifier.startsWith(`${this.CAPTURE_PREFIX}unnamed_`)) {
829
- const match = identifier.match(/__capture_(unnamed_\d+)__/);
830
- return match ? {name: match[1]} : null;
831
- }
832
-
833
- // Handle named captures: "__capture_name__" or "__capture_name_type__"
834
- const match = identifier.match(/__capture_([^_]+)(?:_([^_]+))?__/);
835
- if (!match) {
836
- return null;
837
- }
838
-
839
- return {
840
- name: match[1],
841
- typeConstraint: match[2]
842
- };
843
- }
844
-
845
- /**
846
- * Creates a capture placeholder string.
847
- *
848
- * @param name The capture name
849
- * @param typeConstraint Optional type constraint
850
- * @returns The formatted placeholder string
851
- */
852
- static createCapture(name: string, typeConstraint?: string): string {
853
- return typeConstraint
854
- ? `${this.CAPTURE_PREFIX}${name}_${typeConstraint}__`
855
- : `${this.CAPTURE_PREFIX}${name}__`;
856
- }
857
- }
858
-
859
- /**
860
- * Visitor that replaces placeholder nodes with actual parameter values.
861
- */
862
- class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
863
- constructor(
864
- private readonly substitutions: Map<string, Parameter>,
865
- private readonly values: Pick<Map<string, J>, 'get'> = new Map()
866
- ) {
867
- super();
868
- }
869
-
870
- async visit<R extends J>(tree: J, p: any, parent?: Cursor): Promise<R | undefined> {
871
- // Check if this node is a placeholder
872
- if (this.isPlaceholder(tree)) {
873
- const replacement = this.replacePlaceholder(tree);
874
- if (replacement !== tree) {
875
- return replacement as R;
876
- }
877
- }
878
-
879
- // Continue with normal traversal
880
- return super.visit(tree, p, parent);
881
- }
882
-
883
- /**
884
- * Checks if a node is a placeholder.
885
- *
886
- * @param node The node to check
887
- * @returns True if the node is a placeholder
888
- */
889
- private isPlaceholder(node: J): boolean {
890
- if (node.kind === J.Kind.Identifier) {
891
- const identifier = node as J.Identifier;
892
- return identifier.simpleName.startsWith(PlaceholderUtils.PLACEHOLDER_PREFIX);
893
- } else if (node.kind === J.Kind.Literal) {
894
- const literal = node as J.Literal;
895
- return literal.valueSource?.startsWith(PlaceholderUtils.PLACEHOLDER_PREFIX) || false;
896
- }
897
- return false;
898
- }
899
-
900
- /**
901
- * Replaces a placeholder node with the actual parameter value.
902
- *
903
- * @param placeholder The placeholder node
904
- * @returns The replacement node or the original if not a placeholder
905
- */
906
- private replacePlaceholder(placeholder: J): J {
907
- const placeholderText = this.getPlaceholderText(placeholder);
908
-
909
- if (!placeholderText || !placeholderText.startsWith(PlaceholderUtils.PLACEHOLDER_PREFIX)) {
910
- return placeholder;
911
- }
912
-
913
- // Find the corresponding parameter
914
- const param = this.substitutions.get(placeholderText);
915
- if (!param || param.value === undefined) {
916
- return placeholder;
917
- }
918
-
919
- // If the parameter value is a Capture, look up the matched result
920
- if (param.value instanceof CaptureImpl) {
921
- const matchedNode = this.values.get(param.value.name);
922
- if (matchedNode) {
923
- return produce(matchedNode, draft => {
924
- draft.markers = placeholder.markers;
925
- draft.prefix = placeholder.prefix;
926
- });
927
- }
928
- // If no match found, return placeholder unchanged
929
- return placeholder;
930
- }
931
-
932
- // If the parameter value is an AST node, use it directly
933
- if (isTree(param.value)) {
934
- // Return the AST node, preserving the original prefix
935
- return produce(param.value as J, draft => {
936
- draft.markers = placeholder.markers;
937
- draft.prefix = placeholder.prefix;
938
- });
939
- }
940
-
941
- return placeholder;
942
- }
943
-
944
- /**
945
- * Gets the placeholder text from a node.
946
- *
947
- * @param node The node to get placeholder text from
948
- * @returns The placeholder text or null
949
- */
950
- private getPlaceholderText(node: J): string | null {
951
- if (node.kind === J.Kind.Identifier) {
952
- return (node as J.Identifier).simpleName;
953
- } else if (node.kind === J.Kind.Literal) {
954
- return (node as J.Literal).valueSource || null;
955
- }
956
- return null;
957
- }
958
- }
959
-
960
- /**
961
- * Helper class for applying a template to an AST.
962
- */
963
- class TemplateApplier {
964
- constructor(
965
- private readonly cursor: Cursor,
966
- private readonly coordinates: JavaCoordinates,
967
- private readonly ast: J,
968
- private readonly parameters: Parameter[] = []
969
- ) {
970
- }
971
-
972
- /**
973
- * Applies the template to the current AST.
974
- *
975
- * @returns A Promise resolving to the modified AST
976
- */
977
- async apply(): Promise<J | undefined> {
978
- const {loc} = this.coordinates;
979
-
980
- // Apply the template based on the location and mode
981
- switch (loc || 'EXPRESSION_PREFIX') {
982
- case 'EXPRESSION_PREFIX':
983
- return this.applyToExpression();
984
- case 'STATEMENT_PREFIX':
985
- return this.applyToStatement();
986
- case 'BLOCK_END':
987
- return this.applyToBlock();
988
- default:
989
- throw new Error(`Unsupported location: ${loc}`);
990
- }
991
- }
992
-
993
- /**
994
- * Applies the template to an expression.
995
- *
996
- * @returns A Promise resolving to the modified AST
997
- */
998
- private async applyToExpression(): Promise<J | undefined> {
999
- const {tree} = this.coordinates;
1000
-
1001
- // Create a copy of the AST with the prefix from the target
1002
- return tree ? produce(this.ast, draft => {
1003
- draft.prefix = (tree as J).prefix;
1004
- }) : this.ast;
1005
- }
1006
-
1007
- /**
1008
- * Applies the template to a statement.
1009
- *
1010
- * @returns A Promise resolving to the modified AST
1011
- */
1012
- private async applyToStatement(): Promise<J | undefined> {
1013
- const {tree} = this.coordinates;
1014
-
1015
- // Create a copy of the AST with the prefix from the target
1016
- return produce(this.ast, draft => {
1017
- draft.prefix = (tree as J).prefix;
1018
- });
1019
- }
1020
-
1021
- /**
1022
- * Applies the template to a block.
1023
- *
1024
- * @returns A Promise resolving to the modified AST
1025
- */
1026
- private async applyToBlock(): Promise<J | undefined> {
1027
- const {tree} = this.coordinates;
1028
-
1029
- // Create a copy of the AST with the prefix from the target
1030
- return produce(this.ast, draft => {
1031
- draft.prefix = (tree as J).prefix;
1032
- });
1033
- }
1034
- }
1035
-
1036
- /**
1037
- * Processor for template strings.
1038
- * Converts a template string with captures into an AST pattern.
1039
- */
1040
- class TemplateProcessor {
1041
- /**
1042
- * Creates a new template processor.
1043
- *
1044
- * @param templateParts The string parts of the template
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
1048
- */
1049
- constructor(
1050
- private readonly templateParts: TemplateStringsArray,
1051
- private readonly captures: Capture[],
1052
- private readonly imports: string[] = [],
1053
- private readonly dependencies: Record<string, string> = {}
1054
- ) {
1055
- }
1056
-
1057
- /**
1058
- * Converts the template to an AST pattern.
1059
- *
1060
- * @returns A Promise resolving to the AST pattern
1061
- */
1062
- async toAstPattern(): Promise<J> {
1063
- // Combine template parts and placeholders
1064
- const templateString = this.buildTemplateString();
1065
-
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
-
1074
- // Extract the relevant part of the AST
1075
- return this.extractPatternFromAst(cu);
1076
- }
1077
-
1078
- /**
1079
- * Builds a template string with placeholders for captures.
1080
- *
1081
- * @returns The template string
1082
- */
1083
- private buildTemplateString(): string {
1084
- let result = '';
1085
- for (let i = 0; i < this.templateParts.length; i++) {
1086
- result += this.templateParts[i];
1087
- if (i < this.captures.length) {
1088
- const capture = this.captures[i];
1089
- result += PlaceholderUtils.createCapture(capture.name);
1090
- }
1091
- }
1092
- return result;
1093
- }
1094
-
1095
- /**
1096
- * Extracts the pattern from the parsed AST.
1097
- *
1098
- * @param cu The compilation unit
1099
- * @returns The extracted pattern
1100
- */
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
-
1105
- // Extract the relevant part of the AST based on the template content
1106
- const firstStatement = cu.statements[patternStatementIndex].element;
1107
-
1108
- let extracted: J;
1109
- // If the first statement is an expression statement, extract the expression
1110
- if (firstStatement.kind === JS.Kind.ExpressionStatement) {
1111
- extracted = (firstStatement as JS.ExpressionStatement).expression;
1112
- } else {
1113
- // Otherwise, return the statement itself
1114
- extracted = firstStatement;
1115
- }
1116
-
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
- }
1177
- }
1178
- }
1179
-
1180
- /**
1181
- * Represents a replacement rule that can match a pattern and apply a template.
1182
- */
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
- */
1193
- tryOn(cursor: Cursor, node: J): Promise<J | undefined>;
1194
- }
1195
-
1196
- /**
1197
- * Configuration for a replacement rule.
1198
- */
1199
- export interface RewriteConfig {
1200
- before: Pattern | Pattern[];
1201
- after: Template;
1202
- }
1203
-
1204
- /**
1205
- * Implementation of a replacement rule.
1206
- */
1207
- class RewriteRuleImpl implements RewriteRule {
1208
- constructor(
1209
- private readonly before: Pattern[],
1210
- private readonly after: Template
1211
- ) {
1212
- }
1213
-
1214
- async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
1215
- for (const pattern of this.before) {
1216
- const match = await pattern.match(node);
1217
- if (match) {
1218
- const result = await this.after.apply(cursor, node, match);
1219
- if (result) {
1220
- return result;
1221
- }
1222
- }
1223
- }
1224
-
1225
- // Return undefined if no patterns match
1226
- return undefined;
1227
- }
1228
- }
1229
-
1230
- /**
1231
- * Creates a replacement rule using a capture context and configuration.
1232
- *
1233
- * @param builderFn Function that takes a capture context and returns before/after configuration
1234
- * @returns A replacement rule that can be applied to AST nodes
1235
- *
1236
- * @example
1237
- * // Single pattern
1238
- * const swapOperands = rewrite(() => ({
1239
- * before: pattern`${"left"} + ${"right"}`,
1240
- * after: template`${"right"} + ${"left"}`
1241
- * }));
1242
- *
1243
- * @example
1244
- * // Multiple patterns
1245
- * const normalizeComparisons = rewrite(() => ({
1246
- * before: [
1247
- * pattern`${"left"} == ${"right"}`,
1248
- * pattern`${"left"} === ${"right"}`
1249
- * ],
1250
- * after: template`${"left"} === ${"right"}`
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
- * }
1265
- */
1266
- export function rewrite(
1267
- builderFn: () => RewriteConfig
1268
- ): RewriteRule {
1269
- const config = builderFn();
1270
-
1271
- // Ensure we have valid before and after properties
1272
- if (!config.before || !config.after) {
1273
- throw new Error('Builder function must return an object with before and after properties');
1274
- }
1275
-
1276
- return new RewriteRuleImpl(Array.isArray(config.before) ? config.before : [config.before], config.after);
1277
- }