/*
* 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, NameTree} from '../../java';
import {JS} from '../index';
import {JavaScriptVisitor} from '../visitor';
import {Marker, Markers} from '../../markers';
import {randomId} from '../../uuid';
import {ConstraintFunction, VariadicOptions} from './types';
/**
* Internal storage value type for pattern match captures.
* - J: Scalar captures without wrapper (fallback)
* - J.RightPadded: Scalar captures with wrapper (preserves trailing markers like semicolons)
* - J[]: Variadic captures without wrapper metadata
* - J.RightPadded[]: Variadic captures with wrapper metadata (preserves markers like commas)
*/
export type CaptureStorageValue = J | J.RightPadded | J[] | J.RightPadded[];
/**
* Symbol to access wrappersMap without exposing it as public API
*/
export const WRAPPERS_MAP_SYMBOL = Symbol('wrappersMap');
/**
* Shared wrapper function name used by both patterns and templates.
* Using the same name allows cache sharing when pattern and template code is identical.
*/
export const WRAPPER_FUNCTION_NAME = '__WRAPPER__';
/**
* Simple LRU (Least Recently Used) cache implementation using Map's insertion order.
* JavaScript Map maintains insertion order, so the first entry is the oldest.
*
* Used by both Pattern and Template caching to provide bounded memory usage.
*/
export class LRUCache {
private cache = new Map();
constructor(private maxSize: number) {
}
get(key: K): V | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
// Move to end (most recently used)
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
set(key: K, value: V): void {
// Remove if exists (to update position)
this.cache.delete(key);
// Add to end
this.cache.set(key, value);
// Evict oldest if over capacity
if (this.cache.size > this.maxSize) {
const iterator = this.cache.keys();
const firstEntry = iterator.next();
if (!firstEntry.done) {
this.cache.delete(firstEntry.value);
}
}
}
clear(): void {
this.cache.clear();
}
}
/**
* Shared global LRU cache for both pattern and template ASTs.
* When pattern and template code is identical, they share the same cached AST.
* This mirrors JavaTemplate's unified approach in the Java implementation.
* Bounded to 100 entries using LRU eviction.
*/
export const globalAstCache = new LRUCache(100);
/**
* Generates a cache key for template/pattern processing.
* Used by both Pattern and Template for consistent cache key generation.
*
* @param templateParts The template string parts
* @param itemsKey String representing the captures/parameters (comma-separated)
* @param contextStatements Context declarations
* @param dependencies NPM dependencies
* @returns A cache key string
*/
export function generateCacheKey(
templateParts: string[] | TemplateStringsArray,
itemsKey: string,
contextStatements: string[],
dependencies: Record
): string {
return [
Array.from(templateParts).join('|'),
itemsKey,
contextStatements.join(';'),
JSON.stringify(dependencies)
].join('::');
}
abstract class IdVisitor extends JavaScriptVisitor {
// `JavaVisitor.visitTypeName` is a no-op stub, which leaves annotation names unreachable
protected override async visitTypeName(nameTree: N, p: any): Promise {
return (await this.visit(nameTree, p)) as N;
}
}
class RandomizeIdVisitor extends IdVisitor {
readonly minted = new Set();
protected override async postVisit(tree: J, p: any): Promise {
const id = randomId();
this.minted.add(id);
return {...tree, id};
}
}
class CollectIdVisitor extends IdVisitor {
readonly ids = new Set();
protected override async postVisit(tree: J, p: any): Promise {
this.ids.add(tree.id);
return tree;
}
}
class RetainIdVisitor extends IdVisitor {
private readonly seen = new Set();
constructor(private readonly retainable: ReadonlySet) {
super();
}
protected override async postVisit(tree: J, p: any): Promise {
if (this.retainable.has(tree.id) && !this.seen.has(tree.id)) {
this.seen.add(tree.id);
return tree;
}
const id = randomId();
this.seen.add(id);
return {...tree, id};
}
}
/**
* Structural copy of `tree` with a fresh `id` on every `Tree` node, and the ids that were minted;
* counterpart of Java's `RandomizeIdVisitor`.
*/
export async function randomizeIds(tree: T): Promise<{ tree: T, ids: ReadonlySet }> {
const visitor = new RandomizeIdVisitor();
return {tree: (await visitor.visit(tree, null)) as T, ids: visitor.minted};
}
/** The `id` of every `Tree` node in `tree`. */
export async function treeIds(tree: J): Promise> {
const visitor = new CollectIdVisitor();
await visitor.visit(tree, null);
return visitor.ids;
}
/** Copy of `tree` in which an `id` survives only where it is `retainable` and unused so far. */
export function retainIds(tree: T, retainable: ReadonlySet): Promise {
return new RetainIdVisitor(retainable).visit(tree, null) as Promise;
}
/**
* Marker that stores capture metadata on pattern AST nodes.
* This avoids the need to parse capture names from identifiers during matching.
*/
export class CaptureMarker implements Marker {
readonly kind = 'org.openrewrite.javascript.CaptureMarker';
readonly id = randomId();
constructor(
public readonly captureName: string,
public readonly variadicOptions?: VariadicOptions,
public readonly constraint?: ConstraintFunction
) {
}
}
/**
* Utility class for managing placeholder naming and parsing.
* Centralizes all logic related to capture placeholders.
*/
export class PlaceholderUtils {
static readonly CAPTURE_PREFIX = '__capt_';
static readonly PLACEHOLDER_PREFIX = '__PLACEHOLDER_';
/**
* Checks if a node is a capture placeholder.
*
* @param node The node to check
* @returns true if the node is a capture placeholder, false otherwise
*/
static isCapture(node: J): boolean {
// Check for CaptureMarker first (efficient)
for (const marker of node.markers.markers) {
if (marker instanceof CaptureMarker) {
return true;
}
}
return false;
}
/**
* Gets the CaptureMarker from a node, if present.
*
* @param node The node to check
* @returns The CaptureMarker or undefined
*/
static getCaptureMarker(node: { markers: Markers }): CaptureMarker | undefined {
for (const marker of node.markers.markers) {
if (marker instanceof CaptureMarker) {
return marker;
}
}
return undefined;
}
/**
* Parses a capture placeholder to extract name and type constraint.
*
* @param identifier The identifier string to parse
* @returns Object with name and optional type constraint, or null if not a valid capture
*/
static parseCapture(identifier: string): { name: string; typeConstraint?: string } | null {
if (!identifier.startsWith(this.CAPTURE_PREFIX)) {
return null;
}
// Handle unnamed captures: "__capt_unnamed_N__"
if (identifier.startsWith(`${this.CAPTURE_PREFIX}unnamed_`)) {
const match = identifier.match(/__capt_(unnamed_\d+)__/);
return match ? {name: match[1]} : null;
}
// Handle all other captures (including any()): "__capt_name__" or "__capt_name_type__"
const match = identifier.match(/__capt_([^_]+(?:_\d+)?)(?:_([^_]+))?__/);
if (!match) {
return null;
}
return {
name: match[1],
typeConstraint: match[2]
};
}
/**
* Creates a capture placeholder string.
*
* @param name The capture name
* @param typeConstraint Optional type constraint
* @returns The formatted placeholder string
*/
static createCapture(name: string, typeConstraint?: string): string {
// Always use CAPTURE_PREFIX - the capturing flag is used internally for binding behavior
return typeConstraint
? `${this.CAPTURE_PREFIX}${name}_${typeConstraint}__`
: `${this.CAPTURE_PREFIX}${name}__`;
}
/**
* Checks if a capture marker indicates a variadic capture.
*
* @param node The node to check
* @returns true if the node has a variadic CaptureMarker, false otherwise
*/
static isVariadicCapture(node: { markers: Markers }): boolean {
for (const marker of node.markers.markers) {
if (marker instanceof CaptureMarker && marker.variadicOptions) {
return true;
}
}
return false;
}
/**
* Gets the variadic options from a capture marker.
*
* @param node The node to extract variadic options from
* @returns The VariadicOptions, or undefined if not a variadic capture
*/
static getVariadicOptions(node: { markers: Markers }): VariadicOptions | undefined {
for (const marker of node.markers.markers) {
if (marker instanceof CaptureMarker) {
return marker.variadicOptions;
}
}
return undefined;
}
/**
* Extracts the relevant AST node from a wrapper function.
* Used by both pattern and template processors to intelligently extract
* code from `function __WRAPPER__() { code }` wrappers.
*
* @param lastStatement The last statement from the compilation unit
* @param contextName Context name for error messages (e.g., 'Pattern', 'Template')
* @returns The extracted AST node
*/
static extractFromWrapper(lastStatement: J, contextName: string): J {
let extracted: J;
// Since we always wrap in function __WRAPPER__() { code }, look for it
if (lastStatement.kind === J.Kind.MethodDeclaration) {
const method = lastStatement as J.MethodDeclaration;
if (method.name?.simpleName === WRAPPER_FUNCTION_NAME && method.body) {
const body = method.body;
// Intelligently extract based on what's in the function body
if (body.statements.length === 0) {
throw new Error(`${contextName} function body is empty`);
} else if (body.statements.length === 1) {
const stmt = body.statements[0].element;
// Single expression statement → extract the expression
if (stmt.kind === JS.Kind.ExpressionStatement) {
extracted = (stmt as JS.ExpressionStatement).expression;
}
// Single block statement → keep the block
else if (stmt.kind === J.Kind.Block) {
extracted = stmt;
}
// Other single statement → keep it
else {
extracted = stmt;
}
} else {
// Multiple statements → keep the block
extracted = body;
}
} else {
// Not our wrapper function
extracted = lastStatement;
}
} else {
// Shouldn't happen with our wrapping strategy, but handle it
extracted = lastStatement;
}
return extracted;
}
}