@openrewrite/rewrite 8.66.0 → 8.66.2

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 (113) hide show
  1. package/dist/java/tree.d.ts +10 -1
  2. package/dist/java/tree.d.ts.map +1 -1
  3. package/dist/java/tree.js +21 -5
  4. package/dist/java/tree.js.map +1 -1
  5. package/dist/java/type-visitor.d.ts +1 -1
  6. package/dist/java/type-visitor.d.ts.map +1 -1
  7. package/dist/java/visitor.d.ts +2 -2
  8. package/dist/java/visitor.d.ts.map +1 -1
  9. package/dist/java/visitor.js +8 -2
  10. package/dist/java/visitor.js.map +1 -1
  11. package/dist/javascript/assertions.d.ts +6 -0
  12. package/dist/javascript/assertions.d.ts.map +1 -1
  13. package/dist/javascript/assertions.js +14 -6
  14. package/dist/javascript/assertions.js.map +1 -1
  15. package/dist/javascript/comparator.d.ts +217 -7
  16. package/dist/javascript/comparator.d.ts.map +1 -1
  17. package/dist/javascript/comparator.js +1020 -2848
  18. package/dist/javascript/comparator.js.map +1 -1
  19. package/dist/javascript/format.d.ts +5 -3
  20. package/dist/javascript/format.d.ts.map +1 -1
  21. package/dist/javascript/format.js +87 -44
  22. package/dist/javascript/format.js.map +1 -1
  23. package/dist/javascript/index.d.ts +2 -1
  24. package/dist/javascript/index.d.ts.map +1 -1
  25. package/dist/javascript/index.js +2 -1
  26. package/dist/javascript/index.js.map +1 -1
  27. package/dist/javascript/parser.d.ts +2 -1
  28. package/dist/javascript/parser.d.ts.map +1 -1
  29. package/dist/javascript/parser.js +54 -43
  30. package/dist/javascript/parser.js.map +1 -1
  31. package/dist/javascript/templating/capture.d.ts +293 -0
  32. package/dist/javascript/templating/capture.d.ts.map +1 -0
  33. package/dist/javascript/templating/capture.js +461 -0
  34. package/dist/javascript/templating/capture.js.map +1 -0
  35. package/dist/javascript/templating/comparator.d.ts +171 -0
  36. package/dist/javascript/templating/comparator.d.ts.map +1 -0
  37. package/dist/javascript/templating/comparator.js +1221 -0
  38. package/dist/javascript/templating/comparator.js.map +1 -0
  39. package/dist/javascript/templating/engine.d.ts +108 -0
  40. package/dist/javascript/templating/engine.d.ts.map +1 -0
  41. package/dist/javascript/templating/engine.js +661 -0
  42. package/dist/javascript/templating/engine.js.map +1 -0
  43. package/dist/javascript/templating/index.d.ts +6 -0
  44. package/dist/javascript/templating/index.d.ts.map +1 -0
  45. package/dist/javascript/templating/index.js +44 -0
  46. package/dist/javascript/templating/index.js.map +1 -0
  47. package/dist/javascript/templating/pattern.d.ts +276 -0
  48. package/dist/javascript/templating/pattern.d.ts.map +1 -0
  49. package/dist/javascript/templating/pattern.js +952 -0
  50. package/dist/javascript/templating/pattern.js.map +1 -0
  51. package/dist/javascript/templating/placeholder-replacement.d.ts +83 -0
  52. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -0
  53. package/dist/javascript/templating/placeholder-replacement.js +467 -0
  54. package/dist/javascript/templating/placeholder-replacement.js.map +1 -0
  55. package/dist/javascript/templating/rewrite.d.ts +84 -0
  56. package/dist/javascript/templating/rewrite.d.ts.map +1 -0
  57. package/dist/javascript/templating/rewrite.js +208 -0
  58. package/dist/javascript/templating/rewrite.js.map +1 -0
  59. package/dist/javascript/templating/template.d.ts +230 -0
  60. package/dist/javascript/templating/template.d.ts.map +1 -0
  61. package/dist/javascript/templating/template.js +367 -0
  62. package/dist/javascript/templating/template.js.map +1 -0
  63. package/dist/javascript/templating/types.d.ts +610 -0
  64. package/dist/javascript/templating/types.d.ts.map +1 -0
  65. package/dist/javascript/templating/types.js +3 -0
  66. package/dist/javascript/templating/types.js.map +1 -0
  67. package/dist/javascript/templating/utils.d.ts +135 -0
  68. package/dist/javascript/templating/utils.d.ts.map +1 -0
  69. package/dist/javascript/templating/utils.js +251 -0
  70. package/dist/javascript/templating/utils.js.map +1 -0
  71. package/dist/javascript/type-mapping.d.ts.map +1 -1
  72. package/dist/javascript/type-mapping.js +21 -11
  73. package/dist/javascript/type-mapping.js.map +1 -1
  74. package/dist/json/rpc.js +2 -2
  75. package/dist/json/rpc.js.map +1 -1
  76. package/dist/recipe/order-imports.js.map +1 -1
  77. package/dist/test/rewrite-test.d.ts.map +1 -1
  78. package/dist/test/rewrite-test.js +10 -6
  79. package/dist/test/rewrite-test.js.map +1 -1
  80. package/dist/version.txt +1 -1
  81. package/dist/visitor.d.ts +4 -4
  82. package/dist/visitor.d.ts.map +1 -1
  83. package/dist/visitor.js +8 -3
  84. package/dist/visitor.js.map +1 -1
  85. package/package.json +5 -2
  86. package/src/java/tree.ts +10 -3
  87. package/src/java/type-visitor.ts +1 -1
  88. package/src/java/visitor.ts +11 -5
  89. package/src/javascript/assertions.ts +9 -3
  90. package/src/javascript/comparator.ts +1095 -3373
  91. package/src/javascript/format.ts +72 -33
  92. package/src/javascript/index.ts +2 -1
  93. package/src/javascript/parser.ts +67 -45
  94. package/src/javascript/templating/capture.ts +595 -0
  95. package/src/javascript/templating/comparator.ts +1383 -0
  96. package/src/javascript/templating/engine.ts +750 -0
  97. package/src/javascript/templating/index.ts +67 -0
  98. package/src/javascript/templating/pattern.ts +1101 -0
  99. package/src/javascript/templating/placeholder-replacement.ts +475 -0
  100. package/src/javascript/templating/rewrite.ts +229 -0
  101. package/src/javascript/templating/template.ts +414 -0
  102. package/src/javascript/templating/types.ts +674 -0
  103. package/src/javascript/templating/utils.ts +298 -0
  104. package/src/javascript/type-mapping.ts +20 -11
  105. package/src/json/rpc.ts +2 -2
  106. package/src/recipe/order-imports.ts +1 -1
  107. package/src/test/rewrite-test.ts +12 -7
  108. package/src/visitor.ts +14 -6
  109. package/dist/javascript/templating.d.ts +0 -265
  110. package/dist/javascript/templating.d.ts.map +0 -1
  111. package/dist/javascript/templating.js +0 -1027
  112. package/dist/javascript/templating.js.map +0 -1
  113. package/src/javascript/templating.ts +0 -1226
