/* * 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, Type} from '../../java'; import {Any, Capture, CaptureConstraintContext, CaptureOptions, ConstraintFunction, TemplateParam, VariadicOptions} from './types'; /** * Combines multiple constraints with AND logic. * All constraints must return true for the combined constraint to pass. * * @example * const largeEvenNumber = capture('n', { * constraint: and( * (node) => typeof node.value === 'number', * (node) => node.value > 100, * (node) => node.value % 2 === 0 * ) * }); */ export function and(...constraints: ConstraintFunction[]): ConstraintFunction { return (node: T, context: CaptureConstraintContext) => constraints.every(c => c(node, context)); } /** * Combines multiple constraints with OR logic. * At least one constraint must return true for the combined constraint to pass. * * @example * const stringOrNumber = capture('value', { * constraint: or( * (node) => node.kind === J.Kind.Literal && typeof node.value === 'string', * (node) => node.kind === J.Kind.Literal && typeof node.value === 'number' * ) * }); */ export function or(...constraints: ConstraintFunction[]): ConstraintFunction { return (node: T, context: CaptureConstraintContext) => constraints.some(c => c(node, context)); } /** * Negates a constraint. * Returns true when the constraint returns false, and vice versa. * * @example * const notString = capture('value', { * constraint: not((node) => typeof node.value === 'string') * }); */ export function not(constraint: ConstraintFunction): ConstraintFunction { return (node: T, context: CaptureConstraintContext) => !constraint(node, context); } // Symbol to access the internal capture name without triggering Proxy export const CAPTURE_NAME_SYMBOL = Symbol('captureName'); // Symbol to access variadic options without triggering Proxy export const CAPTURE_VARIADIC_SYMBOL = Symbol('captureVariadic'); // Symbol to access constraint function without triggering Proxy export const CAPTURE_CONSTRAINT_SYMBOL = Symbol('captureConstraint'); // Symbol to access capturing flag without triggering Proxy export const CAPTURE_CAPTURING_SYMBOL = Symbol('captureCapturing'); // Symbol to access type information without triggering Proxy export const CAPTURE_TYPE_SYMBOL = Symbol('captureType'); // Symbol to identify RawCode instances export const RAW_CODE_SYMBOL = Symbol('rawCode'); export class CaptureImpl implements Capture { public readonly name: string; [CAPTURE_NAME_SYMBOL]: string; [CAPTURE_VARIADIC_SYMBOL]: VariadicOptions | undefined; [CAPTURE_CONSTRAINT_SYMBOL]: ConstraintFunction | undefined; [CAPTURE_CAPTURING_SYMBOL]: boolean; [CAPTURE_TYPE_SYMBOL]: string | Type | undefined; constructor(name: string, options?: CaptureOptions, capturing: boolean = true) { this.name = name; this[CAPTURE_NAME_SYMBOL] = name; this[CAPTURE_CAPTURING_SYMBOL] = capturing; // Normalize variadic options if (options?.variadic) { if (typeof options.variadic === 'boolean') { this[CAPTURE_VARIADIC_SYMBOL] = {}; } else { this[CAPTURE_VARIADIC_SYMBOL] = { min: options.variadic.min, max: options.variadic.max }; } } // Store constraint if provided if (options?.constraint) { this[CAPTURE_CONSTRAINT_SYMBOL] = options.constraint; } // Store type if provided if (options?.type) { this[CAPTURE_TYPE_SYMBOL] = options.type; } } getName(): string { return this[CAPTURE_NAME_SYMBOL]; } isVariadic(): boolean { return this[CAPTURE_VARIADIC_SYMBOL] !== undefined; } getVariadicOptions(): VariadicOptions | undefined { return this[CAPTURE_VARIADIC_SYMBOL]; } getConstraint(): ConstraintFunction | undefined { return this[CAPTURE_CONSTRAINT_SYMBOL]; } isCapturing(): boolean { return this[CAPTURE_CAPTURING_SYMBOL]; } getType(): string | Type | undefined { return this[CAPTURE_TYPE_SYMBOL]; } } export class TemplateParamImpl implements TemplateParam { public readonly name: string; constructor(name: string) { this.name = name; } getName(): string { return this.name; } } /** * Represents a property access on a captured value. * When you access a property on a Capture (e.g., method.name), you get a CaptureValue * that knows how to resolve that property from the matched values. */ export class CaptureValue { constructor( public readonly rootCapture: Capture, public readonly propertyPath: string[], public readonly arrayOperation?: { type: 'index' | 'slice' | 'length'; args?: number[] } ) {} /** * Resolves this capture value by looking up the root capture in the values map * and navigating through the property path. */ resolve(values: Pick, 'get'>): any { const rootName = this.rootCapture.getName(); let current: any = values.get(rootName); // Handle array operations on variadic captures if (this.arrayOperation && Array.isArray(current)) { switch (this.arrayOperation.type) { case 'index': current = current[this.arrayOperation.args![0]]; break; case 'slice': current = current.slice(...this.arrayOperation.args!); break; case 'length': current = current.length; break; } } // Navigate through property path for (const prop of this.propertyPath) { if (current === null || current === undefined || typeof current === 'number') { return undefined; } current = current[prop]; } return current; } /** * Checks if this CaptureValue will resolve to an array that should be expanded. */ isArrayExpansion(): boolean { // Only slice operations and the root variadic capture return arrays return this.arrayOperation?.type === 'slice' || (this.arrayOperation === undefined && this.propertyPath.length === 0 && (this.rootCapture as any).isVariadic?.()); } } /** * Creates a Proxy around a CaptureValue that allows further property access. * This enables chaining like method.name.simpleName */ function createCaptureValueProxy( rootCapture: Capture, propertyPath: string[], arrayOperation?: { type: 'index' | 'slice' | 'length'; args?: number[] } ): any { const captureValue = new CaptureValue(rootCapture, propertyPath, arrayOperation); return new Proxy(captureValue, { get(target: CaptureValue, prop: string | symbol): any { // Allow access to the CaptureValue instance itself and its methods if (prop === 'resolve' || prop === 'rootCapture' || prop === 'propertyPath' || prop === 'arrayOperation' || prop === 'isArrayExpansion') { const value = target[prop as keyof CaptureValue]; return typeof value === 'function' ? value.bind(target) : value; } // For string property access, extend the property path if (typeof prop === 'string') { return createCaptureValueProxy(target.rootCapture, [...target.propertyPath, prop], target.arrayOperation); } return undefined; } }); } /** * Creates a capture specification for use in template patterns. * * @template T The expected type of the captured AST node (for TypeScript autocomplete only) * @returns A Capture object that supports property access for use in templates * * @remarks * **Pattern Matching is Structural:** * * What a capture matches is determined by the pattern structure, not the type parameter: * - `pattern`${capture('x')}`` matches ANY single expression (identifier, literal, call, binary, etc.) * - `pattern`foo(${capture('x')})`` matches only expressions inside `foo()` calls * - `pattern`${capture('x')} + ${capture('y')}`` matches only binary `+` expressions * * The TypeScript type parameter `` provides IDE autocomplete but doesn't enforce runtime types. * * @example * // Named inline captures * const pat = pattern`${capture('left')} + ${capture('right')}`; * // Matches: + * * // Unnamed captures * const {left, right} = {left: capture(), right: capture()}; * const pattern = pattern`${left} + ${right}`; * * @example * // Type parameter is for IDE autocomplete only * const method = capture('method'); * const pat = pattern`foo(${method})`; * // Matches: foo() - not restricted to method invocations! * // Type only helps with autocomplete in your code * * @example * // Structural pattern determines what matches * const arg = capture('arg'); * const pat = pattern`process(${arg})`; * // Matches: process(42), process("text"), process(x + y), etc. * // The 'arg' capture will bind to whatever expression is passed to process() * * @example * // Repeated patterns using the same capture * const expr = capture('expr'); * const redundantOr = pattern`${expr} || ${expr}`; * // Matches expressions where both sides are identical: x || x, foo() || foo() * * @example * // Property access in templates * const method = capture('method'); * template`console.log(${method.name.simpleName})` // Accesses properties of captured node */ /** * Creates a Proxy wrapper for CaptureImpl that intercepts property accesses. * Shared logic between capture() and any() to avoid duplication. */ function createCaptureProxy(impl: CaptureImpl): any { return new Proxy(impl as any, { get(target: any, prop: string | symbol): any { // Allow access to internal symbols if (prop === CAPTURE_NAME_SYMBOL) { return target[CAPTURE_NAME_SYMBOL]; } if (prop === CAPTURE_VARIADIC_SYMBOL) { return target[CAPTURE_VARIADIC_SYMBOL]; } if (prop === CAPTURE_CONSTRAINT_SYMBOL) { return target[CAPTURE_CONSTRAINT_SYMBOL]; } if (prop === CAPTURE_CAPTURING_SYMBOL) { return target[CAPTURE_CAPTURING_SYMBOL]; } if (prop === CAPTURE_TYPE_SYMBOL) { return target[CAPTURE_TYPE_SYMBOL]; } // Support using Capture as object key via computed properties {[x]: value} if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') { return () => target[CAPTURE_NAME_SYMBOL]; } // Allow methods to be called directly on the target if (prop === 'getName' || prop === 'isVariadic' || prop === 'getVariadicOptions' || prop === 'getConstraint' || prop === 'isCapturing' || prop === 'getType') { return target[prop].bind(target); } // For variadic captures, support array-like operations if (target.isVariadic() && typeof prop === 'string') { // Numeric index access: args[0], args[1], etc. const indexNum = Number(prop); if (!isNaN(indexNum) && indexNum >= 0 && Number.isInteger(indexNum)) { return createCaptureValueProxy(target, [], { type: 'index', args: [indexNum] }); } // Array method: slice if (prop === 'slice') { return (...args: number[]) => { return createCaptureValueProxy(target, [], { type: 'slice', args }); }; } // Array property: length if (prop === 'length') { return createCaptureValueProxy(target, [], { type: 'length' }); } } // For string property access, create a CaptureValue with a property path if (typeof prop === 'string') { return createCaptureValueProxy(target, [prop]); } return undefined; } }); } // Overload 1: Options object with constraint (no variadic) export function capture( options: CaptureOptions & { variadic?: never } ): Capture & T; // Overload 2: Options object with variadic export function capture( options: { name?: string; variadic: true | VariadicOptions; constraint?: ConstraintFunction; min?: number; max?: number } ): Capture & T[]; // Overload 3: Just a string name (simple named capture) export function capture(name?: string): Capture & T; // Implementation export function capture(nameOrOptions?: string | CaptureOptions): Capture & T { let name: string | undefined; let options: CaptureOptions | undefined; if (typeof nameOrOptions === 'string') { // Simple named capture: capture('name') name = nameOrOptions; options = undefined; } else { // Options-based API: capture({ name: 'name', ...options }) or capture() options = nameOrOptions; name = options?.name; } const captureName = name || `unnamed_${capture.nextUnnamedId++}`; const impl = new CaptureImpl(captureName, options); return createCaptureProxy(impl); } // Static counter for generating unique IDs for unnamed captures capture.nextUnnamedId = 1; /** * Creates a non-capturing pattern match for use in patterns. * * Use `any()` when you need to match AST structure without binding the matched value to a name. * This is useful for validation patterns where you care about structure but not the specific values. * * **Key Differences from `capture()`:** * - `any()` returns `Any` type (not `Capture`) * - Cannot be used in templates (TypeScript compiler prevents this) * - Does not bind matched values (more memory efficient for patterns) * - Supports same features: constraints, variadic matching * * @template T The expected type of the matched AST node (for TypeScript autocomplete and constraints) * @param options Optional configuration (variadic, constraint) * @returns An Any object that matches patterns without capturing * * @example * // Match any single argument without capturing * const pat = pattern`foo(${any()})`; * * @example * // Match with constraint validation * const numericArg = any({ * constraint: (node) => typeof node.value === 'number' * }); * const pat = pattern`process(${numericArg})`; * * @example * // Variadic any - match zero or more without capturing * const rest = any({ variadic: true }); * const first = capture('first'); * const pat = pattern`foo(${first}, ${rest})`; * * @example * // Mixed with captures - capture some, ignore others * const important = capture('important'); * const pat = pattern` * if (${any()}) { * ${important} * } * `; * * @example * // Variadic with constraints * const numericArgs = any({ * variadic: true, * constraint: (nodes) => nodes.every(n => typeof n.value === 'number') * }); * const pat = pattern`sum(${numericArgs})`; */ // Overload 1: Regular any with constraint (most specific - no variadic property) export function any( options: { constraint: ConstraintFunction } & { variadic?: never } ): Any & T; // Overload 2: Variadic any with constraint export function any( options: { variadic: true | VariadicOptions; constraint?: ConstraintFunction; min?: number; max?: number } ): Any & T[]; // Overload 3: Catch-all for simple any without special options export function any( options?: CaptureOptions ): Any & T; // Implementation export function any(options?: CaptureOptions): Any & T { const anonName = `anon_${any.nextAnonId++}`; const impl = new CaptureImpl(anonName, options, false); // capturing = false return createCaptureProxy(impl); } // Static counter for generating unique IDs for anonymous (non-capturing) patterns any.nextAnonId = 1; /** * Creates a parameter specification for use in standalone templates (not used with patterns). * * Use `param()` when creating templates that are not used with pattern matching. * Use `capture()` when the template works with a pattern. * * @template T The expected type of the parameter value (for TypeScript autocomplete only) * @param name Optional name for the parameter. If not provided, an auto-generated name is used. * @returns A TemplateParam object (simpler than Capture, no property access support) * * @remarks * **When to use `param()` vs `capture()`:** * * - Use `param()` in **standalone templates** (no pattern matching involved) * - Use `capture()` in **patterns** and templates used with patterns * * **Key Differences:** * - `TemplateParam` is simpler - no property access proxy overhead * - `Capture` supports property access (e.g., `capture('x').name.simpleName`) * - Both work in templates, but `param()` makes intent clearer for standalone use * * @example * // ✅ GOOD: Use param() for standalone templates * const value = param('value'); * const tmpl = template`return ${value} * 2;`; * await tmpl.apply(cursor, node, new Map([['value', someLiteral]])); * * @example * // ✅ GOOD: Use capture() with patterns * const value = capture('value'); * const pat = pattern`foo(${value})`; * const tmpl = template`bar(${value})`; // capture() makes sense here * * @example * // ⚠️ CONFUSING: Using capture() in standalone template * const value = capture('value'); * template`return ${value} * 2;`; // "Capturing" what? There's no pattern! * * @example * // ❌ WRONG: param() doesn't support property access * const node = param('invocation'); * template`console.log(${node.name})` // Error! Use capture() for property access */ export function param(name?: string): TemplateParam { const paramName = name || `unnamed_${capture.nextUnnamedId++}`; return new TemplateParamImpl(paramName); } /** * Represents raw code that should be inserted verbatim into templates at construction time. * This is useful for dynamic code generation where the code structure is determined at runtime. */ export class RawCode { [RAW_CODE_SYMBOL] = true; constructor(public readonly code: string) {} } /** * Creates a raw code specification for inserting literal code strings into templates. * * Use `raw()` when you need to insert code that is generated dynamically (e.g., from recipe options, * computed field names, or programmatic string manipulation) directly into a template at construction time. * * The string is spliced into the template before parsing, so it becomes part of the template's AST. * This is different from `param()` or `capture()` which are placeholders replaced during application. * * @param code The code string to insert verbatim into the template * @returns A RawCode object that will be spliced into the template * * @remarks * **When to use `raw()` vs `param()` vs `capture()`:** * * - Use `raw()` when you have a **code string** to insert at **template construction time** * - Use `param()` when you have an **AST node** to substitute at **template application time** * - Use `capture()` when working with **pattern matching** and need to reference matched values * * **Safety Considerations:** * - No validation is performed on the code string * - The code must be syntactically valid at the position where it's inserted * - Recipe authors are trusted to provide valid code * * @example * // Recipe option determines the log level * class MyRecipe extends Recipe { * @Option * logLevel: string = "info"; * * getVisitor() { * // Template constructed with dynamic method name * const replacement = template`logger.${raw(this.logLevel)}(${_('msg')})`; * // Produces: logger.info(...) or logger.warn(...) etc. * } * } * * @example * // Build object literal from collected field names * const fields = ["userId", "timestamp", "status"]; * template`{ ${raw(fields.join(', '))} }` * // Produces: { userId, timestamp, status } * * @example * // Dynamic import path * const modulePath = "./utils"; * template`import { helper } from ${raw(`'${modulePath}'`)}` * // Produces: import { helper } from './utils' * * @example * // Configurable operator * const operator = ">="; * template`${_('value')} ${raw(operator)} threshold` * // Produces: value >= threshold */ export function raw(code: string): RawCode { return new RawCode(code); } /** * Concise alias for `capture`. Works well for inline captures in patterns and templates. * * @param name Optional name for the capture. If not provided, an auto-generated name is used. * @returns A Capture object * * @example * // Inline captures with _ alias * pattern`isDate(${_('dateArg')})` * template`${_('dateArg')} instanceof Date` */ export const _ = capture;