/* * Copyright 2025 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {Cursor, Tree} from '../..'; import {J} from '../../java'; import {JS} from '../index'; import {JavaScriptSemanticComparatorVisitor} from '../comparator'; import {CaptureMarker, CaptureStorageValue, PlaceholderUtils} from './utils'; import {Capture, CaptureConstraintContext, CaptureMap, DebugLogEntry, MatchExplanation} from './types'; import {CAPTURE_NAME_SYMBOL} from './capture'; /** * Debug callbacks for pattern matching. * These are always used together - either all present or all absent. * Part of Layer 1 (Core Instrumentation). */ export interface DebugCallbacks { log: (level: DebugLogEntry['level'], scope: DebugLogEntry['scope'], message: string, data?: any) => void; setExplanation: (reason: MatchExplanation['reason'], expected: string, actual: string, details?: string) => void; getExplanation: () => MatchExplanation | undefined; restoreExplanation: (explanation: MatchExplanation) => void; clearExplanation: () => void; pushPath: (name: string) => void; popPath: () => void; } /** * Snapshot of matcher state for backtracking. * Includes both capture storage and debug state. */ export interface MatcherState { storage: Map; debugState?: { explanation?: MatchExplanation; logLength: number; path: string[]; }; } /** * Callbacks for the matcher (debug and capture handling). * Part of Layer 1 (Core Instrumentation). */ export interface MatcherCallbacks { handleCapture: (capture: CaptureMarker, target: J, wrapper?: J.RightPadded) => boolean; handleVariadicCapture: (capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded[]) => boolean; saveState: () => MatcherState; restoreState: (state: MatcherState) => void; // Debug callbacks - either all present (when debugging enabled) or absent debug?: DebugCallbacks; } /** * Implementation of CaptureMap that wraps the capture storage. * Provides read-only access to previously matched captures. */ class CaptureMapImpl implements CaptureMap { constructor(private readonly storage: Map) {} get(capture: Capture): T | undefined; get(capture: string): any; get(capture: Capture | string): any { // Use symbol to get internal name without triggering Proxy const name = typeof capture === 'string' ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName()); return this.storage.get(name); } has(capture: Capture | string): boolean { // Use symbol to get internal name without triggering Proxy const name = typeof capture === 'string' ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName()); return this.storage.has(name); } } /** * A comparator for pattern matching that is lenient about optional properties. * Allows patterns without type annotations to match actual code with type annotations. * Uses semantic comparison to match semantically equivalent code (e.g., isDate() and util.isDate()). */ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisitor { constructor( protected readonly matcher: MatcherCallbacks, lenientTypeMatching: boolean = true ) { // Enable lenient type matching based on pattern configuration (default: true for backward compatibility) super(lenientTypeMatching); } /** * Builds the constraint context with the cursor and current captures. * @param cursor The cursor to include in the context * @returns The constraint context for evaluating capture constraints */ protected buildConstraintContext(cursor: Cursor): CaptureConstraintContext { const state = this.matcher.saveState(); return { cursor, captures: new CaptureMapImpl(state.storage) }; } override async visit(j: Tree, p: J, parent?: Cursor): Promise { // Check if the pattern node is a capture - this handles unwrapped captures // (Wrapped captures in J.RightPadded are handled by visitRightPadded override) // Note: targetCursor will be pushed by parent's visit() method after this check const captureMarker = PlaceholderUtils.getCaptureMarker(j)!; if (captureMarker) { // Push targetCursor to position it at the captured node for constraint evaluation // Only create cursor if targetCursor was initialized (meaning user provided one) const savedTargetCursor = this.targetCursor; const cursorAtCapturedNode = this.targetCursor !== undefined ? new Cursor(p, this.targetCursor) : new Cursor(p); this.targetCursor = cursorAtCapturedNode; try { // Evaluate constraint with context (cursor + previous captures) // Skip constraint for variadic captures - they're evaluated in matchSequence with the full array if (captureMarker.constraint && !captureMarker.variadicOptions) { const context = this.buildConstraintContext(cursorAtCapturedNode); if (!captureMarker.constraint(p, context)) { const captureName = captureMarker.captureName || 'unnamed'; const targetKind = (p as any).kind || 'unknown'; return this.constraintFailed(captureName, targetKind) as R; } } const success = this.matcher.handleCapture(captureMarker, p, undefined); if (!success) { const captureName = captureMarker.captureName || 'unnamed'; return this.captureConflict(captureName) as R; } return j as R; } finally { this.targetCursor = savedTargetCursor; } } if (!this.match) { return j as R; } // Continue with parent's visit which will push targetCursor and traverse return await super.visit(j, p, parent); } protected hasSameKind(j: J, other: J): boolean { return super.hasSameKind(j, other) || (j.kind == J.Kind.Identifier && PlaceholderUtils.isCapture(j as J.Identifier)); } /** * Additional specialized abort methods for pattern matching scenarios. */ protected constraintFailed(captureName: string, targetKind: string) { const pattern = this.cursor?.value as any; return this.abort(pattern, 'constraint-failed', `capture[${captureName}]`, 'constraint satisfied', `constraint failed for ${targetKind}`); } protected captureConflict(captureName: string) { const pattern = this.cursor?.value as any; return this.abort(pattern, 'capture-conflict', `capture[${captureName}]`, 'compatible binding', 'conflicting binding'); } /** * Override visitRightPadded to check if this wrapper has a CaptureMarker. * If so, capture the entire wrapper (to preserve markers like semicolons). */ override async visitRightPadded(right: J.RightPadded, p: J): Promise> { if (!this.match) { return right; } // Check if this RightPadded has a CaptureMarker (attached during pattern construction) // Note: Markers are now only at the wrapper level, not at the element level const captureMarker = PlaceholderUtils.getCaptureMarker(right); if (captureMarker) { // Extract the target wrapper if it's also a RightPadded const isRightPadded = (p as any).kind === J.Kind.RightPadded; const targetWrapper = isRightPadded ? (p as unknown) as J.RightPadded : undefined; const targetElement = isRightPadded ? targetWrapper!.element : p; // Push targetCursor to position it at the captured element for constraint evaluation const savedTargetCursor = this.targetCursor; const cursorAtCapturedNode = this.targetCursor !== undefined ? (targetWrapper ? new Cursor(targetWrapper, this.targetCursor) : new Cursor(targetElement, this.targetCursor)) : (targetWrapper ? new Cursor(targetWrapper) : new Cursor(targetElement)); this.targetCursor = cursorAtCapturedNode; try { // Evaluate constraint with cursor at the captured node (always defined) // Skip constraint for variadic captures - they're evaluated in matchSequence with the full array if (captureMarker.constraint && !captureMarker.variadicOptions && !captureMarker.constraint(targetElement as J, this.buildConstraintContext(cursorAtCapturedNode))) { const captureName = captureMarker.captureName || 'unnamed'; const targetKind = (targetElement as any).kind || 'unknown'; return this.constraintFailed(captureName, targetKind); } // Handle the capture with the wrapper - use the element for pattern matching const success = this.matcher.handleCapture(captureMarker, targetElement as J, targetWrapper as J.RightPadded | undefined); if (!success) { const captureName = captureMarker.captureName || 'unnamed'; return this.captureConflict(captureName); } return right; } finally { this.targetCursor = savedTargetCursor; } } // Not a capture wrapper - use parent implementation return super.visitRightPadded(right, p); } override async visitContainer(container: J.Container, p: J): Promise> { // Check if any elements are variadic captures const hasVariadicCapture = container.elements.some(elem => PlaceholderUtils.isVariadicCapture(elem) ); // If no variadic captures, use parent implementation if (!hasVariadicCapture) { return super.visitContainer(container, p); } // Otherwise, handle variadic captures ourselves if (!this.match) { return container; } // Extract the other container const isContainer = (p as any).kind === J.Kind.Container; if (!isContainer) { // Set up cursors temporarily for kindMismatch to use const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(container, this.cursor); this.targetCursor = new Cursor(p, this.targetCursor); try { return this.kindMismatch(); } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } } const otherContainer = p as unknown as J.Container; // Push wrappers onto both cursors const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(container, this.cursor); this.targetCursor = new Cursor(otherContainer, this.targetCursor); try { // Use matchSequence for variadic matching // filterEmpty=true to skip J.Empty elements (they represent missing elements in destructuring) if (!await this.matchSequence(container.elements as J.RightPadded[], otherContainer.elements as J.RightPadded[], true)) { return this.structuralMismatch('elements'); } } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } return container; } /** * Visit a single element in a container (for non-variadic matching). * Extracted to allow debug subclass to add path tracking. * * @param element The pattern element * @param otherElement The target element * @param index The index in the container * @returns true if matching should continue, false if it failed */ protected async visitContainerElement( element: J.RightPadded, otherElement: J.RightPadded, index: number ): Promise { await this.visitRightPadded(element as any, otherElement as any); return this.match; } override async visitMethodInvocation(methodInvocation: J.MethodInvocation, other: J): Promise { // Check if any arguments are variadic captures const hasVariadicCapture = methodInvocation.arguments.elements.some(arg => PlaceholderUtils.isVariadicCapture(arg) ); // If no variadic captures, use parent implementation (which includes semantic/type-aware matching) if (!hasVariadicCapture) { return super.visitMethodInvocation(methodInvocation, other); } // Otherwise, handle variadic captures ourselves if (!this.match) { return this.abort(methodInvocation); } if (other.kind !== J.Kind.MethodInvocation) { // Set up cursors for kindMismatch const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(methodInvocation, this.cursor); this.targetCursor = new Cursor(other, this.targetCursor); try { return this.kindMismatch(); } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } } const otherMethodInvocation = other as J.MethodInvocation; // Set up cursors for the entire method const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(methodInvocation, this.cursor); this.targetCursor = new Cursor(otherMethodInvocation, this.targetCursor); try { // Compare select if ((methodInvocation.select === undefined) !== (otherMethodInvocation.select === undefined)) { return this.structuralMismatch('select'); } // Visit select if present if (methodInvocation.select && otherMethodInvocation.select) { await this.visit(methodInvocation.select.element, otherMethodInvocation.select.element); if (!this.match) return methodInvocation; } // Compare typeParameters if ((methodInvocation.typeParameters === undefined) !== (otherMethodInvocation.typeParameters === undefined)) { return this.structuralMismatch('typeParameters'); } // Visit typeParameters if present if (methodInvocation.typeParameters && otherMethodInvocation.typeParameters) { if (methodInvocation.typeParameters.elements.length !== otherMethodInvocation.typeParameters.elements.length) { return this.arrayLengthMismatch('typeParameters.elements'); } // Visit each type parameter in lock step (visit RightPadded to check for markers) for (let i = 0; i < methodInvocation.typeParameters.elements.length; i++) { await this.visitRightPadded(methodInvocation.typeParameters.elements[i], otherMethodInvocation.typeParameters.elements[i] as any); if (!this.match) return methodInvocation; } } // Visit name await this.visit(methodInvocation.name, otherMethodInvocation.name); if (!this.match) { return methodInvocation; } // Special handling for variadic captures in arguments if (!await this.matchArguments(methodInvocation.arguments.elements, otherMethodInvocation.arguments.elements)) { return this.structuralMismatch('arguments'); } return methodInvocation; } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } } override async visitBlock(block: J.Block, other: J): Promise { // Check if any statements have CaptureMarker indicating they're variadic const hasVariadicCapture = block.statements.some(stmt => { const captureMarker = PlaceholderUtils.getCaptureMarker(stmt); return captureMarker?.variadicOptions !== undefined; }); // If no variadic captures, use parent implementation if (!hasVariadicCapture) { return super.visitBlock(block, other); } // Otherwise, handle variadic captures ourselves if (!this.match) { return this.abort(block); } if (other.kind !== J.Kind.Block) { // Set up cursors for kindMismatch const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(block, this.cursor); this.targetCursor = new Cursor(other, this.targetCursor); try { return this.kindMismatch(); } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } } const otherBlock = other as J.Block; // Set up cursors for structural comparison const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(block, this.cursor); this.targetCursor = new Cursor(otherBlock, this.targetCursor); try { // Special handling for variadic captures in statements if (!await this.matchSequence(block.statements, otherBlock.statements, false)) { return this.structuralMismatch('statements'); } return block; } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } } override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, other: J): Promise { // Check if any statements are variadic captures const hasVariadicCapture = compilationUnit.statements.some(stmt => { return PlaceholderUtils.isVariadicCapture(stmt); }); // If no variadic captures, use parent implementation if (!hasVariadicCapture) { return super.visitJsCompilationUnit(compilationUnit, other); } // Otherwise, handle variadic captures ourselves if (!this.match) { return this.abort(compilationUnit); } if (other.kind !== JS.Kind.CompilationUnit) { // Set up cursors for kindMismatch const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(compilationUnit, this.cursor); this.targetCursor = new Cursor(other, this.targetCursor); try { return this.kindMismatch(); } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } } const otherCompilationUnit = other as JS.CompilationUnit; // Set up cursors for structural comparison const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(compilationUnit, this.cursor); this.targetCursor = new Cursor(otherCompilationUnit, this.targetCursor); try { // Special handling for variadic captures in top-level statements if (!await this.matchSequence(compilationUnit.statements, otherCompilationUnit.statements, false)) { return this.structuralMismatch('statements'); } return compilationUnit; } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } } /** * Matches argument lists, with special handling for variadic captures. * A variadic capture can match zero or more consecutive arguments. */ private async matchArguments(patternArgs: J.RightPadded[], targetArgs: J.RightPadded[]): Promise { return await this.matchSequence(patternArgs, targetArgs, true); } /** * Generic sequence matching with variadic capture support. * Works for any sequence of JRightPadded elements (arguments, statements, etc.). * A variadic capture can match zero or more consecutive elements. * * Uses pivot detection to optimize matching, with backtracking as fallback. * * @param patternElements The pattern elements (JRightPadded) * @param targetElements The target elements to match against (JRightPadded) * @param filterEmpty Whether to filter out J.Empty elements when capturing (true for arguments, false for statements) * @returns true if the sequence matches, false otherwise */ protected async matchSequence(patternElements: J.RightPadded[], targetElements: J.RightPadded[], filterEmpty: boolean): Promise { return await this.matchSequenceOptimized(patternElements, targetElements, 0, 0, filterEmpty); } /** * Optimized sequence matcher with pivot detection and backtracking. * For variadic patterns, tries to detect pivots (where next pattern matches) to avoid * unnecessary backtracking. Falls back to full backtracking when pivots are ambiguous. * * @param patternElements The pattern elements (JRightPadded) * @param targetElements The target elements to match against (JRightPadded) * @param patternIdx Current position in pattern * @param targetIdx Current position in target * @param filterEmpty Whether to filter out J.Empty elements when capturing * @returns true if the remaining sequence matches, false otherwise */ protected async matchSequenceOptimized( patternElements: J.RightPadded[], targetElements: J.RightPadded[], patternIdx: number, targetIdx: number, filterEmpty: boolean ): Promise { // Base case: all patterns matched if (patternIdx >= patternElements.length) { return targetIdx >= targetElements.length; // Success if all targets consumed } // Check for markers at wrapper level only (markers are now only at the outermost level) const patternWrapper = patternElements[patternIdx]; const captureMarker = PlaceholderUtils.getCaptureMarker(patternWrapper); const isVariadic = captureMarker?.variadicOptions !== undefined; if (isVariadic) { // Variadic pattern: try different consumption amounts with backtracking const variadicOptions = captureMarker!.variadicOptions; const min = variadicOptions?.min ?? 0; const max = variadicOptions?.max ?? Infinity; // Calculate maximum possible consumption and check if remaining patterns are deterministic let nonVariadicRemainingPatterns = 0; let allRemainingPatternsAreDeterministic = true; for (let i = patternIdx + 1; i < patternElements.length; i++) { const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(patternElements[i]); const nextIsVariadic = nextCaptureMarker?.variadicOptions !== undefined; if (!nextIsVariadic) { nonVariadicRemainingPatterns++; } // A pattern is deterministic if it's not a capture at all (i.e., a literal/fixed structure) // Variadic captures and non-variadic captures are both non-deterministic if (nextCaptureMarker) { allRemainingPatternsAreDeterministic = false; } } const remainingTargetElements = targetElements.length - targetIdx; const maxPossible = Math.min(remainingTargetElements - nonVariadicRemainingPatterns, max); // Pivot detection optimization: try to find where next pattern matches // This avoids unnecessary backtracking when constraints make the split point obvious let pivotDetected = false; let pivotAt = -1; // Skip pivot detection if we're using deterministic optimization // (when all remaining patterns are literals, there's only ONE valid consumption amount) const useDeterministicOptimization = allRemainingPatternsAreDeterministic && maxPossible >= min && maxPossible <= max; if (!useDeterministicOptimization && patternIdx + 1 < patternElements.length && min <= maxPossible) { const nextPattern = patternElements[patternIdx + 1]; // Scan through possible consumption amounts starting from min for (let tryConsume = min; tryConsume <= maxPossible; tryConsume++) { // Check if element after our consumption would match next pattern if (targetIdx + tryConsume < targetElements.length) { const candidateElement = targetElements[targetIdx + tryConsume]; // Skip J.Empty for arguments if (filterEmpty && candidateElement.element.kind === J.Kind.Empty) { continue; } // Test if next pattern matches this element const savedMatch = this.match; const savedState = this.matcher.saveState(); await this.visitRightPadded(nextPattern, candidateElement as any); const matchesNext = this.match; this.match = savedMatch; this.matcher.restoreState(savedState); if (matchesNext) { // Found pivot! Try this consumption amount first pivotDetected = true; pivotAt = tryConsume; break; } } } } // Determine consumption order const consumptionOrder: number[] = []; // OPTIMIZATION: If all remaining patterns are deterministic (literals, not captures), // there's only ONE mathematically valid consumption amount. Skip backtracking entirely. // Example: foo(${args}, 999) matching foo(1,2,42) -> args MUST be [1,2], only try consume=2 if (useDeterministicOptimization) { consumptionOrder.push(maxPossible); } else if (pivotDetected && pivotAt >= 0) { // Try pivot first, then others as fallback consumptionOrder.push(pivotAt); for (let c = maxPossible; c >= min; c--) { if (c !== pivotAt) { consumptionOrder.push(c); } } } else { // Greedy approach: max to min for (let c = maxPossible; c >= min; c--) { consumptionOrder.push(c); } } for (const consume of consumptionOrder) { // Capture elements for this consumption amount // For empty argument lists, there will be a single J.Empty element that we need to filter out const rawWrappers = targetElements.slice(targetIdx, targetIdx + consume); const capturedWrappers = filterEmpty ? rawWrappers.filter(w => w.element.kind !== J.Kind.Empty) : rawWrappers; const capturedElements: J[] = capturedWrappers.map(w => w.element); // Check min/max constraints against filtered elements if (capturedElements.length < min || capturedElements.length > max) { continue; } // Evaluate constraint for variadic capture // For variadic captures, constraint receives the entire array of captured elements // The targetCursor points to the parent container (always defined in container matching) if (captureMarker.constraint) { const cursor = this.targetCursor || new Cursor(targetElements[0]); if (!captureMarker.constraint(capturedElements as any, this.buildConstraintContext(cursor))) { continue; // Try next consumption amount } } // Save current state for backtracking const savedState = this.matcher.saveState(); // Handle the variadic capture const success = this.matcher.handleVariadicCapture(captureMarker, capturedElements, capturedWrappers); if (!success) { // Restore state and try next amount this.matcher.restoreState(savedState); continue; } // Try to match the rest of the pattern const restMatches = await this.matchSequenceOptimized( patternElements, targetElements, patternIdx + 1, targetIdx + consume, filterEmpty ); if (restMatches) { return true; // Found a valid matching } // Backtrack: restore state and try next amount this.matcher.restoreState(savedState); } return false; // No consumption amount worked } else { // Regular non-variadic element - must match exactly one target element if (targetIdx >= targetElements.length) { return false; // Pattern has more elements than target } const targetWrapper = targetElements[targetIdx]; const targetElement = targetWrapper.element; // For arguments, J.Empty represents no argument, so regular captures should not match it if (filterEmpty && targetElement.kind === J.Kind.Empty) { return false; } if (!await this.visitSequenceElement(patternWrapper, targetWrapper, targetIdx)) { return false; } // Continue matching the rest return await this.matchSequenceOptimized( patternElements, targetElements, patternIdx + 1, targetIdx + 1, filterEmpty ); } } /** * Visit a single element in a sequence during non-variadic matching. * Extracted to allow debug subclass to add path tracking. * * @param patternWrapper The pattern element * @param targetWrapper The target element * @param targetIdx The index in the target sequence * @returns true if matching succeeded, false otherwise */ protected async visitSequenceElement( patternWrapper: J.RightPadded, targetWrapper: J.RightPadded, targetIdx: number ): Promise { // Save current state for backtracking (both match state and capture bindings) const savedMatch = this.match; const savedState = this.matcher.saveState(); await this.visitRightPadded(patternWrapper, targetWrapper as any); if (!this.match) { // Restore state on match failure this.match = savedMatch; this.matcher.restoreState(savedState); return false; } return true; } } /** * Debug-instrumented version of PatternMatchingComparator. * Overrides methods to add path tracking, logging, and explanation capture. * Zero cost when not instantiated - production code uses the base class. */ export class DebugPatternMatchingComparator extends PatternMatchingComparator { private get debug(): DebugCallbacks { return this.matcher.debug!; } /** * Extracts the last segment of a kind string (after the last dot). * For example: "org.openrewrite.java.tree.J.MethodInvocation" -> "MethodInvocation" */ private formatKind(kind: string): string { return kind.substring(kind.lastIndexOf('.') + 1); } /** * Formats a value for display in error messages. */ private formatValue(value: any): string { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'string') return `"${value}"`; if (typeof value === 'number' || typeof value === 'boolean') return String(value); // For objects with a kind property (LST nodes) if (value && typeof value === 'object' && value.kind) { const kind = this.formatKind(value.kind); // Show key identifying properties for common node types if (value.simpleName) return `${kind}("${value.simpleName}")`; if (value.value !== undefined) return `${kind}(${this.formatValue(value.value)})`; return kind; } return String(value); } /** * Override abort to capture explanation when debug is enabled. * Only sets explanation on the first abort call (when this.match is still true). * This preserves the most specific explanation closest to the actual mismatch. */ protected override abort(t: T, reason?: string, propertyName?: string, expected?: any, actual?: any): T { // If already aborted, don't overwrite the explanation // The first abort is typically the most specific if (!this.match) { return t; } // If we have context about the mismatch, capture it if (reason && this.debug && (expected !== undefined || actual !== undefined)) { const expectedStr = this.formatValue(expected); const actualStr = this.formatValue(actual); this.debug.setExplanation( reason as any, expectedStr, actualStr, 'Property values do not match' ); } // Set `this.match = false` return super.abort(t, reason, propertyName, expected, actual); } /** * Override helper methods to extract detailed context from cursors. */ protected override kindMismatch() { const pattern = this.cursor?.value as any; const target = this.targetCursor?.value as any; // Pass the full kind strings - formatValue() will detect and format them return this.abort(pattern, 'kind-mismatch', 'kind', this.formatKind(pattern?.kind), this.formatKind(target?.kind)); } protected override structuralMismatch(propertyName: string) { const pattern = this.cursor?.value as any; const target = this.targetCursor?.value as any; const expectedValue = pattern?.[propertyName]; const actualValue = target?.[propertyName]; return this.abort(pattern, 'structural-mismatch', propertyName, expectedValue, actualValue); } protected override arrayLengthMismatch(propertyName: string) { const pattern = this.cursor?.value as any; const target = this.targetCursor?.value as any; const expectedArray = pattern?.[propertyName]; const actualArray = target?.[propertyName]; const expectedLen = Array.isArray(expectedArray) ? expectedArray.length : 'not an array'; const actualLen = Array.isArray(actualArray) ? actualArray.length : 'not an array'; return this.abort(pattern, 'array-length-mismatch', propertyName, expectedLen, actualLen); } protected override valueMismatch(propertyName?: string, expected?: any, actual?: any) { const pattern = this.cursor?.value as any; const target = this.targetCursor?.value as any; // Track number of paths pushed for cleanup let pathsPushed = 0; // Handle path tracking only if propertyName is provided if (propertyName) { // Split dotted property paths (e.g., "name.simpleName" → ["name", "simpleName"]) const pathParts = propertyName.split('.'); pathsPushed = pathParts.length; // Add each property to path with kind information for nested objects const kindStr = this.formatKind(pattern?.kind); this.debug.pushPath(`${kindStr}#${pathParts[0]}`); // For nested properties, try to get the kind of intermediate objects let currentObj = pattern?.[pathParts[0]]; for (let i = 1; i < pathParts.length; i++) { if (currentObj && typeof currentObj === 'object' && currentObj.kind) { // Include the kind of the nested object const nestedKind = this.formatKind(currentObj.kind); this.debug.pushPath(`${nestedKind}#${pathParts[i]}`); } else { // Fallback to just the property name if no kind available this.debug.pushPath(pathParts[i]); } currentObj = currentObj?.[pathParts[i]]; } } try { // If expected/actual provided, use them directly if (expected !== undefined || actual !== undefined) { return this.abort(pattern, 'value-mismatch', propertyName, expected, actual); } // Otherwise, try to extract from cursors (fallback for older code) if (propertyName) { // Navigate dotted property paths const getNestedValue = (obj: any, path: string) => { return path.split('.').reduce((current, prop) => current?.[prop], obj); }; const expectedValue = getNestedValue(pattern, propertyName); const actualValue = getNestedValue(target, propertyName); return this.abort(pattern, 'value-mismatch', propertyName, expectedValue, actualValue); } else { // No property name - compare whole objects return this.abort(pattern, 'value-mismatch', propertyName, pattern, target); } } finally { // Pop all the path components we pushed for (let i = 0; i < pathsPushed; i++) { this.debug.popPath(); } } } override async visit(j: Tree, p: J, parent?: Cursor): Promise { const captureMarker = PlaceholderUtils.getCaptureMarker(j)!; if (captureMarker) { const savedTargetCursor = this.targetCursor; const cursorAtCapturedNode = this.targetCursor !== undefined ? new Cursor(p, this.targetCursor) : new Cursor(p); this.targetCursor = cursorAtCapturedNode; try { if (captureMarker.constraint && !captureMarker.variadicOptions) { this.debug.log('debug', 'constraint', `Evaluating constraint for capture: ${captureMarker.captureName}`); const constraintResult = captureMarker.constraint(p, this.buildConstraintContext(cursorAtCapturedNode)); if (!constraintResult) { this.debug.log('info', 'constraint', `Constraint failed for capture: ${captureMarker.captureName}`); this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(p as any).kind}`, `Constraint evaluation returned false`); return this.abort(j) as R; } this.debug.log('debug', 'constraint', `Constraint passed for capture: ${captureMarker.captureName}`); } const success = this.matcher.handleCapture(captureMarker, p, undefined); if (!success) { return this.abort(j) as R; } return j as R; } finally { this.targetCursor = savedTargetCursor; } } return await super.visit(j, p, parent); } protected override async visitElement(j: T, other: T): Promise { if (!this.match) { return j; } const kindStr = this.formatKind(j.kind); if (j.kind !== other.kind) { return this.abort(j, 'kind-mismatch', 'kind', kindStr, this.formatKind(other.kind)); } for (const key of Object.keys(j)) { if (key.startsWith('_') || key === 'kind' || key === 'id' || key === 'markers' || key === 'prefix') { continue; } const jValue = (j as any)[key]; const otherValue = (other as any)[key]; if (Array.isArray(jValue)) { if (!Array.isArray(otherValue) || jValue.length !== otherValue.length) { this.debug.pushPath(`${kindStr}#${key}`); const result = this.abort(j, 'array-length-mismatch', key, jValue.length, Array.isArray(otherValue) ? otherValue.length : otherValue); this.debug.popPath(); return result; } for (let i = 0; i < jValue.length; i++) { this.debug.pushPath(`${kindStr}#${key}`); this.debug.pushPath(i.toString()); try { await this.visitProperty(jValue[i], otherValue[i]); if (!this.match) { return j; } } finally { this.debug.popPath(); this.debug.popPath(); } } } else { this.debug.pushPath(`${kindStr}#${key}`); try { await this.visitProperty(jValue, otherValue); if (!this.match) { return j; } } finally { this.debug.popPath(); } } } return j; } override async visitRightPadded(right: J.RightPadded, p: J): Promise> { if (!this.match) { return right; } const captureMarker = PlaceholderUtils.getCaptureMarker(right); if (captureMarker) { const isRightPadded = (p as any).kind === J.Kind.RightPadded; const targetWrapper = isRightPadded ? (p as unknown) as J.RightPadded : undefined; const targetElement = isRightPadded ? targetWrapper!.element : p; const savedTargetCursor = this.targetCursor; const cursorAtCapturedNode = this.targetCursor !== undefined ? (targetWrapper ? new Cursor(targetWrapper, this.targetCursor) : new Cursor(targetElement, this.targetCursor)) : (targetWrapper ? new Cursor(targetWrapper) : new Cursor(targetElement)); this.targetCursor = cursorAtCapturedNode; try { if (captureMarker.constraint && !captureMarker.variadicOptions) { this.debug.log('debug', 'constraint', `Evaluating constraint for wrapped capture: ${captureMarker.captureName}`); const constraintResult = captureMarker.constraint(targetElement as J, this.buildConstraintContext(cursorAtCapturedNode)); if (!constraintResult) { this.debug.log('info', 'constraint', `Constraint failed for wrapped capture: ${captureMarker.captureName}`); this.debug.setExplanation('constraint-failed', `Capture ${captureMarker.captureName} with valid constraint`, `Constraint failed for ${(targetElement as any).kind}`, `Constraint evaluation returned false`); return this.abort(right); } this.debug.log('debug', 'constraint', `Constraint passed for wrapped capture: ${captureMarker.captureName}`); } const success = this.matcher.handleCapture(captureMarker, targetElement as J, targetWrapper as J.RightPadded | undefined); if (!success) { return this.abort(right); } return right; } finally { this.targetCursor = savedTargetCursor; } } return await super.visitRightPadded(right, p); } override async visitContainer(container: J.Container, p: J): Promise> { if (!this.match) { return container; } const isContainer = (p as any).kind === J.Kind.Container; if (!isContainer) { return this.abort(container); } const otherContainer = p as unknown as J.Container; const hasVariadicCapture = container.elements.some(elem => PlaceholderUtils.isVariadicCapture(elem) ); const savedCursor = this.cursor; const savedTargetCursor = this.targetCursor; this.cursor = new Cursor(container, this.cursor); this.targetCursor = new Cursor(otherContainer, this.targetCursor); try { if (hasVariadicCapture) { if (!await this.matchSequence(container.elements as J.RightPadded[], otherContainer.elements as J.RightPadded[], true)) { return this.arrayLengthMismatch('elements'); } } else { // Non-variadic path - track indices if (container.elements.length !== otherContainer.elements.length) { return this.arrayLengthMismatch('elements'); } for (let i = 0; i < container.elements.length; i++) { this.debug.pushPath(i.toString()); try { if (!await this.visitContainerElement(container.elements[i], otherContainer.elements[i], i)) { return container; } } finally { this.debug.popPath(); } } } } finally { this.cursor = savedCursor; this.targetCursor = savedTargetCursor; } return container; } /** * Override visitContainerProperty to add path tracking with property context. */ protected override async visitContainerProperty( propertyName: string, container: J.Container, otherContainer: J.Container ): Promise> { // Get parent from cursor const parent = this.cursor.value as J; // Push path for the property const kindStr = this.formatKind((parent as any).kind); this.debug.pushPath(`${kindStr}#${propertyName}`); try { await this.visitContainer(container, otherContainer as any); return container; } finally { this.debug.popPath(); } } /** * Override visitRightPaddedProperty to add path tracking with property context. */ protected override async visitRightPaddedProperty( propertyName: string, rightPadded: J.RightPadded, otherRightPadded: J.RightPadded ): Promise> { // Get parent from cursor const parent = this.cursor.value as J; // Push path for the property const kindStr = this.formatKind((parent as any).kind); this.debug.pushPath(`${kindStr}#${propertyName}`); try { return await this.visitRightPadded(rightPadded, otherRightPadded as any); } finally { this.debug.popPath(); } } /** * Override visitLeftPaddedProperty to add path tracking with property context. */ protected override async visitLeftPaddedProperty( propertyName: string, leftPadded: J.LeftPadded, otherLeftPadded: J.LeftPadded ): Promise> { // Get parent from cursor const parent = this.cursor.value as J; // Push path for the property const kindStr = this.formatKind((parent as any).kind); this.debug.pushPath(`${kindStr}#${propertyName}`); try { return await this.visitLeftPadded(leftPadded, otherLeftPadded as any); } finally { this.debug.popPath(); } } protected override async visitContainerElement( element: J.RightPadded, otherElement: J.RightPadded, index: number ): Promise { // Don't push index here - it should be handled by the caller with proper context return await super.visitContainerElement(element, otherElement, index); } protected override async visitArrayProperty( parent: J, propertyName: string, array1: T[], array2: T[], visitor: (item1: T, item2: T, index: number) => Promise ): Promise { // Push path for the property const kindStr = this.formatKind((parent as any).kind); this.debug.pushPath(`${kindStr}#${propertyName}`); try { // Check length mismatch (will have path context) if (array1.length !== array2.length) { this.arrayLengthMismatch(propertyName); return; } // Visit each element with index tracking for (let i = 0; i < array1.length; i++) { this.debug.pushPath(i.toString()); try { await visitor(array1[i], array2[i], i); if (!this.match) { return; } } finally { this.debug.popPath(); } } } finally { this.debug.popPath(); } } protected override async matchSequence( patternElements: J.RightPadded[], targetElements: J.RightPadded[], filterEmpty: boolean ): Promise { // Push path component for the container // Extract kind from cursors if available const pattern = this.cursor?.value as any; if (pattern && pattern.kind) { const kindStr = this.formatKind(pattern.kind); // Determine property name based on the kind let propertyName = 'elements'; if (pattern.kind.includes('MethodInvocation')) { propertyName = 'arguments'; } else if (pattern.kind.includes('Block')) { propertyName = 'statements'; } this.debug.pushPath(`${kindStr}#${propertyName}`); } try { return await super.matchSequence(patternElements, targetElements, filterEmpty); } finally { if (this.cursor?.value) { this.debug.popPath(); } } } protected override async visitSequenceElement( patternWrapper: J.RightPadded, targetWrapper: J.RightPadded, targetIdx: number ): Promise { this.debug.pushPath(targetIdx.toString()); try { // Save current state for backtracking (both match state and capture bindings) const savedMatch = this.match; const savedState = this.matcher.saveState(); await this.visitRightPadded(patternWrapper, targetWrapper as any); if (!this.match) { // Preserve explanation before restoring state const explanation = this.debug.getExplanation(); // Restore state on match failure this.match = savedMatch; this.matcher.restoreState(savedState); // Restore the explanation if one was set during matching if (explanation) { this.debug.restoreExplanation(explanation); } return false; } return true; } finally { this.debug.popPath(); } } protected override async matchSequenceOptimized( patternElements: J.RightPadded[], targetElements: J.RightPadded[], patternIdx: number, targetIdx: number, filterEmpty: boolean ): Promise { if (patternIdx >= patternElements.length) { return targetIdx >= targetElements.length; } const patternWrapper = patternElements[patternIdx]; const captureMarker = PlaceholderUtils.getCaptureMarker(patternWrapper); const isVariadic = captureMarker?.variadicOptions !== undefined; if (isVariadic) { const variadicOptions = captureMarker!.variadicOptions; const min = variadicOptions?.min ?? 0; const max = variadicOptions?.max ?? Infinity; let nonVariadicRemainingPatterns = 0; let allRemainingPatternsAreDeterministic = true; for (let i = patternIdx + 1; i < patternElements.length; i++) { const nextCaptureMarker = PlaceholderUtils.getCaptureMarker(patternElements[i]); const nextIsVariadic = nextCaptureMarker?.variadicOptions !== undefined; if (!nextIsVariadic) { nonVariadicRemainingPatterns++; } if (nextCaptureMarker) { allRemainingPatternsAreDeterministic = false; } } const remainingTargetElements = targetElements.length - targetIdx; const maxPossible = Math.min(remainingTargetElements - nonVariadicRemainingPatterns, max); let pivotDetected = false; let pivotAt = -1; // Skip pivot detection if we're using deterministic optimization // (when all remaining patterns are literals, there's only ONE valid consumption amount) const useDeterministicOptimization = allRemainingPatternsAreDeterministic && maxPossible >= min && maxPossible <= max; if (!useDeterministicOptimization && patternIdx + 1 < patternElements.length && min <= maxPossible) { const nextPattern = patternElements[patternIdx + 1]; for (let tryConsume = min; tryConsume <= maxPossible; tryConsume++) { if (targetIdx + tryConsume < targetElements.length) { const candidateElement = targetElements[targetIdx + tryConsume]; if (filterEmpty && candidateElement.element.kind === J.Kind.Empty) { continue; } const savedMatch = this.match; const savedState = this.matcher.saveState(); await this.visitRightPadded(nextPattern, candidateElement as any); const matchesNext = this.match; this.match = savedMatch; this.matcher.restoreState(savedState); if (matchesNext) { pivotDetected = true; pivotAt = tryConsume; break; } } } } const consumptionOrder: number[] = []; // OPTIMIZATION: If all remaining patterns are deterministic (literals, not captures), // there's only ONE mathematically valid consumption amount. Skip backtracking entirely. // Example: foo(${args}, 999) matching foo(1,2,42) -> args MUST be [1,2], only try consume=2 if (useDeterministicOptimization) { consumptionOrder.push(maxPossible); } else if (pivotDetected && pivotAt >= 0) { consumptionOrder.push(pivotAt); for (let c = maxPossible; c >= min; c--) { if (c !== pivotAt) { consumptionOrder.push(c); } } } else { for (let c = maxPossible; c >= min; c--) { consumptionOrder.push(c); } } for (const consume of consumptionOrder) { // Capture elements for this consumption amount // For empty argument lists, there will be a single J.Empty element that we need to filter out const rawWrappers = targetElements.slice(targetIdx, targetIdx + consume); const capturedWrappers = filterEmpty ? rawWrappers.filter(w => w.element.kind !== J.Kind.Empty) : rawWrappers; const capturedElements: J[] = capturedWrappers.map(w => w.element); // Check min/max constraints against filtered elements if (capturedElements.length < min || capturedElements.length > max) { continue; } if (captureMarker.constraint) { this.debug.log('debug', 'constraint', `Evaluating variadic constraint for capture: ${captureMarker.captureName} (${capturedElements.length} elements)`); const cursor = this.targetCursor || new Cursor(targetElements[0]); const constraintResult = captureMarker.constraint(capturedElements as any, this.buildConstraintContext(cursor)); if (!constraintResult) { this.debug.log('info', 'constraint', `Variadic constraint failed for capture: ${captureMarker.captureName}`); continue; } this.debug.log('debug', 'constraint', `Variadic constraint passed for capture: ${captureMarker.captureName}`); } const savedState = this.matcher.saveState(); const success = this.matcher.handleVariadicCapture(captureMarker, capturedElements, capturedWrappers); if (!success) { this.matcher.restoreState(savedState); continue; } const restMatches = await this.matchSequenceOptimized( patternElements, targetElements, patternIdx + 1, targetIdx + consume, filterEmpty ); if (restMatches) { return true; } // Preserve explanation from this failed attempt before restoring state // This is especially important when using deterministic optimization (only one attempt) const currentExplanation = this.debug.getExplanation(); this.matcher.restoreState(savedState); // Restore the explanation if one was set during this attempt if (currentExplanation) { this.debug.restoreExplanation(currentExplanation); } } return false; } else { if (targetIdx >= targetElements.length) { return false; } const targetWrapper = targetElements[targetIdx]; const targetElement = targetWrapper.element; if (filterEmpty && targetElement.kind === J.Kind.Empty) { return false; } if (!await this.visitSequenceElement(patternWrapper, targetWrapper, targetIdx)) { return false; } return await this.matchSequenceOptimized( patternElements, targetElements, patternIdx + 1, targetIdx + 1, filterEmpty ); } } }