/* * 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} from '../..'; import {J} from '../../java'; import { Any, Capture, DebugLogEntry, DebugOptions, MatchAttemptResult, MatchExplanation, MatchOptions, MatchResult as IMatchResult, PatternOptions } from './types'; import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CaptureImpl, RAW_CODE_SYMBOL, RawCode} from './capture'; import {DebugPatternMatchingComparator, MatcherCallbacks, MatcherState, PatternMatchingComparator} from './comparator'; import {CaptureMarker, CaptureStorageValue, generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils'; import {TemplateEngine} from './engine'; import {TreePrinters} from '../../print'; import {JS} from '../index'; /** * Builder for creating patterns programmatically. * Use when pattern structure is not known at compile time. * * @example * // Loop-based pattern generation * const builder = Pattern.builder().code('myFunction('); * for (let i = 0; i < argCount; i++) { * if (i > 0) builder.code(', '); * builder.capture(capture(`arg${i}`)); * } * builder.code(')'); * const pat = builder.build(); * * @example * // Conditional pattern construction * const builder = Pattern.builder().code('foo('); * builder.capture(capture('first')); * if (needsSecondArg) { * builder.code(', ').capture(capture('second')); * } * builder.code(')'); * const pat = builder.build(); */ export class PatternBuilder { private parts: string[] = []; private captures: (Capture | Any | RawCode)[] = []; /** * Adds a static string part to the pattern. * * @param str The string to add * @returns This builder for chaining */ code(str: string): this { // If there are already captures, we need to add an empty string before this if (this.captures.length > this.parts.length) { this.parts.push(''); } // Append to the last part or start a new one if (this.parts.length === 0) { this.parts.push(str); } else { this.parts[this.parts.length - 1] += str; } return this; } /** * Adds a capture to the pattern. * * @param value The capture object (Capture, Any, or RawCode) or string name * @returns This builder for chaining */ capture(value: Capture | Any | RawCode | string): this { // Ensure we have a part for after this capture if (this.parts.length === 0) { this.parts.push(''); } // Convert string to Capture if needed, or use value as-is for RawCode const captureObj = typeof value === 'string' ? new CaptureImpl(value) : value; this.captures.push(captureObj as any); // Add an empty string for the next part this.parts.push(''); return this; } /** * Builds the pattern from accumulated parts and captures. * * @returns A Pattern instance */ build(): Pattern { // Ensure parts array is one longer than captures array while (this.parts.length <= this.captures.length) { this.parts.push(''); } // Create a synthetic TemplateStringsArray const templateStrings = this.parts.slice() as any; templateStrings.raw = this.parts.slice(); Object.defineProperty(templateStrings, 'raw', { value: this.parts.slice(), writable: false }); // Delegate to the pattern() function return pattern(templateStrings, ...this.captures); } } /** * Represents a pattern that can be matched against AST nodes. */ export class Pattern { private _options: PatternOptions = {}; private _cachedAstPattern?: J; private static nextPatternId = 1; private readonly patternId: number; private readonly unnamedCaptureMapping = new Map(); /** * Gets the configuration options for this pattern. * @readonly */ get options(): Readonly { return this._options; } /** * Creates a new builder for constructing patterns programmatically. * * @returns A new PatternBuilder instance * * @example * const pat = Pattern.builder() * .code('function ') * .capture(capture('name')) * .code('() { return ') * .capture(capture('value')) * .code('; }') * .build(); */ static builder(): PatternBuilder { return new PatternBuilder(); } /** * Creates a new pattern from template parts and captures. * * @param templateParts The string parts of the template * @param captures The captures between the string parts (can be Capture, Any, or RawCode) */ constructor( public readonly templateParts: TemplateStringsArray, public readonly captures: (Capture | Any | RawCode)[] ) { this.patternId = Pattern.nextPatternId++; // Build mapping for unnamed captures (unnamed_N -> _X) let unnamedIndex = 1; for (const cap of captures) { if (cap && typeof cap === 'object' && 'getName' in cap) { const name = (cap as Capture | Any).getName(); if (name && name.startsWith('unnamed_')) { this.unnamedCaptureMapping.set(name, `_${unnamedIndex}`); unnamedIndex++; } } } } /** * Configures this pattern with additional options. * * @param options Configuration options * @returns This pattern for method chaining * * @example * pattern`forwardRef((${props}, ${ref}) => ${body})` * .configure({ * context: ['import { forwardRef } from "react"'], * dependencies: {'@types/react': '^18.0.0'} * }) */ configure(options: PatternOptions): Pattern { this._options = { ...this._options, ...options }; // Invalidate cache when configuration changes this._cachedAstPattern = undefined; return this; } /** * Gets the AST pattern for this pattern, using two-level caching: * 1. Instance-level cache (fastest - this pattern instance) * 2. Global LRU cache (fast - shared across pattern instances with same code) * 3. Compute via TemplateProcessor (slow - parse and process) * * @returns The cached or newly computed pattern AST * @internal */ async getAstPattern(): Promise { // Level 1: Instance cache (fastest path) if (this._cachedAstPattern) { return this._cachedAstPattern; } // Generate cache key for global lookup // Include raw code values in the key since they affect the generated AST const contextStatements = this._options.context || this._options.imports || []; const capturesKey = this.captures.map(c => { if (c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) { return `raw:${(c as RawCode).code}`; } return c.getName(); }).join(','); const cacheKey = generateCacheKey( this.templateParts, capturesKey, contextStatements, this._options.dependencies || {} ); // Level 2: Global cache (fast path - shared with Template) const cached = globalAstCache.get(cacheKey); if (cached) { this._cachedAstPattern = cached; return cached; } // Level 3: Compute via TemplateEngine (slow path) const result = await TemplateEngine.getPatternTree( this.templateParts, this.captures, contextStatements, this._options.dependencies || {} ); // Cache in both levels globalAstCache.set(cacheKey, result); this._cachedAstPattern = result; return result; } /** * Creates a matcher for this pattern against a specific AST node. * * @param tree The AST node to match against * @param cursor Cursor at the node's position in a larger tree. Used for context-aware * capture constraints to navigate to parent nodes. * @param options Optional match options (e.g., debug flag) * @returns A MatchResult if the pattern matches, undefined otherwise * * @example * ```typescript * // Normal match * const match = await pattern.match(node, cursor); * * // Debug this specific call * const match = await pattern.match(node, cursor, { debug: true }); * ``` */ async match(tree: J, cursor: Cursor, options?: MatchOptions): Promise { // Three-level precedence: call > pattern > global const debugEnabled = options?.debug !== undefined ? options.debug // 1. Explicit call-level (true OR false) : (this._options.debug !== undefined ? this._options.debug // 2. Explicit pattern-level : process.env.PATTERN_DEBUG === 'true'); // 3. Global if (debugEnabled) { // Use matchWithExplanation and log the result const result = await this.matchWithExplanation(tree, cursor); await this.logMatchResult(tree, cursor, result); if (result.matched) { // result.result is the MatchResult class instance return result.result as MatchResult | undefined; } else { return undefined; } } // Fast path - no debug const matcher = new Matcher(this, tree, cursor); const success = await matcher.matches(); if (!success) { return undefined; } // Create MatchResult with unified storage const storage = (matcher as any).storage; return new MatchResult(new Map(storage)); } /** * Formats and logs the match result to stderr. * @private */ private async logMatchResult(tree: J, cursor: Cursor | undefined, result: MatchAttemptResult): Promise { const patternSource = this.getPatternSource(); const patternId = `Pattern #${this.patternId}`; const nodeKind = (tree as any).kind || 'unknown'; // Format kind: extract short name (e.g., "org.openrewrite.java.tree.J$Binary" -> "J$Binary") const shortKind = typeof nodeKind === 'string' ? nodeKind.split('.').pop() || nodeKind : nodeKind; // First, log the pattern source console.error(`[${patternId}] ${patternSource}`); // Build the complete match result message const lines: string[] = []; // Print the target tree being matched let treeStr: string; try { const printer = TreePrinters.printer(JS.Kind.CompilationUnit); treeStr = await printer.print(tree); } catch (e) { treeStr = '(tree printing unavailable)'; } if (result.matched) { // Success case - result first, then tree, then captures lines.push(`[${patternId}] ✅ SUCCESS matching against ${shortKind}:`); treeStr.split('\n').forEach(line => lines.push(`[${patternId}] ${line}`)); // Log captured values if (result.result) { const storage = (result.result as any).storage as Map; if (storage && storage.size > 0) { for (const [name, value] of storage) { const extractedValue = (result.result as any).extractElements(value); const valueStr = this.formatCapturedValue(extractedValue); const displayName = this.unnamedCaptureMapping.get(name) || name; lines.push(`[${patternId}] Captured '${displayName}': ${valueStr}`); } } } } else { // Failure case - result first, then tree, then explanation lines.push(`[${patternId}] ❌ FAILED matching against ${shortKind}:`); treeStr.split('\n').forEach(line => lines.push(`[${patternId}] ${line}`)); const explanation = result.explanation; if (explanation) { // Always show path, even if empty, to make it clear where the mismatch occurred const compactedPath = this.compactPath(explanation.path); const pathStr = compactedPath.length > 0 ? compactedPath.join(' → ') : ''; lines.push(`[${patternId}] At path: [${pathStr}]`); lines.push(`[${patternId}] Reason: ${explanation.reason}`); lines.push(`[${patternId}] Expected: ${explanation.expected}`); lines.push(`[${patternId}] Actual: ${explanation.actual}`); } } // Single console.error call with all lines joined console.error(lines.join('\n')); } /** * Compacts array index navigations into the previous path element. * For example: ['J$VariableDeclarations#variables', '0'] → ['J$VariableDeclarations#variables[0]'] * @private */ private compactPath(path: string[]): string[] { const compacted: string[] = []; let i = 0; while (i < path.length) { const current = path[i]; // Check if current element is itself a numeric index if (/^\d+$/.test(current)) { // This is a bare numeric index - shouldn't normally happen // If we have a previous element, append to it if (compacted.length > 0) { compacted[compacted.length - 1] += `[${current}]`; } else { // No previous element to attach to - this is an error in path construction // Skip it to avoid bare [0] in output console.warn(`Warning: Path starts with numeric index '${current}' - skipping`); } i++; continue; } // Look ahead to collect consecutive numeric indices let j = i + 1; const indices: string[] = []; while (j < path.length && /^\d+$/.test(path[j])) { indices.push(path[j]); j++; } // If we found numeric indices, append them to current element if (indices.length > 0) { compacted.push(current + indices.map(idx => `[${idx}]`).join('')); i = j; // Skip the indices we just processed } else { compacted.push(current); i++; } } return compacted; } /** * Gets the source code representation of this pattern for logging. * @private */ private getPatternSource(): string { // Reconstruct pattern source from template parts let source = ''; for (let i = 0; i < this.templateParts.length; i++) { source += this.templateParts[i]; if (i < this.captures.length) { const cap = this.captures[i]; // Skip raw code if (cap instanceof RawCode || (cap && typeof cap === 'object' && (cap as any)[RAW_CODE_SYMBOL])) { source += '${raw(...)}'; continue; } // Show capture name or placeholder const name = (cap as any)[CAPTURE_NAME_SYMBOL]; if (cap && typeof cap === 'object' && name) { // Use mapped name for unnamed captures, or original name const displayName = this.unnamedCaptureMapping.get(name) || name; source += `\${${displayName}}`; } else { source += '${...}'; } } } return source; } /** * Formats a captured value for logging. * @private */ private formatCapturedValue(value: any): string { if (value === null) return 'null'; if (value === undefined) return 'undefined'; // Check if it's an array (variadic capture) if (Array.isArray(value)) { if (value.length === 0) return '[]'; const items = value.slice(0, 3).map(v => this.formatSingleValue(v)); const suffix = value.length > 3 ? `, ... (${value.length} total)` : ''; return `[${items.join(', ')}${suffix}]`; } return this.formatSingleValue(value); } /** * Formats a single AST node for logging. * @private */ private formatSingleValue(value: any): string { if (!value || typeof value !== 'object') { return String(value); } const kind = (value as any).kind; if (!kind) return String(value); // Extract simple kind name (last segment) const kindStr = kind.split('.').pop(); // For literals, show the value if (kindStr === 'Literal' && value.value !== undefined) { const litValue = typeof value.value === 'string' ? `"${value.value}"` : String(value.value); return `${kindStr}(${litValue})`; } // For identifiers, show the name if (kindStr === 'Identifier' && value.simpleName) { return `${kindStr}(${value.simpleName})`; } // Default: just the kind return kindStr; } /** * Matches a pattern against an AST node with detailed debug information. * Part of Layer 2 (Public API). * * This method always enables debug logging and returns detailed information about * the match attempt, including: * - Whether the pattern matched * - Captured nodes (if matched) * - Explanation of failure (if not matched) * - Debug log entries showing the matching process * * @param tree The AST node to match against * @param cursor Cursor at the node's position in a larger tree * @param debugOptions Optional debug options (defaults to all logging enabled) * @returns Detailed result with debug information * * @example * const x = capture('x'); * const pat = pattern`console.log(${x})`; * const attempt = await pat.matchWithExplanation(node, cursor); * if (attempt.matched) { * console.log('Matched!'); * console.log('Captured x:', attempt.result.get('x')); * } else { * console.log('Failed:', attempt.explanation); * console.log('Debug log:', attempt.debugLog); * } */ async matchWithExplanation( tree: J, cursor: Cursor, debugOptions?: DebugOptions ): Promise { // Default to full debug logging if not specified const options: DebugOptions = { enabled: true, logComparison: true, logConstraints: true, ...debugOptions }; const matcher = new Matcher(this, tree, cursor, options); const success = await matcher.matches(); if (success) { // Match succeeded - return MatchResult with debug info const storage = (matcher as any).storage; const matchResult = new MatchResult(new Map(storage)); return { matched: true, result: matchResult, debugLog: matcher.getDebugLog() }; } else { // Match failed - return explanation return { matched: false, explanation: matcher.getExplanation(), debugLog: matcher.getDebugLog() }; } } } /** * Result of a successful pattern match containing captured values. * * Provides access to captured AST nodes from pattern matching operations. * Use the `get()` method to retrieve captured values by name or by Capture object. * * @example * const x = capture('x'); * const pat = pattern`foo(${x})`; * const match = await pat.match(someNode, cursor); * if (match) { * const captured = match.get('x'); // Get by name * // or * const captured = match.get(x); // Get by Capture object * } * * @example * // Variadic captures return arrays * const args = capture({ variadic: true }); * const pat = pattern`foo(${args})`; * const match = await pat.match(methodInvocation, cursor); * if (match) { * const capturedArgs = match.get(args); // Returns J[] for variadic captures * } */ export class MatchResult implements IMatchResult { constructor( private readonly storage: Map = new Map() ) { } // Overload: get with Capture returns value get(capture: Capture): T | undefined; // Overload: get with string returns value get(capture: string): any; // Implementation get(capture: Capture | string): J | J[] | undefined { // Use symbol to get internal name without triggering Proxy const name = typeof capture === "string" ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName()); const value = this.storage.get(name); if (value === undefined) { return undefined; } return this.extractElements(value); } /** * Checks if a capture has been matched. * * @param capture The capture name (string) or Capture object * @returns true if the capture exists in the match result */ has(capture: Capture | string): boolean { const name = typeof capture === "string" ? capture : ((capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName()); return this.storage.has(name); } /** * Extracts semantic elements from storage value. * For wrappers, extracts the .element; for arrays, returns array of elements. * * @param value The storage value * @returns The semantic element(s) */ private extractElements(value: CaptureStorageValue): J { if (Array.isArray(value)) { // Check if it's an array of wrappers if (value.length > 0 && (value[0] as any).element !== undefined) { // Array of J.RightPadded - extract elements return (value as J.RightPadded[]).map(w => w.element) as any; } // Already an array of elements return value as any; } // Check if it's a scalar wrapper if ((value as any).element !== undefined) { return (value as J.RightPadded).element; } // Scalar element return value as J; } /** * Internal method to get wrappers (used by template expansion). * Returns both scalar and variadic wrappers. * @internal */ [WRAPPERS_MAP_SYMBOL](): Map | J.RightPadded[]> { const result = new Map | J.RightPadded[]>(); for (const [name, value] of this.storage) { if (Array.isArray(value) && value.length > 0 && (value[0] as any).element !== undefined) { // This is an array of wrappers (variadic) result.set(name, value as J.RightPadded[]); } else if (!Array.isArray(value) && (value as any).element !== undefined) { // This is a scalar wrapper result.set(name, value as J.RightPadded); } } return result; } } /** * Matcher for checking if a pattern matches an AST node and extracting captured nodes. */ class Matcher { // Unified storage: holds J for scalar captures, J.RightPadded[] or J[] for variadic captures private readonly storage = new Map(); private patternAst?: J; // Debug tracking (Layer 1: Core Instrumentation) private readonly debugOptions: DebugOptions; private readonly debugLog: DebugLogEntry[] = []; private explanation?: MatchExplanation; private readonly currentPath: string[] = []; /** * Creates a new matcher for a pattern against an AST node. * * @param pattern The pattern to match * @param ast The AST node to match against * @param cursor Cursor at the AST node's position * @param debugOptions Optional debug options for instrumentation */ constructor( private readonly pattern: Pattern, private readonly ast: J, cursor: Cursor, debugOptions?: DebugOptions ) { this.cursor = cursor; this.debugOptions = debugOptions ?? {}; } private readonly cursor: Cursor; /** * Checks if the pattern matches the AST node. * * @returns true if the pattern matches, false otherwise */ async matches(): Promise { if (!this.patternAst) { this.patternAst = await this.pattern.getAstPattern(); } return this.matchNode(this.patternAst, this.ast); } /** * Gets all captured nodes (projected view: extracts elements from wrappers). * * @returns A map of capture names to captured nodes */ getAll(): Map { const result = new Map(); for (const [name, value] of this.storage) { result.set(name, this.extractElements(value)); } return result; } /** * Extracts semantic elements from storage value. * For wrappers, extracts the .element; for arrays, returns array of elements. * * @param value The storage value * @returns The semantic element(s) */ private extractElements(value: CaptureStorageValue): J { if (Array.isArray(value)) { // Check if it's an array of wrappers if (value.length > 0 && (value[0] as any).element !== undefined) { // Array of J.RightPadded - extract elements return (value as J.RightPadded[]).map(w => w.element) as any; } // Already an array of elements return value as any; } // Check if it's a scalar wrapper if ((value as any).element !== undefined) { return (value as J.RightPadded).element; } // Scalar element return value as J; } /** * Logs a debug message if debugging is enabled. * Part of Layer 1 (Core Instrumentation). * * @param level The severity level * @param scope The scope/category * @param message The message to log * @param data Optional data to include */ private log( level: DebugLogEntry['level'], scope: DebugLogEntry['scope'], message: string, data?: any ): void { if (!this.debugOptions.enabled) return; // Filter by scope if specific logging is requested if (scope === 'comparison' && !this.debugOptions.logComparison) return; if (scope === 'constraint' && !this.debugOptions.logConstraints) return; this.debugLog.push({ level, scope, path: [...this.currentPath], message, data }); } /** * Sets the explanation for why the pattern match failed. * Only sets the first failure (most relevant). * Part of Layer 1 (Core Instrumentation). * * @param reason The reason for failure * @param expected Human-readable description of what was expected * @param actual Human-readable description of what was found * @param details Optional additional context */ private setExplanation( reason: MatchExplanation['reason'], expected: string, actual: string, details?: string ): void { // Only set the first failure (most relevant) if (this.explanation) return; this.explanation = { reason, path: [...this.currentPath], expected, actual, details }; } /** * Pushes a path component onto the current path. * Used to track where in the AST tree we are during matching. * Part of Layer 1 (Core Instrumentation). * * @param name The path component to push */ private pushPath(name: string): void { this.currentPath.push(name); } /** * Pops the last path component from the current path. * Part of Layer 1 (Core Instrumentation). */ private popPath(): void { this.currentPath.pop(); } /** * Matches a pattern node against a target node. * * @param pattern The pattern node * @param target The target node * @returns true if the pattern matches the target, false otherwise */ private async matchNode(pattern: J, target: J): Promise { // Always delegate to the comparator visitor, which handles: // - Capture detection and constraint evaluation // - Kind checking // - Deep structural comparison // This centralizes all matching logic in one place const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true; // Factory pattern: instantiate debug or production comparator // Zero cost in production - DebugPatternMatchingComparator is never instantiated const matcherCallbacks: MatcherCallbacks = { handleCapture: (capture: CaptureMarker, t: J, w?: J.RightPadded) => this.handleCapture(capture, t, w), handleVariadicCapture: (capture: CaptureMarker, ts: J[], ws?: J.RightPadded[]) => this.handleVariadicCapture(capture, ts, ws), saveState: () => this.saveState(), restoreState: (state) => this.restoreState(state), // Debug callbacks (Layer 1) - grouped together, always present or absent debug: this.debugOptions.enabled ? { log: (level: DebugLogEntry['level'], scope: DebugLogEntry['scope'], message: string, data?: any) => this.log(level, scope, message, data), setExplanation: (reason: MatchExplanation['reason'], expected: string, actual: string, details?: string) => this.setExplanation(reason, expected, actual, details), getExplanation: () => this.explanation, restoreExplanation: (explanation: MatchExplanation) => { this.explanation = explanation; }, clearExplanation: () => { this.explanation = undefined; }, pushPath: (name: string) => this.pushPath(name), popPath: () => this.popPath() } : undefined }; const comparator = this.debugOptions.enabled ? new DebugPatternMatchingComparator(matcherCallbacks, lenientTypeMatching) : new PatternMatchingComparator(matcherCallbacks, lenientTypeMatching); // Pass cursors to allow constraints to navigate to root // Pattern cursor is undefined (pattern is the root), target cursor is provided by user const result = await comparator.compare(pattern, target, undefined, this.cursor); // If match failed and no explanation was set, provide a generic one if (!result && this.debugOptions.enabled && !this.explanation) { const patternKind = (pattern as any).kind?.split('.').pop() || 'unknown'; const targetKind = (target as any).kind?.split('.').pop() || 'unknown'; this.setExplanation( 'structural-mismatch', `Pattern node of type ${patternKind}`, `Target node of type ${targetKind}`, 'Nodes did not match structurally' ); } return result; } /** * Saves the current state for backtracking. * Includes both capture storage AND debug state (explanation, log, path). * * @returns A snapshot of the current state */ private saveState(): MatcherState { return { storage: new Map(this.storage), debugState: this.debugOptions.enabled ? { explanation: this.explanation, logLength: this.debugLog.length, path: [...this.currentPath] } : undefined }; } /** * Restores a previously saved state for backtracking. * Restores both capture storage AND debug state. * * @param state The state to restore */ private restoreState(state: MatcherState): void { // Restore capture storage this.storage.clear(); state.storage.forEach((value, key) => this.storage.set(key, value)); // Restore debug state if it was saved if (state.debugState) { // Restore explanation to the saved state // This clears any explanations set during failed exploratory attempts (like pivot detection) this.explanation = state.debugState.explanation; // Truncate debug log to saved length (remove entries added during failed attempt) this.debugLog.length = state.debugState.logLength; // Restore path this.currentPath.length = 0; this.currentPath.push(...state.debugState.path); } } /** * Handles a capture placeholder. * * @param capture The pattern node capture * @param target The target node * @param wrapper Optional wrapper containing the target (for preserving markers) * @returns true if the capture is successful, false otherwise */ private handleCapture(capture: CaptureMarker, target: J, wrapper?: J.RightPadded): boolean { const captureName = capture.captureName; if (!captureName) { return false; } // Find the original capture object to get capturing flag // Note: Constraints are now evaluated in PatternMatchingComparator where cursor is correctly positioned // Filter out RawCode since it doesn't have getName() const captureObj = this.pattern.captures.find(c => !(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) && c.getName() === captureName ); // Only store the binding if this is a capturing placeholder const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true; if (capturing) { // Store wrapper if available (preserves markers), otherwise store element this.storage.set(captureName, wrapper ?? target); } return true; } /** * Handles a variadic capture placeholder. * * @param capture The pattern node capture (the variadic capture) * @param targets The target nodes that were matched * @param wrappers Optional wrappers to preserve markers * @returns true if the capture is successful, false otherwise */ private handleVariadicCapture(capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded[]): boolean { const captureName = capture.captureName; if (!captureName) { return false; } // Find the original capture object to get capturing flag // Note: Constraints are now evaluated in PatternMatchingComparator where cursor is correctly positioned // Filter out RawCode since it doesn't have getName() const captureObj = this.pattern.captures.find(c => !(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL])) && c.getName() === captureName ); // Only store the binding if this is a capturing placeholder const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true; if (capturing) { // Store the richest representation: wrappers if available, otherwise elements if (wrappers && wrappers.length > 0) { this.storage.set(captureName, wrappers); } else { this.storage.set(captureName, targets); } } return true; } /** * Gets the debug log entries collected during matching. * Part of Layer 2 (Public API). * * @returns The debug log entries, or undefined if debug wasn't enabled */ getDebugLog(): DebugLogEntry[] | undefined { return this.debugOptions.enabled ? [...this.debugLog] : undefined; } /** * Gets the explanation for why the match failed. * Part of Layer 2 (Public API). * * @returns The match explanation, or undefined if match succeeded or no explanation available */ getExplanation(): MatchExplanation | undefined { return this.explanation; } } /** * Tagged template function for creating patterns. * * @param strings The string parts of the template * @param captures The captures between the string parts (Capture, Any, RawCode, or string names) * @returns A Pattern object * * @example * // Using the same capture multiple times for repeated patterns * const expr = capture('expr'); * const redundantOr = pattern`${expr} || ${expr}`; * * @example * // Using any() for non-capturing matches * const pat = pattern`foo(${any()})`; * * @example * // Using raw() for dynamic pattern construction * const operator = '==='; * const pat = pattern`x ${raw(operator)} y`; */ /** * Creates a pattern from a template literal (direct usage). * * @example * ```typescript * const pat = pattern`console.log(${x})`; * ``` */ export function pattern(strings: TemplateStringsArray, ...captures: (Capture | Any | RawCode | string)[]): Pattern; /** * Creates a pattern factory with options that returns a tagged template function. * * @example * ```typescript * const pat = pattern({ debug: true })`console.log(${x})`; * ``` */ export function pattern(options: PatternOptions): (strings: TemplateStringsArray, ...captures: (Capture | Any | RawCode | string)[]) => Pattern; // Implementation export function pattern( stringsOrOptions: TemplateStringsArray | PatternOptions, ...captures: (Capture | Any | RawCode | string)[] ): Pattern | ((strings: TemplateStringsArray, ...captures: (Capture | Any | RawCode | string)[]) => Pattern) { // Check if first arg is TemplateStringsArray (direct usage) if (Array.isArray(stringsOrOptions) && 'raw' in stringsOrOptions) { // Direct usage: pattern`...` return createPattern(stringsOrOptions as TemplateStringsArray, captures, {}); } // Options usage: pattern({ ... })`...` const options = stringsOrOptions as PatternOptions; return (strings: TemplateStringsArray, ...caps: (Capture | Any | RawCode | string)[]): Pattern => { return createPattern(strings, caps, options); }; } /** * Internal helper to create a Pattern instance. * @private */ function createPattern( strings: TemplateStringsArray, captures: (Capture | Any | RawCode | string)[], options: PatternOptions ): Pattern { const capturesByName = captures.reduce((map, c) => { // Skip raw code - it's not a capture if (c instanceof RawCode || (typeof c === 'object' && c && (c as any)[RAW_CODE_SYMBOL])) { return map; } const capture = typeof c === "string" ? new CaptureImpl(c) : c; // Use symbol to get internal name without triggering Proxy const name = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName(); return map.set(name, capture); }, new Map>()); const pat = new Pattern(strings, captures.map(c => { // Return raw code as-is if (c instanceof RawCode || (typeof c === 'object' && c && (c as any)[RAW_CODE_SYMBOL])) { return c as RawCode; } // Use symbol to get internal name without triggering Proxy const name = typeof c === "string" ? c : ((c as any)[CAPTURE_NAME_SYMBOL] || c.getName()); return capturesByName.get(name)!; })); // Apply options if provided if (options && Object.keys(options).length > 0) { pat.configure(options); } return pat; }