@openrewrite/rewrite 8.66.0 → 8.66.1

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