@@ -0,0 +1,1101 @@
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 {Cursor} from '../..';
17
+ import {J} from '../../java';
18
+ import {Any, Capture, DebugLogEntry, DebugOptions, MatchAttemptResult, MatchExplanation, MatchOptions, PatternOptions, MatchResult as IMatchResult} from './types';
19
+ import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CaptureImpl, RAW_CODE_SYMBOL, RawCode} from './capture';
20
+ import {DebugPatternMatchingComparator, MatcherCallbacks, MatcherState, PatternMatchingComparator} from './comparator';
21
+ import {CaptureMarker, CaptureStorageValue, generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
22
+ import {TemplateEngine} from './engine';
23
+ import {TreePrinters} from '../../print';
24
+ import {JS} from '../index';
25
+
26
+
27
+ /**
28
+ * Builder for creating patterns programmatically.
29
+ * Use when pattern structure is not known at compile time.
30
+ *
31
+ * @example
32
+ * // Loop-based pattern generation
33
+ * const builder = Pattern.builder().code('myFunction(');
34
+ * for (let i = 0; i < argCount; i++) {
35
+ * if (i > 0) builder.code(', ');
36
+ * builder.capture(capture(`arg${i}`));
37
+ * }
38
+ * builder.code(')');
39
+ * const pat = builder.build();
40
+ *
41
+ * @example
42
+ * // Conditional pattern construction
43
+ * const builder = Pattern.builder().code('foo(');
44
+ * builder.capture(capture('first'));
45
+ * if (needsSecondArg) {
46
+ * builder.code(', ').capture(capture('second'));
47
+ * }
48
+ * builder.code(')');
49
+ * const pat = builder.build();
50
+ */
51
+ export class PatternBuilder {
52
+ private parts: string[] = [];
53
+ private captures: (Capture | Any<any> | RawCode)[] = [];
54
+
55
+ /**
56
+ * Adds a static string part to the pattern.
57
+ *
58
+ * @param str The string to add
59
+ * @returns This builder for chaining
60
+ */
61
+ code(str: string): this {
62
+ // If there are already captures, we need to add an empty string before this
63
+ if (this.captures.length > this.parts.length) {
64
+ this.parts.push('');
65
+ }
66
+ // Append to the last part or start a new one
67
+ if (this.parts.length === 0) {
68
+ this.parts.push(str);
69
+ } else {
70
+ this.parts[this.parts.length - 1] += str;
71
+ }
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Adds a capture to the pattern.
77
+ *
78
+ * @param value The capture object (Capture, Any, or RawCode) or string name
79
+ * @returns This builder for chaining
80
+ */
81
+ capture(value: Capture | Any<any> | RawCode | string): this {
82
+ // Ensure we have a part for after this capture
83
+ if (this.parts.length === 0) {
84
+ this.parts.push('');
85
+ }
86
+ // Convert string to Capture if needed, or use value as-is for RawCode
87
+ const captureObj = typeof value === 'string' ? new CaptureImpl(value) : value;
88
+ this.captures.push(captureObj as any);
89
+ // Add an empty string for the next part
90
+ this.parts.push('');
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Builds the pattern from accumulated parts and captures.
96
+ *
97
+ * @returns A Pattern instance
98
+ */
99
+ build(): Pattern {
100
+ // Ensure parts array is one longer than captures array
101
+ while (this.parts.length <= this.captures.length) {
102
+ this.parts.push('');
103
+ }
104
+
105
+ // Create a synthetic TemplateStringsArray
106
+ const templateStrings = this.parts.slice() as any;
107
+ templateStrings.raw = this.parts.slice();
108
+ Object.defineProperty(templateStrings, 'raw', {
109
+ value: this.parts.slice(),
110
+ writable: false
111
+ });
112
+
113
+ // Delegate to the pattern() function
114
+ return pattern(templateStrings, ...this.captures);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Represents a pattern that can be matched against AST nodes.
120
+ */
121
+ export class Pattern {
122
+ private _options: PatternOptions = {};
123
+ private _cachedAstPattern?: J;
124
+ private static nextPatternId = 1;
125
+ private readonly patternId: number;
126
+ private readonly unnamedCaptureMapping = new Map<string, string>();
127
+
128
+ /**
129
+ * Gets the configuration options for this pattern.
130
+ * @readonly
131
+ */
132
+ get options(): Readonly<PatternOptions> {
133
+ return this._options;
134
+ }
135
+
136
+ /**
137
+ * Creates a new builder for constructing patterns programmatically.
138
+ *
139
+ * @returns A new PatternBuilder instance
140
+ *
141
+ * @example
142
+ * const pat = Pattern.builder()
143
+ * .code('function ')
144
+ * .capture(capture('name'))
145
+ * .code('() { return ')
146
+ * .capture(capture('value'))
147
+ * .code('; }')
148
+ * .build();
149
+ */
150
+ static builder(): PatternBuilder {
151
+ return new PatternBuilder();
152
+ }
153
+
154
+ /**
155
+ * Creates a new pattern from template parts and captures.
156
+ *
157
+ * @param templateParts The string parts of the template
158
+ * @param captures The captures between the string parts (can be Capture, Any, or RawCode)
159
+ */
160
+ constructor(
161
+ public readonly templateParts: TemplateStringsArray,
162
+ public readonly captures: (Capture | Any<any> | RawCode)[]
163
+ ) {
164
+ this.patternId = Pattern.nextPatternId++;
165
+
166
+ // Build mapping for unnamed captures (unnamed_N -> _X)
167
+ let unnamedIndex = 1;
168
+ for (const cap of captures) {
169
+ if (cap && typeof cap === 'object' && 'getName' in cap) {
170
+ const name = (cap as Capture<any> | Any<any>).getName();
171
+ if (name && name.startsWith('unnamed_')) {
172
+ this.unnamedCaptureMapping.set(name, `_${unnamedIndex}`);
173
+ unnamedIndex++;
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Configures this pattern with additional options.
181
+ *
182
+ * @param options Configuration options
183
+ * @returns This pattern for method chaining
184
+ *
185
+ * @example
186
+ * pattern`forwardRef((${props}, ${ref}) => ${body})`
187
+ * .configure({
188
+ * context: ['import { forwardRef } from "react"'],
189
+ * dependencies: {'@types/react': '^18.0.0'}
190
+ * })
191
+ */
192
+ configure(options: PatternOptions): Pattern {
193
+ this._options = {...this._options, ...options};
194
+ // Invalidate cache when configuration changes
195
+ this._cachedAstPattern = undefined;
196
+ return this;
197
+ }
198
+
199
+ /**
200
+ * Gets the AST pattern for this pattern, using two-level caching:
201
+ * 1. Instance-level cache (fastest - this pattern instance)
202
+ * 2. Global LRU cache (fast - shared across pattern instances with same code)
203
+ * 3. Compute via TemplateProcessor (slow - parse and process)
204
+ *
205
+ * @returns The cached or newly computed pattern AST
206
+ * @internal
207
+ */
208
+ async getAstPattern(): Promise<J> {
209
+ // Level 1: Instance cache (fastest path)
210
+ if (this._cachedAstPattern) {
211
+ return this._cachedAstPattern;
212
+ }
213
+
214
+ // Generate cache key for global lookup
215
+ // Include raw code values in the key since they affect the generated AST
216
+ const contextStatements = this._options.context || this._options.imports || [];
217
+ const capturesKey = this.captures.map(c => {
218
+ if (c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) {
219
+ return `raw:${(c as RawCode).code}`;
220
+ }
221
+ return c.getName();
222
+ }).join(',');
223
+ const cacheKey = generateCacheKey(
224
+ this.templateParts,
225
+ capturesKey,
226
+ contextStatements,
227
+ this._options.dependencies || {}
228
+ );
229
+
230
+ // Level 2: Global cache (fast path - shared with Template)
231
+ const cached = globalAstCache.get(cacheKey);
232
+ if (cached) {
233
+ this._cachedAstPattern = cached;
234
+ return cached;
235
+ }
236
+
237
+ // Level 3: Compute via TemplateEngine (slow path)
238
+ const result = await TemplateEngine.getPatternTree(
239
+ this.templateParts,
240
+ this.captures,
241
+ contextStatements,
242
+ this._options.dependencies || {}
243
+ );
244
+
245
+ // Cache in both levels
246
+ globalAstCache.set(cacheKey, result);
247
+ this._cachedAstPattern = result;
248
+
249
+ return result;
250
+ }
251
+
252
+ /**
253
+ * Creates a matcher for this pattern against a specific AST node.
254
+ *
255
+ * @param ast The AST node to match against
256
+ * @param cursor Optional cursor at the node's position in a larger tree. Used for context-aware
257
+ * capture constraints to navigate to parent nodes. If omitted, a cursor will be
258
+ * created at the ast root, allowing constraints to navigate within the matched subtree.
259
+ * @param options Optional match options (e.g., debug flag)
260
+ * @returns A MatchResult if the pattern matches, undefined otherwise
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * // Normal match
265
+ * const match = await pattern.match(node);
266
+ *
267
+ * // Debug this specific call
268
+ * const match = await pattern.match(node, cursor, { debug: true });
269
+ * ```
270
+ */
271
+ async match(ast: J, cursor?: Cursor, options?: MatchOptions): Promise<MatchResult | undefined> {
272
+ // Three-level precedence: call > pattern > global
273
+ const debugEnabled =
274
+ options?.debug !== undefined
275
+ ? options.debug // 1. Explicit call-level (true OR false)
276
+ : (this._options.debug !== undefined
277
+ ? this._options.debug // 2. Explicit pattern-level
278
+ : process.env.PATTERN_DEBUG === 'true'); // 3. Global
279
+
280
+ if (debugEnabled) {
281
+ // Use matchWithExplanation and log the result
282
+ const result = await this.matchWithExplanation(ast, cursor);
283
+ await this.logMatchResult(ast, cursor, result);
284
+
285
+ if (result.matched) {
286
+ // result.result is the MatchResult class instance
287
+ return result.result as MatchResult | undefined;
288
+ } else {
289
+ return undefined;
290
+ }
291
+ }
292
+
293
+ // Fast path - no debug
294
+ const matcher = new Matcher(this, ast, cursor);
295
+ const success = await matcher.matches();
296
+ if (!success) {
297
+ return undefined;
298
+ }
299
+ // Create MatchResult with unified storage
300
+ const storage = (matcher as any).storage;
301
+ return new MatchResult(new Map(storage));
302
+ }
303
+
304
+ /**
305
+ * Formats and logs the match result to stderr.
306
+ * @private
307
+ */
308
+ private async logMatchResult(ast: J, cursor: Cursor | undefined, result: MatchAttemptResult): Promise<void> {
309
+ const patternSource = this.getPatternSource();
310
+ const patternId = `Pattern #${this.patternId}`;
311
+ const nodeKind = (ast as any).kind || 'unknown';
312
+ // Format kind: extract short name (e.g., "org.openrewrite.java.tree.J$Binary" -> "J$Binary")
313
+ const shortKind = typeof nodeKind === 'string'
314
+ ? nodeKind.split('.').pop() || nodeKind
315
+ : nodeKind;
316
+
317
+ // First, log the pattern source
318
+ console.error(`[${patternId}] ${patternSource}`);
319
+
320
+ // Build the complete match result message
321
+ const lines: string[] = [];
322
+
323
+ // Print the target tree being matched
324
+ let treeStr: string;
325
+ try {
326
+ const printer = TreePrinters.printer(JS.Kind.CompilationUnit);
327
+ treeStr = await printer.print(ast);
328
+ } catch (e) {
329
+ treeStr = '(tree printing unavailable)';
330
+ }
331
+
332
+ if (result.matched) {
333
+ // Success case - result first, then tree, then captures
334
+ lines.push(`[${patternId}] ✅ SUCCESS matching against ${shortKind}:`);
335
+ treeStr.split('\n').forEach(line => lines.push(`[${patternId}] ${line}`));
336
+
337
+ // Log captured values
338
+ if (result.result) {
339
+ const storage = (result.result as any).storage as Map<string, CaptureStorageValue>;
340
+ if (storage && storage.size > 0) {
341
+ for (const [name, value] of storage) {
342
+ const extractedValue = (result.result as any).extractElements(value);
343
+ const valueStr = this.formatCapturedValue(extractedValue);
344
+ const displayName = this.unnamedCaptureMapping.get(name) || name;
345
+ lines.push(`[${patternId}] Captured '${displayName}': ${valueStr}`);
346
+ }
347
+ }
348
+ }
349
+ } else {
350
+ // Failure case - result first, then tree, then explanation
351
+ lines.push(`[${patternId}] ❌ FAILED matching against ${shortKind}:`);
352
+ treeStr.split('\n').forEach(line => lines.push(`[${patternId}] ${line}`));
353
+
354
+ const explanation = result.explanation;
355
+ if (explanation) {
356
+ // Always show path, even if empty, to make it clear where the mismatch occurred
357
+ const compactedPath = this.compactPath(explanation.path);
358
+ const pathStr = compactedPath.length > 0 ? compactedPath.join(' → ') : '';
359
+ lines.push(`[${patternId}] At path: [${pathStr}]`);
360
+ lines.push(`[${patternId}] Reason: ${explanation.reason}`);
361
+ lines.push(`[${patternId}] Expected: ${explanation.expected}`);
362
+ lines.push(`[${patternId}] Actual: ${explanation.actual}`);
363
+ }
364
+ }
365
+
366
+ // Single console.error call with all lines joined
367
+ console.error(lines.join('\n'));
368
+ }
369
+
370
+ /**
371
+ * Compacts array index navigations into the previous path element.
372
+ * For example: ['J$VariableDeclarations#variables', '0'] → ['J$VariableDeclarations#variables[0]']
373
+ * @private
374
+ */
375
+ private compactPath(path: string[]): string[] {
376
+ const compacted: string[] = [];
377
+ let i = 0;
378
+
379
+ while (i < path.length) {
380
+ const current = path[i];
381
+
382
+ // Check if current element is itself a numeric index
383
+ if (/^\d+$/.test(current)) {
384
+ // This is a bare numeric index - shouldn't normally happen
385
+ // If we have a previous element, append to it
386
+ if (compacted.length > 0) {
387
+ compacted[compacted.length - 1] += `[${current}]`;
388
+ } else {
389
+ // No previous element to attach to - this is an error in path construction
390
+ // Skip it to avoid bare [0] in output
391
+ console.warn(`Warning: Path starts with numeric index '${current}' - skipping`);
392
+ }
393
+ i++;
394
+ continue;
395
+ }
396
+
397
+ // Look ahead to collect consecutive numeric indices
398
+ let j = i + 1;
399
+ const indices: string[] = [];
400
+ while (j < path.length && /^\d+$/.test(path[j])) {
401
+ indices.push(path[j]);
402
+ j++;
403
+ }
404
+
405
+ // If we found numeric indices, append them to current element
406
+ if (indices.length > 0) {
407
+ compacted.push(current + indices.map(idx => `[${idx}]`).join(''));
408
+ i = j; // Skip the indices we just processed
409
+ } else {
410
+ compacted.push(current);
411
+ i++;
412
+ }
413
+ }
414
+
415
+ return compacted;
416
+ }
417
+
418
+ /**
419
+ * Gets the source code representation of this pattern for logging.
420
+ * @private
421
+ */
422
+ private getPatternSource(): string {
423
+ // Reconstruct pattern source from template parts
424
+ let source = '';
425
+ for (let i = 0; i < this.templateParts.length; i++) {
426
+ source += this.templateParts[i];
427
+ if (i < this.captures.length) {
428
+ const cap = this.captures[i];
429
+ // Skip raw code
430
+ if (cap instanceof RawCode || (cap && typeof cap === 'object' && (cap as any)[RAW_CODE_SYMBOL])) {
431
+ source += '${raw(...)}';
432
+ continue;
433
+ }
434
+ // Show capture name or placeholder
435
+ const name = (cap as any)[CAPTURE_NAME_SYMBOL];
436
+ if (cap && typeof cap === 'object' && name) {
437
+ // Use mapped name for unnamed captures, or original name
438
+ const displayName = this.unnamedCaptureMapping.get(name) || name;
439
+ source += `\${${displayName}}`;
440
+ } else {
441
+ source += '${...}';
442
+ }
443
+ }
444
+ }
445
+
446
+ return source;
447
+ }
448
+
449
+ /**
450
+ * Formats a captured value for logging.
451
+ * @private
452
+ */
453
+ private formatCapturedValue(value: any): string {
454
+ if (value === null) return 'null';
455
+ if (value === undefined) return 'undefined';
456
+
457
+ // Check if it's an array (variadic capture)
458
+ if (Array.isArray(value)) {
459
+ if (value.length === 0) return '[]';
460
+ const items = value.slice(0, 3).map(v => this.formatSingleValue(v));
461
+ const suffix = value.length > 3 ? `, ... (${value.length} total)` : '';
462
+ return `[${items.join(', ')}${suffix}]`;
463
+ }
464
+
465
+ return this.formatSingleValue(value);
466
+ }
467
+
468
+ /**
469
+ * Formats a single AST node for logging.
470
+ * @private
471
+ */
472
+ private formatSingleValue(value: any): string {
473
+ if (!value || typeof value !== 'object') {
474
+ return String(value);
475
+ }
476
+
477
+ const kind = (value as any).kind;
478
+ if (!kind) return String(value);
479
+
480
+ // Extract simple kind name (last segment)
481
+ const kindStr = kind.split('.').pop();
482
+
483
+ // For literals, show the value
484
+ if (kindStr === 'Literal' && value.value !== undefined) {
485
+ const litValue = typeof value.value === 'string'
486
+ ? `"${value.value}"`
487
+ : String(value.value);
488
+ return `${kindStr}(${litValue})`;
489
+ }
490
+
491
+ // For identifiers, show the name
492
+ if (kindStr === 'Identifier' && value.simpleName) {
493
+ return `${kindStr}(${value.simpleName})`;
494
+ }
495
+
496
+ // Default: just the kind
497
+ return kindStr;
498
+ }
499
+
500
+ /**
501
+ * Matches a pattern against an AST node with detailed debug information.
502
+ * Part of Layer 2 (Public API).
503
+ *
504
+ * This method always enables debug logging and returns detailed information about
505
+ * the match attempt, including:
506
+ * - Whether the pattern matched
507
+ * - Captured nodes (if matched)
508
+ * - Explanation of failure (if not matched)
509
+ * - Debug log entries showing the matching process
510
+ *
511
+ * @param ast The AST node to match against
512
+ * @param cursor Optional cursor at the node's position in a larger tree
513
+ * @param debugOptions Optional debug options (defaults to all logging enabled)
514
+ * @returns Detailed result with debug information
515
+ *
516
+ * @example
517
+ * const x = capture('x');
518
+ * const pat = pattern`console.log(${x})`;
519
+ * const attempt = await pat.matchWithExplanation(node);
520
+ * if (attempt.matched) {
521
+ * console.log('Matched!');
522
+ * console.log('Captured x:', attempt.result.get('x'));
523
+ * } else {
524
+ * console.log('Failed:', attempt.explanation);
525
+ * console.log('Debug log:', attempt.debugLog);
526
+ * }
527
+ */
528
+ async matchWithExplanation(
529
+ ast: J,
530
+ cursor?: Cursor,
531
+ debugOptions?: DebugOptions
532
+ ): Promise<MatchAttemptResult> {
533
+ // Default to full debug logging if not specified
534
+ const options: DebugOptions = {
535
+ enabled: true,
536
+ logComparison: true,
537
+ logConstraints: true,
538
+ ...debugOptions
539
+ };
540
+
541
+ const matcher = new Matcher(this, ast, cursor, options);
542
+ const success = await matcher.matches();
543
+
544
+ if (success) {
545
+ // Match succeeded - return MatchResult with debug info
546
+ const storage = (matcher as any).storage;
547
+ const matchResult = new MatchResult(new Map(storage));
548
+ return {
549
+ matched: true,
550
+ result: matchResult,
551
+ debugLog: matcher.getDebugLog()
552
+ };
553
+ } else {
554
+ // Match failed - return explanation
555
+ return {
556
+ matched: false,
557
+ explanation: matcher.getExplanation(),
558
+ debugLog: matcher.getDebugLog()
559
+ };
560
+ }
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Result of a successful pattern match containing captured values.
566
+ *
567
+ * Provides access to captured AST nodes from pattern matching operations.
568
+ * Use the `get()` method to retrieve captured values by name or by Capture object.
569
+ *
570
+ * @example
571
+ * const x = capture('x');
572
+ * const pat = pattern`foo(${x})`;
573
+ * const match = await pat.match(someNode);
574
+ * if (match) {
575
+ * const captured = match.get('x'); // Get by name
576
+ * // or
577
+ * const captured = match.get(x); // Get by Capture object
578
+ * }
579
+ *
580
+ * @example
581
+ * // Variadic captures return arrays
582
+ * const args = capture({ variadic: true });
583
+ * const pat = pattern`foo(${args})`;
584
+ * const match = await pat.match(methodInvocation);
585
+ * if (match) {
586
+ * const capturedArgs = match.get(args); // Returns J[] for variadic captures
587
+ * }
588
+ */
589
+ export class MatchResult implements IMatchResult {
590
+ constructor(
591
+ private readonly storage: Map<string, CaptureStorageValue> = new Map()
592
+ ) {
593
+ }
594
+
595
+ // Overload: get with Capture returns value
596
+ get<T>(capture: Capture<T>): T | undefined;
597
+ // Overload: get with string returns value
598
+ get(capture: string): any;
599
+ // Implementation
600
+ get(capture: Capture<any> | string): J | J[] | undefined {
601
+ // Use symbol to get internal name without triggering Proxy
602
+ const name = typeof capture === "string" ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName());
603
+ const value = this.storage.get(name);
604
+ if (value === undefined) {
605
+ return undefined;
606
+ }
607
+ return this.extractElements(value);
608
+ }
609
+
610
+ /**
611
+ * Extracts semantic elements from storage value.
612
+ * For wrappers, extracts the .element; for arrays, returns array of elements.
613
+ *
614
+ * @param value The storage value
615
+ * @returns The semantic element(s)
616
+ */
617
+ private extractElements(value: CaptureStorageValue): J {
618
+ if (Array.isArray(value)) {
619
+ // Check if it's an array of wrappers
620
+ if (value.length > 0 && (value[0] as any).element !== undefined) {
621
+ // Array of J.RightPadded - extract elements
622
+ return (value as J.RightPadded<J>[]).map(w => w.element) as any;
623
+ }
624
+ // Already an array of elements
625
+ return value as any;
626
+ }
627
+ // Check if it's a scalar wrapper
628
+ if ((value as any).element !== undefined) {
629
+ return (value as J.RightPadded<J>).element;
630
+ }
631
+ // Scalar element
632
+ return value as J;
633
+ }
634
+
635
+ /**
636
+ * Internal method to get wrappers (used by template expansion).
637
+ * Returns both scalar and variadic wrappers.
638
+ * @internal
639
+ */
640
+ [WRAPPERS_MAP_SYMBOL](): Map<string, J.RightPadded<J> | J.RightPadded<J>[]> {
641
+ const result = new Map<string, J.RightPadded<J> | J.RightPadded<J>[]>();
642
+ for (const [name, value] of this.storage) {
643
+ if (Array.isArray(value) && value.length > 0 && (value[0] as any).element !== undefined) {
644
+ // This is an array of wrappers (variadic)
645
+ result.set(name, value as J.RightPadded<J>[]);
646
+ } else if (!Array.isArray(value) && (value as any).element !== undefined) {
647
+ // This is a scalar wrapper
648
+ result.set(name, value as J.RightPadded<J>);
649
+ }
650
+ }
651
+ return result;
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Matcher for checking if a pattern matches an AST node and extracting captured nodes.
657
+ */
658
+ class Matcher {
659
+ // Unified storage: holds J for scalar captures, J.RightPadded<J>[] or J[] for variadic captures
660
+ private readonly storage = new Map<string, CaptureStorageValue>();
661
+ private patternAst?: J;
662
+
663
+ // Debug tracking (Layer 1: Core Instrumentation)
664
+ private readonly debugOptions: DebugOptions;
665
+ private readonly debugLog: DebugLogEntry[] = [];
666
+ private explanation?: MatchExplanation;
667
+ private readonly currentPath: string[] = [];
668
+
669
+ /**
670
+ * Creates a new matcher for a pattern against an AST node.
671
+ *
672
+ * @param pattern The pattern to match
673
+ * @param ast The AST node to match against
674
+ * @param cursor Optional cursor at the AST node's position
675
+ * @param debugOptions Optional debug options for instrumentation
676
+ */
677
+ constructor(
678
+ private readonly pattern: Pattern,
679
+ private readonly ast: J,
680
+ cursor?: Cursor,
681
+ debugOptions?: DebugOptions
682
+ ) {
683
+ // If no cursor provided, create one at the ast root so constraints can navigate up
684
+ this.cursor = cursor ?? new Cursor(ast, undefined);
685
+ this.debugOptions = debugOptions ?? {};
686
+ }
687
+
688
+ private readonly cursor: Cursor;
689
+
690
+ /**
691
+ * Checks if the pattern matches the AST node.
692
+ *
693
+ * @returns true if the pattern matches, false otherwise
694
+ */
695
+ async matches(): Promise<boolean> {
696
+ if (!this.patternAst) {
697
+ this.patternAst = await this.pattern.getAstPattern();
698
+ }
699
+
700
+ return this.matchNode(this.patternAst, this.ast);
701
+ }
702
+
703
+ /**
704
+ * Gets all captured nodes (projected view: extracts elements from wrappers).
705
+ *
706
+ * @returns A map of capture names to captured nodes
707
+ */
708
+ getAll(): Map<string, J> {
709
+ const result = new Map<string, J>();
710
+ for (const [name, value] of this.storage) {
711
+ result.set(name, this.extractElements(value));
712
+ }
713
+ return result;
714
+ }
715
+
716
+ /**
717
+ * Extracts semantic elements from storage value.
718
+ * For wrappers, extracts the .element; for arrays, returns array of elements.
719
+ *
720
+ * @param value The storage value
721
+ * @returns The semantic element(s)
722
+ */
723
+ private extractElements(value: CaptureStorageValue): J {
724
+ if (Array.isArray(value)) {
725
+ // Check if it's an array of wrappers
726
+ if (value.length > 0 && (value[0] as any).element !== undefined) {
727
+ // Array of J.RightPadded - extract elements
728
+ return (value as J.RightPadded<J>[]).map(w => w.element) as any;
729
+ }
730
+ // Already an array of elements
731
+ return value as any;
732
+ }
733
+ // Check if it's a scalar wrapper
734
+ if ((value as any).element !== undefined) {
735
+ return (value as J.RightPadded<J>).element;
736
+ }
737
+ // Scalar element
738
+ return value as J;
739
+ }
740
+
741
+ /**
742
+ * Logs a debug message if debugging is enabled.
743
+ * Part of Layer 1 (Core Instrumentation).
744
+ *
745
+ * @param level The severity level
746
+ * @param scope The scope/category
747
+ * @param message The message to log
748
+ * @param data Optional data to include
749
+ */
750
+ private log(
751
+ level: DebugLogEntry['level'],
752
+ scope: DebugLogEntry['scope'],
753
+ message: string,
754
+ data?: any
755
+ ): void {
756
+ if (!this.debugOptions.enabled) return;
757
+
758
+ // Filter by scope if specific logging is requested
759
+ if (scope === 'comparison' && !this.debugOptions.logComparison) return;
760
+ if (scope === 'constraint' && !this.debugOptions.logConstraints) return;
761
+
762
+ this.debugLog.push({
763
+ level,
764
+ scope,
765
+ path: [...this.currentPath],
766
+ message,
767
+ data
768
+ });
769
+ }
770
+
771
+ /**
772
+ * Sets the explanation for why the pattern match failed.
773
+ * Only sets the first failure (most relevant).
774
+ * Part of Layer 1 (Core Instrumentation).
775
+ *
776
+ * @param reason The reason for failure
777
+ * @param expected Human-readable description of what was expected
778
+ * @param actual Human-readable description of what was found
779
+ * @param details Optional additional context
780
+ */
781
+ private setExplanation(
782
+ reason: MatchExplanation['reason'],
783
+ expected: string,
784
+ actual: string,
785
+ details?: string
786
+ ): void {
787
+ // Only set the first failure (most relevant)
788
+ if (this.explanation) return;
789
+
790
+ this.explanation = {
791
+ reason,
792
+ path: [...this.currentPath],
793
+ expected,
794
+ actual,
795
+ details
796
+ };
797
+ }
798
+
799
+ /**
800
+ * Pushes a path component onto the current path.
801
+ * Used to track where in the AST tree we are during matching.
802
+ * Part of Layer 1 (Core Instrumentation).
803
+ *
804
+ * @param name The path component to push
805
+ */
806
+ private pushPath(name: string): void {
807
+ this.currentPath.push(name);
808
+ }
809
+
810
+ /**
811
+ * Pops the last path component from the current path.
812
+ * Part of Layer 1 (Core Instrumentation).
813
+ */
814
+ private popPath(): void {
815
+ this.currentPath.pop();
816
+ }
817
+
818
+ /**
819
+ * Matches a pattern node against a target node.
820
+ *
821
+ * @param pattern The pattern node
822
+ * @param target The target node
823
+ * @returns true if the pattern matches the target, false otherwise
824
+ */
825
+ private async matchNode(pattern: J, target: J): Promise<boolean> {
826
+ // Always delegate to the comparator visitor, which handles:
827
+ // - Capture detection and constraint evaluation
828
+ // - Kind checking
829
+ // - Deep structural comparison
830
+ // This centralizes all matching logic in one place
831
+ const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
832
+
833
+ // Factory pattern: instantiate debug or production comparator
834
+ // Zero cost in production - DebugPatternMatchingComparator is never instantiated
835
+ const matcherCallbacks: MatcherCallbacks = {
836
+ handleCapture: (capture: CaptureMarker, t: J, w?: J.RightPadded<J>) => this.handleCapture(capture, t, w),
837
+ handleVariadicCapture: (capture: CaptureMarker, ts: J[], ws?: J.RightPadded<J>[]) => this.handleVariadicCapture(capture, ts, ws),
838
+ saveState: () => this.saveState(),
839
+ restoreState: (state) => this.restoreState(state),
840
+ // Debug callbacks (Layer 1) - grouped together, always present or absent
841
+ debug: this.debugOptions.enabled ? {
842
+ log: (level: DebugLogEntry['level'], scope: DebugLogEntry['scope'], message: string, data?: any) => this.log(level, scope, message, data),
843
+ setExplanation: (reason: MatchExplanation['reason'], expected: string, actual: string, details?: string) => this.setExplanation(reason, expected, actual, details),
844
+ getExplanation: () => this.explanation,
845
+ restoreExplanation: (explanation: MatchExplanation) => { this.explanation = explanation; },
846
+ clearExplanation: () => { this.explanation = undefined; },
847
+ pushPath: (name: string) => this.pushPath(name),
848
+ popPath: () => this.popPath()
849
+ } : undefined
850
+ };
851
+
852
+ const comparator = this.debugOptions.enabled
853
+ ? new DebugPatternMatchingComparator(matcherCallbacks, lenientTypeMatching)
854
+ : new PatternMatchingComparator(matcherCallbacks, lenientTypeMatching);
855
+ // Pass cursors to allow constraints to navigate to root
856
+ // Pattern cursor is undefined (pattern is the root), target cursor is provided by user
857
+ const result = await comparator.compare(pattern, target, undefined, this.cursor);
858
+
859
+ // If match failed and no explanation was set, provide a generic one
860
+ if (!result && this.debugOptions.enabled && !this.explanation) {
861
+ const patternKind = (pattern as any).kind?.split('.').pop() || 'unknown';
862
+ const targetKind = (target as any).kind?.split('.').pop() || 'unknown';
863
+ this.setExplanation(
864
+ 'structural-mismatch',
865
+ `Pattern node of type ${patternKind}`,
866
+ `Target node of type ${targetKind}`,
867
+ 'Nodes did not match structurally'
868
+ );
869
+ }
870
+
871
+ return result;
872
+ }
873
+
874
+ /**
875
+ * Saves the current state for backtracking.
876
+ * Includes both capture storage AND debug state (explanation, log, path).
877
+ *
878
+ * @returns A snapshot of the current state
879
+ */
880
+ private saveState(): MatcherState {
881
+ return {
882
+ storage: new Map(this.storage),
883
+ debugState: this.debugOptions.enabled ? {
884
+ explanation: this.explanation,
885
+ logLength: this.debugLog.length,
886
+ path: [...this.currentPath]
887
+ } : undefined
888
+ };
889
+ }
890
+
891
+ /**
892
+ * Restores a previously saved state for backtracking.
893
+ * Restores both capture storage AND debug state.
894
+ *
895
+ * @param state The state to restore
896
+ */
897
+ private restoreState(state: MatcherState): void {
898
+ // Restore capture storage
899
+ this.storage.clear();
900
+ state.storage.forEach((value, key) => this.storage.set(key, value));
901
+
902
+ // Restore debug state if it was saved
903
+ if (state.debugState) {
904
+ // Restore explanation to the saved state
905
+ // This clears any explanations set during failed exploratory attempts (like pivot detection)
906
+ this.explanation = state.debugState.explanation;
907
+ // Truncate debug log to saved length (remove entries added during failed attempt)
908
+ this.debugLog.length = state.debugState.logLength;
909
+ // Restore path
910
+ this.currentPath.length = 0;
911
+ this.currentPath.push(...state.debugState.path);
912
+ }
913
+ }
914
+
915
+ /**
916
+ * Handles a capture placeholder.
917
+ *
918
+ * @param capture The pattern node capture
919
+ * @param target The target node
920
+ * @param wrapper Optional wrapper containing the target (for preserving markers)
921
+ * @returns true if the capture is successful, false otherwise
922
+ */
923
+ private handleCapture(capture: CaptureMarker, target: J, wrapper?: J.RightPadded<J>): boolean {
924
+ const captureName = capture.captureName;
925
+
926
+ if (!captureName) {
927
+ return false;
928
+ }
929
+
930
+ // Find the original capture object to get capturing flag
931
+ // Note: Constraints are now evaluated in PatternMatchingComparator where cursor is correctly positioned
932
+ // Filter out RawCode since it doesn't have getName()
933
+ const captureObj = this.pattern.captures.find(c =>
934
+ !(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) &&
935
+ c.getName() === captureName
936
+ );
937
+
938
+ // Only store the binding if this is a capturing placeholder
939
+ const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
940
+ if (capturing) {
941
+ // Store wrapper if available (preserves markers), otherwise store element
942
+ this.storage.set(captureName, wrapper ?? target);
943
+ }
944
+
945
+ return true;
946
+ }
947
+
948
+ /**
949
+ * Handles a variadic capture placeholder.
950
+ *
951
+ * @param capture The pattern node capture (the variadic capture)
952
+ * @param targets The target nodes that were matched
953
+ * @param wrappers Optional wrappers to preserve markers
954
+ * @returns true if the capture is successful, false otherwise
955
+ */
956
+ private handleVariadicCapture(capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded<J>[]): boolean {
957
+ const captureName = capture.captureName;
958
+
959
+ if (!captureName) {
960
+ return false;
961
+ }
962
+
963
+ // Find the original capture object to get capturing flag
964
+ // Note: Constraints are now evaluated in PatternMatchingComparator where cursor is correctly positioned
965
+ // Filter out RawCode since it doesn't have getName()
966
+ const captureObj = this.pattern.captures.find(c =>
967
+ !(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) &&
968
+ c.getName() === captureName
969
+ );
970
+
971
+ // Only store the binding if this is a capturing placeholder
972
+ const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
973
+ if (capturing) {
974
+ // Store the richest representation: wrappers if available, otherwise elements
975
+ if (wrappers && wrappers.length > 0) {
976
+ this.storage.set(captureName, wrappers);
977
+ } else {
978
+ this.storage.set(captureName, targets);
979
+ }
980
+ }
981
+
982
+ return true;
983
+ }
984
+
985
+ /**
986
+ * Gets the debug log entries collected during matching.
987
+ * Part of Layer 2 (Public API).
988
+ *
989
+ * @returns The debug log entries, or undefined if debug wasn't enabled
990
+ */
991
+ getDebugLog(): DebugLogEntry[] | undefined {
992
+ return this.debugOptions.enabled ? [...this.debugLog] : undefined;
993
+ }
994
+
995
+ /**
996
+ * Gets the explanation for why the match failed.
997
+ * Part of Layer 2 (Public API).
998
+ *
999
+ * @returns The match explanation, or undefined if match succeeded or no explanation available
1000
+ */
1001
+ getExplanation(): MatchExplanation | undefined {
1002
+ return this.explanation;
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Tagged template function for creating patterns.
1008
+ *
1009
+ * @param strings The string parts of the template
1010
+ * @param captures The captures between the string parts (Capture, Any, RawCode, or string names)
1011
+ * @returns A Pattern object
1012
+ *
1013
+ * @example
1014
+ * // Using the same capture multiple times for repeated patterns
1015
+ * const expr = capture('expr');
1016
+ * const redundantOr = pattern`${expr} || ${expr}`;
1017
+ *
1018
+ * @example
1019
+ * // Using any() for non-capturing matches
1020
+ * const pat = pattern`foo(${any()})`;
1021
+ *
1022
+ * @example
1023
+ * // Using raw() for dynamic pattern construction
1024
+ * const operator = '===';
1025
+ * const pat = pattern`x ${raw(operator)} y`;
1026
+ */
1027
+ /**
1028
+ * Creates a pattern from a template literal (direct usage).
1029
+ *
1030
+ * @example
1031
+ * ```typescript
1032
+ * const pat = pattern`console.log(${x})`;
1033
+ * ```
1034
+ */
1035
+ export function pattern(strings: TemplateStringsArray, ...captures: (Capture | Any<any> | RawCode | string)[]): Pattern;
1036
+
1037
+ /**
1038
+ * Creates a pattern factory with options that returns a tagged template function.
1039
+ *
1040
+ * @example
1041
+ * ```typescript
1042
+ * const pat = pattern({ debug: true })`console.log(${x})`;
1043
+ * ```
1044
+ */
1045
+ export function pattern(options: PatternOptions): (strings: TemplateStringsArray, ...captures: (Capture | Any<any> | RawCode | string)[]) => Pattern;
1046
+
1047
+ // Implementation
1048
+ export function pattern(
1049
+ stringsOrOptions: TemplateStringsArray | PatternOptions,
1050
+ ...captures: (Capture | Any<any> | RawCode | string)[]
1051
+ ): Pattern | ((strings: TemplateStringsArray, ...captures: (Capture | Any<any> | RawCode | string)[]) => Pattern) {
1052
+ // Check if first arg is TemplateStringsArray (direct usage)
1053
+ if (Array.isArray(stringsOrOptions) && 'raw' in stringsOrOptions) {
1054
+ // Direct usage: pattern`...`
1055
+ return createPattern(stringsOrOptions as TemplateStringsArray, captures, {});
1056
+ }
1057
+
1058
+ // Options usage: pattern({ ... })`...`
1059
+ const options = stringsOrOptions as PatternOptions;
1060
+ return (strings: TemplateStringsArray, ...caps: (Capture | Any<any> | RawCode | string)[]): Pattern => {
1061
+ return createPattern(strings, caps, options);
1062
+ };
1063
+ }
1064
+
1065
+ /**
1066
+ * Internal helper to create a Pattern instance.
1067
+ * @private
1068
+ */
1069
+ function createPattern(
1070
+ strings: TemplateStringsArray,
1071
+ captures: (Capture | Any<any> | RawCode | string)[],
1072
+ options: PatternOptions
1073
+ ): Pattern {
1074
+ const capturesByName = captures.reduce((map, c) => {
1075
+ // Skip raw code - it's not a capture
1076
+ if (c instanceof RawCode || (typeof c === 'object' && c && (c as any)[RAW_CODE_SYMBOL])) {
1077
+ return map;
1078
+ }
1079
+ const capture = typeof c === "string" ? new CaptureImpl(c) : c;
1080
+ // Use symbol to get internal name without triggering Proxy
1081
+ const name = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
1082
+ return map.set(name, capture);
1083
+ }, new Map<string, Capture | Any<any>>());
1084
+
1085
+ const pat = new Pattern(strings, captures.map(c => {
1086
+ // Return raw code as-is
1087
+ if (c instanceof RawCode || (typeof c === 'object' && c && (c as any)[RAW_CODE_SYMBOL])) {
1088
+ return c as RawCode;
1089
+ }
1090
+ // Use symbol to get internal name without triggering Proxy
1091
+ const name = typeof c === "string" ? c : ((c as any)[CAPTURE_NAME_SYMBOL] || c.getName());
1092
+ return capturesByName.get(name)!;
1093
+ }));
1094
+
1095
+ // Apply options if provided
1096
+ if (options && Object.keys(options).length > 0) {
1097
+ pat.configure(options);
1098
+ }
1099
+
1100
+ return pat;
1101
+ }