@openrewrite/rewrite 8.66.1 → 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 (106) 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 +154 -7
  16. package/dist/javascript/comparator.d.ts.map +1 -1
  17. package/dist/javascript/comparator.js +623 -180
  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 +85 -43
  22. package/dist/javascript/format.js.map +1 -1
  23. package/dist/javascript/index.d.ts +1 -0
  24. package/dist/javascript/index.d.ts.map +1 -1
  25. package/dist/javascript/index.js +1 -0
  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 +39 -30
  30. package/dist/javascript/parser.js.map +1 -1
  31. package/dist/javascript/templating/capture.d.ts +81 -14
  32. package/dist/javascript/templating/capture.d.ts.map +1 -1
  33. package/dist/javascript/templating/capture.js +98 -8
  34. package/dist/javascript/templating/capture.js.map +1 -1
  35. package/dist/javascript/templating/comparator.d.ts +125 -15
  36. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  37. package/dist/javascript/templating/comparator.js +946 -118
  38. package/dist/javascript/templating/comparator.js.map +1 -1
  39. package/dist/javascript/templating/engine.d.ts +58 -25
  40. package/dist/javascript/templating/engine.d.ts.map +1 -1
  41. package/dist/javascript/templating/engine.js +527 -94
  42. package/dist/javascript/templating/engine.js.map +1 -1
  43. package/dist/javascript/templating/index.d.ts +3 -3
  44. package/dist/javascript/templating/index.d.ts.map +1 -1
  45. package/dist/javascript/templating/index.js +3 -1
  46. package/dist/javascript/templating/index.js.map +1 -1
  47. package/dist/javascript/templating/pattern.d.ts +121 -16
  48. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  49. package/dist/javascript/templating/pattern.js +528 -257
  50. package/dist/javascript/templating/pattern.js.map +1 -1
  51. package/dist/javascript/templating/placeholder-replacement.d.ts +30 -5
  52. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  53. package/dist/javascript/templating/placeholder-replacement.js +183 -81
  54. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  55. package/dist/javascript/templating/rewrite.d.ts +56 -11
  56. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  57. package/dist/javascript/templating/rewrite.js +143 -16
  58. package/dist/javascript/templating/rewrite.js.map +1 -1
  59. package/dist/javascript/templating/template.d.ts +31 -5
  60. package/dist/javascript/templating/template.d.ts.map +1 -1
  61. package/dist/javascript/templating/template.js +89 -15
  62. package/dist/javascript/templating/template.js.map +1 -1
  63. package/dist/javascript/templating/types.d.ts +359 -12
  64. package/dist/javascript/templating/types.d.ts.map +1 -1
  65. package/dist/javascript/templating/utils.d.ts +52 -35
  66. package/dist/javascript/templating/utils.d.ts.map +1 -1
  67. package/dist/javascript/templating/utils.js +107 -109
  68. package/dist/javascript/templating/utils.js.map +1 -1
  69. package/dist/javascript/type-mapping.d.ts.map +1 -1
  70. package/dist/javascript/type-mapping.js +21 -11
  71. package/dist/javascript/type-mapping.js.map +1 -1
  72. package/dist/json/rpc.js +2 -2
  73. package/dist/json/rpc.js.map +1 -1
  74. package/dist/recipe/order-imports.js.map +1 -1
  75. package/dist/test/rewrite-test.d.ts.map +1 -1
  76. package/dist/test/rewrite-test.js +10 -6
  77. package/dist/test/rewrite-test.js.map +1 -1
  78. package/dist/version.txt +1 -1
  79. package/dist/visitor.d.ts +4 -4
  80. package/dist/visitor.d.ts.map +1 -1
  81. package/dist/visitor.js +8 -3
  82. package/dist/visitor.js.map +1 -1
  83. package/package.json +4 -2
  84. package/src/java/tree.ts +10 -3
  85. package/src/java/type-visitor.ts +1 -1
  86. package/src/java/visitor.ts +11 -5
  87. package/src/javascript/assertions.ts +9 -3
  88. package/src/javascript/comparator.ts +676 -185
  89. package/src/javascript/format.ts +72 -34
  90. package/src/javascript/index.ts +1 -0
  91. package/src/javascript/parser.ts +51 -31
  92. package/src/javascript/templating/capture.ts +107 -15
  93. package/src/javascript/templating/comparator.ts +1087 -134
  94. package/src/javascript/templating/engine.ts +601 -103
  95. package/src/javascript/templating/index.ts +9 -2
  96. package/src/javascript/templating/pattern.ts +655 -281
  97. package/src/javascript/templating/placeholder-replacement.ts +183 -80
  98. package/src/javascript/templating/rewrite.ts +152 -18
  99. package/src/javascript/templating/template.ts +110 -22
  100. package/src/javascript/templating/types.ts +386 -12
  101. package/src/javascript/templating/utils.ts +116 -102
  102. package/src/javascript/type-mapping.ts +20 -11
  103. package/src/json/rpc.ts +2 -2
  104. package/src/recipe/order-imports.ts +1 -1
  105. package/src/test/rewrite-test.ts +12 -7
  106. package/src/visitor.ts +14 -6
