@openrewrite/rewrite 8.67.0-20251112-160335 → 8.67.0-20251113-160321
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/javascript/comparator.d.ts +64 -3
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +216 -155
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/templating/comparator.d.ts +118 -9
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +821 -73
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/index.d.ts +1 -1
- package/dist/javascript/templating/index.d.ts.map +1 -1
- package/dist/javascript/templating/index.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts +89 -5
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +442 -31
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +157 -1
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/javascript/comparator.ts +249 -169
- package/src/javascript/templating/comparator.ts +952 -87
- package/src/javascript/templating/index.ts +6 -1
- package/src/javascript/templating/pattern.ts +543 -23
- package/src/javascript/templating/types.ts +178 -1
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {Cursor} from '../..';
|
|
17
17
|
import {J} from '../../java';
|
|
18
|
-
import {Any, Capture, PatternOptions} from './types';
|
|
18
|
+
import {Any, Capture, DebugLogEntry, DebugOptions, MatchAttemptResult, MatchExplanation, MatchOptions, PatternOptions, MatchResult as IMatchResult} from './types';
|
|
19
19
|
import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CaptureImpl, RAW_CODE_SYMBOL, RawCode} from './capture';
|
|
20
|
-
import {PatternMatchingComparator} from './comparator';
|
|
20
|
+
import {DebugPatternMatchingComparator, MatcherCallbacks, MatcherState, PatternMatchingComparator} from './comparator';
|
|
21
21
|
import {CaptureMarker, CaptureStorageValue, generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
|
|
22
22
|
import {TemplateEngine} from './engine';
|
|
23
|
+
import {TreePrinters} from '../../print';
|
|
24
|
+
import {JS} from '../index';
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -119,6 +121,9 @@ export class PatternBuilder {
|
|
|
119
121
|
export class Pattern {
|
|
120
122
|
private _options: PatternOptions = {};
|
|
121
123
|
private _cachedAstPattern?: J;
|
|
124
|
+
private static nextPatternId = 1;
|
|
125
|
+
private readonly patternId: number;
|
|
126
|
+
private readonly unnamedCaptureMapping = new Map<string, string>();
|
|
122
127
|
|
|
123
128
|
/**
|
|
124
129
|
* Gets the configuration options for this pattern.
|
|
@@ -156,6 +161,19 @@ export class Pattern {
|
|
|
156
161
|
public readonly templateParts: TemplateStringsArray,
|
|
157
162
|
public readonly captures: (Capture | Any<any> | RawCode)[]
|
|
158
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
|
+
}
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
/**
|
|
@@ -238,9 +256,41 @@ export class Pattern {
|
|
|
238
256
|
* @param cursor Optional cursor at the node's position in a larger tree. Used for context-aware
|
|
239
257
|
* capture constraints to navigate to parent nodes. If omitted, a cursor will be
|
|
240
258
|
* created at the ast root, allowing constraints to navigate within the matched subtree.
|
|
259
|
+
* @param options Optional match options (e.g., debug flag)
|
|
241
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
|
+
* ```
|
|
242
270
|
*/
|
|
243
|
-
async match(ast: J, cursor?: Cursor): Promise<MatchResult | undefined> {
|
|
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
|
|
244
294
|
const matcher = new Matcher(this, ast, cursor);
|
|
245
295
|
const success = await matcher.matches();
|
|
246
296
|
if (!success) {
|
|
@@ -250,6 +300,265 @@ export class Pattern {
|
|
|
250
300
|
const storage = (matcher as any).storage;
|
|
251
301
|
return new MatchResult(new Map(storage));
|
|
252
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
|
+
}
|
|
253
562
|
}
|
|
254
563
|
|
|
255
564
|
/**
|
|
@@ -277,18 +586,16 @@ export class Pattern {
|
|
|
277
586
|
* const capturedArgs = match.get(args); // Returns J[] for variadic captures
|
|
278
587
|
* }
|
|
279
588
|
*/
|
|
280
|
-
export class MatchResult {
|
|
589
|
+
export class MatchResult implements IMatchResult {
|
|
281
590
|
constructor(
|
|
282
591
|
private readonly storage: Map<string, CaptureStorageValue> = new Map()
|
|
283
592
|
) {
|
|
284
593
|
}
|
|
285
594
|
|
|
286
|
-
// Overload: get with
|
|
287
|
-
get<T>(capture: Capture<T[]>): T[] | undefined;
|
|
288
|
-
// Overload: get with regular Capture returns single value
|
|
595
|
+
// Overload: get with Capture returns value
|
|
289
596
|
get<T>(capture: Capture<T>): T | undefined;
|
|
290
|
-
// Overload: get with string returns
|
|
291
|
-
get(capture: string):
|
|
597
|
+
// Overload: get with string returns value
|
|
598
|
+
get(capture: string): any;
|
|
292
599
|
// Implementation
|
|
293
600
|
get(capture: Capture<any> | string): J | J[] | undefined {
|
|
294
601
|
// Use symbol to get internal name without triggering Proxy
|
|
@@ -353,20 +660,29 @@ class Matcher {
|
|
|
353
660
|
private readonly storage = new Map<string, CaptureStorageValue>();
|
|
354
661
|
private patternAst?: J;
|
|
355
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
|
+
|
|
356
669
|
/**
|
|
357
670
|
* Creates a new matcher for a pattern against an AST node.
|
|
358
671
|
*
|
|
359
672
|
* @param pattern The pattern to match
|
|
360
673
|
* @param ast The AST node to match against
|
|
361
674
|
* @param cursor Optional cursor at the AST node's position
|
|
675
|
+
* @param debugOptions Optional debug options for instrumentation
|
|
362
676
|
*/
|
|
363
677
|
constructor(
|
|
364
678
|
private readonly pattern: Pattern,
|
|
365
679
|
private readonly ast: J,
|
|
366
|
-
cursor?: Cursor
|
|
680
|
+
cursor?: Cursor,
|
|
681
|
+
debugOptions?: DebugOptions
|
|
367
682
|
) {
|
|
368
683
|
// If no cursor provided, create one at the ast root so constraints can navigate up
|
|
369
684
|
this.cursor = cursor ?? new Cursor(ast, undefined);
|
|
685
|
+
this.debugOptions = debugOptions ?? {};
|
|
370
686
|
}
|
|
371
687
|
|
|
372
688
|
private readonly cursor: Cursor;
|
|
@@ -422,6 +738,83 @@ class Matcher {
|
|
|
422
738
|
return value as J;
|
|
423
739
|
}
|
|
424
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
|
+
|
|
425
818
|
/**
|
|
426
819
|
* Matches a pattern node against a target node.
|
|
427
820
|
*
|
|
@@ -436,34 +829,87 @@ class Matcher {
|
|
|
436
829
|
// - Deep structural comparison
|
|
437
830
|
// This centralizes all matching logic in one place
|
|
438
831
|
const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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),
|
|
442
838
|
saveState: () => this.saveState(),
|
|
443
|
-
restoreState: (state) => this.restoreState(state)
|
|
444
|
-
|
|
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);
|
|
445
855
|
// Pass cursors to allow constraints to navigate to root
|
|
446
856
|
// Pattern cursor is undefined (pattern is the root), target cursor is provided by user
|
|
447
|
-
|
|
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;
|
|
448
872
|
}
|
|
449
873
|
|
|
450
874
|
/**
|
|
451
|
-
* Saves the current state
|
|
875
|
+
* Saves the current state for backtracking.
|
|
876
|
+
* Includes both capture storage AND debug state (explanation, log, path).
|
|
452
877
|
*
|
|
453
878
|
* @returns A snapshot of the current state
|
|
454
879
|
*/
|
|
455
|
-
private saveState():
|
|
456
|
-
return
|
|
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
|
+
};
|
|
457
889
|
}
|
|
458
890
|
|
|
459
891
|
/**
|
|
460
892
|
* Restores a previously saved state for backtracking.
|
|
893
|
+
* Restores both capture storage AND debug state.
|
|
461
894
|
*
|
|
462
895
|
* @param state The state to restore
|
|
463
896
|
*/
|
|
464
|
-
private restoreState(state:
|
|
897
|
+
private restoreState(state: MatcherState): void {
|
|
898
|
+
// Restore capture storage
|
|
465
899
|
this.storage.clear();
|
|
466
|
-
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
|
+
}
|
|
467
913
|
}
|
|
468
914
|
|
|
469
915
|
/**
|
|
@@ -535,6 +981,26 @@ class Matcher {
|
|
|
535
981
|
|
|
536
982
|
return true;
|
|
537
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
|
+
}
|
|
538
1004
|
}
|
|
539
1005
|
|
|
540
1006
|
/**
|
|
@@ -558,7 +1024,53 @@ class Matcher {
|
|
|
558
1024
|
* const operator = '===';
|
|
559
1025
|
* const pat = pattern`x ${raw(operator)} y`;
|
|
560
1026
|
*/
|
|
561
|
-
|
|
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 {
|
|
562
1074
|
const capturesByName = captures.reduce((map, c) => {
|
|
563
1075
|
// Skip raw code - it's not a capture
|
|
564
1076
|
if (c instanceof RawCode || (typeof c === 'object' && c && (c as any)[RAW_CODE_SYMBOL])) {
|
|
@@ -569,7 +1081,8 @@ export function pattern(strings: TemplateStringsArray, ...captures: (Capture | A
|
|
|
569
1081
|
const name = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
|
|
570
1082
|
return map.set(name, capture);
|
|
571
1083
|
}, new Map<string, Capture | Any<any>>());
|
|
572
|
-
|
|
1084
|
+
|
|
1085
|
+
const pat = new Pattern(strings, captures.map(c => {
|
|
573
1086
|
// Return raw code as-is
|
|
574
1087
|
if (c instanceof RawCode || (typeof c === 'object' && c && (c as any)[RAW_CODE_SYMBOL])) {
|
|
575
1088
|
return c as RawCode;
|
|
@@ -578,4 +1091,11 @@ export function pattern(strings: TemplateStringsArray, ...captures: (Capture | A
|
|
|
578
1091
|
const name = typeof c === "string" ? c : ((c as any)[CAPTURE_NAME_SYMBOL] || c.getName());
|
|
579
1092
|
return capturesByName.get(name)!;
|
|
580
1093
|
}));
|
|
1094
|
+
|
|
1095
|
+
// Apply options if provided
|
|
1096
|
+
if (options && Object.keys(options).length > 0) {
|
|
1097
|
+
pat.configure(options);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return pat;
|
|
581
1101
|
}
|