@@ -13,14 +13,16 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {produce} from 'immer';
16
+ import {Cursor} from '../..';
17
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';
18
24
  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';
25
+
24
26
 
25
27
  /**
26
28
  * Builder for creating patterns programmatically.
@@ -48,7 +50,7 @@ import {PlaceholderUtils, templateCache, CaptureMarker, CaptureStorageValue, WRA
48
50
  */
49
51
  export class PatternBuilder {
50
52
  private parts: string[] = [];
51
- private captures: (Capture | Any<any>)[] = [];
53
+ private captures: (Capture | Any<any> | RawCode)[] = [];
52
54
 
53
55
  /**
54
56
  * Adds a static string part to the pattern.
@@ -73,15 +75,15 @@ export class PatternBuilder {
73
75
  /**
74
76
  * Adds a capture to the pattern.
75
77
  *
76
- * @param value The capture object (Capture or Any) or string name
78
+ * @param value The capture object (Capture, Any, or RawCode) or string name
77
79
  * @returns This builder for chaining
78
80
  */
79
- capture(value: Capture | Any<any> | string): this {
81
+ capture(value: Capture | Any<any> | RawCode | string): this {
80
82
  // Ensure we have a part for after this capture
81
83
  if (this.parts.length === 0) {
82
84
  this.parts.push('');
83
85
  }
84
- // Convert string to Capture if needed
86
+ // Convert string to Capture if needed, or use value as-is for RawCode
85
87
  const captureObj = typeof value === 'string' ? new CaptureImpl(value) : value;
86
88
  this.captures.push(captureObj as any);
87
89
  // Add an empty string for the next part
@@ -118,6 +120,10 @@ export class PatternBuilder {
118
120
  */
119
121
  export class Pattern {
120
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>();
121
127
 
122
128
  /**
123
129
  * Gets the configuration options for this pattern.
@@ -149,12 +155,25 @@ export class Pattern {
149
155
  * Creates a new pattern from template parts and captures.
150
156
  *
151
157
  * @param templateParts The string parts of the template
152
- * @param captures The captures between the string parts (can be Capture or Any)
158
+ * @param captures The captures between the string parts (can be Capture, Any, or RawCode)
153
159
  */
154
160
  constructor(
155
161
  public readonly templateParts: TemplateStringsArray,
156
- public readonly captures: (Capture | Any<any>)[]
162
+ public readonly captures: (Capture | Any<any> | RawCode)[]
157
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
+ }
158
177
  }
159
178
 
160
179
  /**
@@ -164,25 +183,115 @@ export class Pattern {
164
183
  * @returns This pattern for method chaining
165
184
  *
166
185
  * @example
167
- * pattern`isDate(${capture('date')})`
186
+ * pattern`forwardRef((${props}, ${ref}) => ${body})`
168
187
  * .configure({
169
- * imports: ['import { isDate } from \"util\"'],
170
- * dependencies: { 'util': '^1.0.0' }
188
+ * context: ['import { forwardRef } from "react"'],
189
+ * dependencies: {'@types/react': '^18.0.0'}
171
190
  * })
172
191
  */
173
192
  configure(options: PatternOptions): Pattern {
174
- this._options = { ...this._options, ...options };
193
+ this._options = {...this._options, ...options};
194
+ // Invalidate cache when configuration changes
195
+ this._cachedAstPattern = undefined;
175
196
  return this;
176
197
  }
177
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
+
178
252
  /**
179
253
  * Creates a matcher for this pattern against a specific AST node.
180
254
  *
181
255
  * @param ast The AST node to match against
182
- * @returns A Matcher object
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
+ * ```
183
270
  */
184
- async match(ast: J): Promise<MatchResult | undefined> {
185
- const matcher = new Matcher(this, ast);
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);
186
295
  const success = await matcher.matches();
187
296
  if (!success) {
188
297
  return undefined;
@@ -191,6 +300,265 @@ export class Pattern {
191
300
  const storage = (matcher as any).storage;
192
301
  return new MatchResult(new Map(storage));
193
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
+ }
194
562
  }
195
563
 
196
564
  /**
@@ -218,18 +586,16 @@ export class Pattern {
218
586
  * const capturedArgs = match.get(args); // Returns J[] for variadic captures
219
587
  * }
220
588
  */
221
- export class MatchResult implements Pick<Map<string, J>, "get"> {
589
+ export class MatchResult implements IMatchResult {
222
590
  constructor(
223
591
  private readonly storage: Map<string, CaptureStorageValue> = new Map()
224
592
  ) {
225
593
  }
226
594
 
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
595
+ // Overload: get with Capture returns value
230
596
  get<T>(capture: Capture<T>): T | undefined;
231
- // Overload: get with string returns J
232
- get(capture: string): J | undefined;
597
+ // Overload: get with string returns value
598
+ get(capture: string): any;
233
599
  // Implementation
234
600
  get(capture: Capture<any> | string): J | J[] | undefined {
235
601
  // Use symbol to get internal name without triggering Proxy
@@ -294,18 +660,33 @@ class Matcher {
294
660
  private readonly storage = new Map<string, CaptureStorageValue>();
295
661
  private patternAst?: J;
296
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
+
297
669
  /**
298
670
  * Creates a new matcher for a pattern against an AST node.
299
671
  *
300
672
  * @param pattern The pattern to match
301
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
302
676
  */
303
677
  constructor(
304
678
  private readonly pattern: Pattern,
305
- private readonly ast: J
679
+ private readonly ast: J,
680
+ cursor?: Cursor,
681
+ debugOptions?: DebugOptions
306
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 ?? {};
307
686
  }
308
687
 
688
+ private readonly cursor: Cursor;
689
+
309
690
  /**
310
691
  * Checks if the pattern matches the AST node.
311
692
  *
@@ -313,15 +694,7 @@ class Matcher {
313
694
  */
314
695
  async matches(): Promise<boolean> {
315
696
  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();
697
+ this.patternAst = await this.pattern.getAstPattern();
325
698
  }
326
699
 
327
700
  return this.matchNode(this.patternAst, this.ast);
@@ -365,6 +738,83 @@ class Matcher {
365
738
  return value as J;
366
739
  }
367
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
+
368
818
  /**
369
819
  * Matches a pattern node against a target node.
370
820
  *
@@ -373,70 +823,117 @@ class Matcher {
373
823
  * @returns true if the pattern matches the target, false otherwise
374
824
  */
375
825
  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
- }
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;
380
832
 
381
- // Check if nodes have the same kind
382
- if (pattern.kind !== target.kind) {
383
- return false;
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
+ );
384
869
  }
385
870
 
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);
871
+ return result;
396
872
  }
397
873
 
398
874
  /**
399
- * Saves the current state of storage for backtracking.
875
+ * Saves the current state for backtracking.
876
+ * Includes both capture storage AND debug state (explanation, log, path).
400
877
  *
401
878
  * @returns A snapshot of the current state
402
879
  */
403
- private saveState(): Map<string, CaptureStorageValue> {
404
- return new Map(this.storage);
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
+ };
405
889
  }
406
890
 
407
891
  /**
408
892
  * Restores a previously saved state for backtracking.
893
+ * Restores both capture storage AND debug state.
409
894
  *
410
895
  * @param state The state to restore
411
896
  */
412
- private restoreState(state: Map<string, CaptureStorageValue>): void {
897
+ private restoreState(state: MatcherState): void {
898
+ // Restore capture storage
413
899
  this.storage.clear();
414
- state.forEach((value, key) => this.storage.set(key, value));
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
+ }
415
913
  }
416
914
 
417
915
  /**
418
916
  * Handles a capture placeholder.
419
917
  *
420
- * @param pattern The pattern node
918
+ * @param capture The pattern node capture
421
919
  * @param target The target node
422
920
  * @param wrapper Optional wrapper containing the target (for preserving markers)
423
921
  * @returns true if the capture is successful, false otherwise
424
922
  */
425
- private handleCapture(pattern: J, target: J, wrapper?: J.RightPadded<J>): boolean {
426
- const captureName = PlaceholderUtils.getCaptureName(pattern);
923
+ private handleCapture(capture: CaptureMarker, target: J, wrapper?: J.RightPadded<J>): boolean {
924
+ const captureName = capture.captureName;
427
925
 
428
926
  if (!captureName) {
429
927
  return false;
430
928
  }
431
929
 
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
- }
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
+ );
440
937
 
441
938
  // Only store the binding if this is a capturing placeholder
442
939
  const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
@@ -451,26 +948,25 @@ class Matcher {
451
948
  /**
452
949
  * Handles a variadic capture placeholder.
453
950
  *
454
- * @param pattern The pattern node (the variadic capture)
951
+ * @param capture The pattern node capture (the variadic capture)
455
952
  * @param targets The target nodes that were matched
456
953
  * @param wrappers Optional wrappers to preserve markers
457
954
  * @returns true if the capture is successful, false otherwise
458
955
  */
459
- private handleVariadicCapture(pattern: J, targets: J[], wrappers?: J.RightPadded<J>[]): boolean {
460
- const captureName = PlaceholderUtils.getCaptureName(pattern);
956
+ private handleVariadicCapture(capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded<J>[]): boolean {
957
+ const captureName = capture.captureName;
461
958
 
462
959
  if (!captureName) {
463
960
  return false;
464
961
  }
465
962
 
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
- }
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
+ );
474
970
 
475
971
  // Only store the binding if this is a capturing placeholder
476
972
  const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
@@ -485,214 +981,25 @@ class Matcher {
485
981
 
486
982
  return true;
487
983
  }
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
984
 
564
985
  /**
565
- * Extracts the pattern from the parsed AST.
986
+ * Gets the debug log entries collected during matching.
987
+ * Part of Layer 2 (Public API).
566
988
  *
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
989
+ * @returns The debug log entries, or undefined if debug wasn't enabled
616
990
  */
617
- private attachCaptureMarkers(ast: J): J {
618
- const visited = new Set<J | object>();
619
- return produce(ast, draft => {
620
- this.visitAndAttachMarkers(draft, visited);
621
- });
991
+ getDebugLog(): DebugLogEntry[] | undefined {
992
+ return this.debugOptions.enabled ? [...this.debugLog] : undefined;
622
993
  }
623
994
 
624
995
  /**
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.
996
+ * Gets the explanation for why the match failed.
997
+ * Part of Layer 2 (Public API).
628
998
  *
629
- * @param node The node to visit
630
- * @param visited Set of already visited nodes to avoid cycles
999
+ * @returns The match explanation, or undefined if match succeeded or no explanation available
631
1000
  */
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
- }
1001
+ getExplanation(): MatchExplanation | undefined {
1002
+ return this.explanation;
696
1003
  }
697
1004
  }
698
1005
 
@@ -700,7 +1007,7 @@ class TemplateProcessor {
700
1007
  * Tagged template function for creating patterns.
701
1008
  *
702
1009
  * @param strings The string parts of the template
703
- * @param captures The captures between the string parts (Capture, Any, or string names)
1010
+ * @param captures The captures between the string parts (Capture, Any, RawCode, or string names)
704
1011
  * @returns A Pattern object
705
1012
  *
706
1013
  * @example
@@ -711,17 +1018,84 @@ class TemplateProcessor {
711
1018
  * @example
712
1019
  * // Using any() for non-capturing matches
713
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
+ * ```
714
1034
  */
715
- export function pattern(strings: TemplateStringsArray, ...captures: (Capture | Any<any> | string)[]): Pattern {
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 {
716
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
+ }
717
1079
  const capture = typeof c === "string" ? new CaptureImpl(c) : c;
718
1080
  // Use symbol to get internal name without triggering Proxy
719
1081
  const name = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
720
1082
  return map.set(name, capture);
721
1083
  }, new Map<string, Capture | Any<any>>());
722
- return new Pattern(strings, captures.map(c => {
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
+ }
723
1090
  // Use symbol to get internal name without triggering Proxy
724
1091
  const name = typeof c === "string" ? c : ((c as any)[CAPTURE_NAME_SYMBOL] || c.getName());
725
1092
  return capturesByName.get(name)!;
726
1093
  }));
1094
+
1095
+ // Apply options if provided
1096
+ if (options && Object.keys(options).length > 0) {
1097
+ pat.configure(options);
1098
+ }
1099
+
1100
+ return pat;
727
1101
  }