/* * 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 {ApplyOptions, Parameter, TemplateOptions, TemplateParameter} from './types'; import {MatchResult} from './pattern'; import {generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils'; import {CAPTURE_NAME_SYMBOL, RAW_CODE_SYMBOL} from './capture'; import {TemplateEngine} from './engine'; import {JS} from '..'; /** * Coordinates for template application. */ type JavaCoordinates = { tree?: Tree; loc?: JavaCoordinates.Location; mode?: JavaCoordinates.Mode; }; namespace JavaCoordinates { // FIXME need to come up with the equivalent of `Space.Location` support export type Location = 'EXPRESSION_PREFIX' | 'STATEMENT_PREFIX' | 'BLOCK_END'; export enum Mode { Before, After, Replace, } } /** * Builder for creating templates programmatically. * Use when template structure is not known at compile time. * * @example * // Conditional construction * const builder = Template.builder().code('function foo(x) {'); * if (needsValidation) { * builder.code('if (typeof x !== "number") throw new Error("Invalid");'); * } * builder.code('return x * 2; }'); * const tmpl = builder.build(); * * @example * // Composition from fragments * function createWrapper(innerBody: Capture): Template { * return Template.builder() * .code('function wrapper() { try { ') * .param(innerBody) * .code(' } catch(e) { console.error(e); } }') * .build(); * } */ export class TemplateBuilder { private parts: string[] = []; private params: TemplateParameter[] = []; /** * Adds a static string part to the template. * * @param str The string to add * @returns This builder for chaining */ code(str: string): this { // If there are already params, we need to add an empty string before this if (this.params.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 parameter to the template. * * @param value The parameter value (Capture, Tree, or primitive); may be added more than once * (see {@link template}) * @returns This builder for chaining */ param(value: TemplateParameter): this { // Ensure we have a part for after this parameter if (this.parts.length === 0) { this.parts.push(''); } this.params.push(value); // Add an empty string for the next part this.parts.push(''); return this; } /** * Builds the template from accumulated parts and parameters. * * @returns A Template instance */ build(): Template { // Ensure parts array is one longer than params array while (this.parts.length <= this.params.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 template() function return template(templateStrings, ...this.params); } } /** * Template for creating AST nodes. * * This class provides the public API for template generation. * The actual templating logic is handled by the internal TemplateEngine. * * Templates can reference captures from patterns, and you can access properties * of captured nodes using dot notation. This allows you to extract and insert * specific subtrees from matched AST nodes. * * @example * // Generate a literal AST node * const result = await template`2`.apply(node, cursor); * * @example * // Generate an AST node with a parameter * const result = await template`${capture()}`.apply(node, cursor); * * @example * // Access properties of captured nodes in templates * const method = capture('method'); * const pat = pattern`foo(${method})`; * const tmpl = template`bar(${method.name})`; // Access the 'name' property * * const match = await pat.match(someNode, cursor); * if (match) { * // The template will insert just the 'name' subtree from the captured method * const result = await tmpl.apply(someNode, cursor, { values: match }); * } * * @example * // Deep property access chains * const method = capture('method'); * template`console.log(${method.name.simpleName})` // Navigate multiple properties * * @example * // Array element access * const invocation = capture('invocation'); * template`bar(${invocation.arguments.elements[0].element})` // Access array elements */ export class Template { private options: TemplateOptions = {}; private _cachedTemplate?: J; /** * Creates a new template. * * @param templateParts The string parts of the template * @param parameters The parameters between the string parts */ constructor( private readonly templateParts: TemplateStringsArray, private readonly parameters: Parameter[] ) { } /** * Creates a new builder for constructing templates programmatically. * * @returns A new TemplateBuilder instance * * @example * const tmpl = Template.builder() * .code('function foo() {') * .code('return ') * .param(capture('value')) * .code('; }') * .build(); */ static builder(): TemplateBuilder { return new TemplateBuilder(); } /** * Configures this template with additional options. * * @param options Configuration options * @returns This template for method chaining * * @example * template`isDate(${capture('date')})` * .configure({ * context: ['import { isDate } from "util"'], * dependencies: { 'util': '^1.0.0' } * }) */ configure(options: TemplateOptions): Template { this.options = {...this.options, ...options}; // Invalidate cache when configuration changes this._cachedTemplate = undefined; return this; } /** * Gets the template tree for this template, using two-level caching: * - Level 1: Instance cache (this._cachedTemplate) - fastest, no lookup needed * - Level 2: Global cache (globalAstCache) - fast, shared across all templates * - Level 3: TemplateEngine - slow, parses and processes the template * * Most parameters use placeholders that are replaced during application, so templates * with the same structure share cached ASTs. However, raw() parameters are spliced at * construction time, so their values must be included in the cache key. * * @returns The cached or newly computed template tree * @internal */ private async getTemplateTree(): Promise { // Level 1: Instance cache (fastest path) if (this._cachedTemplate) { return this._cachedTemplate as JS.CompilationUnit; } // Generate cache key for global lookup // For raw() parameters, we need to include their code values in the key // since they're spliced at construction time, not application time const contextStatements = this.options.context || this.options.imports || []; const parametersKey = this.parameters.map((p, i) => { const value = p.value; // Include raw code values in the cache key using the symbol if (value && typeof value === 'object' && value[RAW_CODE_SYMBOL]) { return `raw:${value.code}`; } return i.toString(); }).join(','); const cacheKey = generateCacheKey( this.templateParts, parametersKey, contextStatements, this.options.dependencies || {} ); // Level 2: Global cache (fast path - shared with Pattern) const cached = globalAstCache.get(cacheKey); if (cached) { this._cachedTemplate = cached as JS.CompilationUnit; return cached as JS.CompilationUnit; } // Level 3: Compute via TemplateEngine (slow path) const result = await TemplateEngine.getTemplateTree( this.templateParts, this.parameters, contextStatements, this.options.dependencies || {} ) as JS.CompilationUnit; // Cache in both levels globalAstCache.set(cacheKey, result); this._cachedTemplate = result; return result; } /** * Applies this template and returns the resulting tree. * * @param tree Input tree to transform * @param cursor The cursor pointing to the current location in the AST * @param options Optional configuration including values for parameters * @returns A Promise resolving to the generated AST node * * @example * ```typescript * // Simple application without values * const result = await tmpl.apply(node, cursor); * * // With values from pattern match * const match = await pat.match(node, cursor); * const result = await tmpl.apply(node, cursor, { values: match }); * * // With explicit values * const result = await tmpl.apply(node, cursor, { * values: { x: someNode, y: anotherNode } * }); * ``` */ async apply(tree: J, cursor: Cursor, options?: ApplyOptions): Promise { // Extract values from options const values = options?.values; // Normalize the values map: convert any Capture keys to string keys let normalizedValues: Pick, 'get'> | undefined; let wrappersMap: Map | J.RightPadded[]> = new Map(); if (values instanceof MatchResult) { // MatchResult - extract both bindings and wrappersMap normalizedValues = values; wrappersMap = (values as any)[WRAPPERS_MAP_SYMBOL](); } else if (values instanceof Map) { const normalized = new Map(); for (const [key, value] of values.entries()) { const stringKey = typeof key === 'string' ? key : ((key as any)[CAPTURE_NAME_SYMBOL] || key.getName()); normalized.set(stringKey, value); } normalizedValues = normalized; } else if (values && typeof values === 'object') { // Check if it's a Map-like object with 'get' method, or a plain object literal if ('get' in values && typeof values.get === 'function') { // Map-like object with get method normalizedValues = values as Pick, 'get'>; } else { // Plain object literal - convert to Map // Keys may be strings or Capture objects (via computed properties {[x]: value}) const normalized = new Map(); for (const [key, value] of Object.entries(values)) { // If the key happens to be a stringified Capture (from computed properties), // it's already been converted to a string by JavaScript normalized.set(key, value); } normalizedValues = normalized; } } // Use instance-level cache to get the template tree const ast = await this.getTemplateTree(); // Delegate to TemplateEngine for placeholder substitution and application return TemplateEngine.applyTemplateFromAst( ast, this.parameters, cursor, { tree, mode: JavaCoordinates.Mode.Replace }, normalizedValues, wrappersMap ); } } /** * Tagged template function for creating templates that generate AST nodes. * * Templates support property access on captures from patterns, allowing you to * extract and insert specific subtrees from matched AST nodes. Use dot notation * to navigate properties (e.g., `method.name`) or array bracket notation to * access array elements (e.g., `args.elements[0].element`). * * Templates can also accept AST wrapper types directly: * - J.RightPadded: The element will be extracted and inserted * - J.RightPadded[]: Elements will be expanded in place * - J.Container: Elements will be expanded in place * * @param strings The string parts of the template * @param parameters The parameters between the string parts (Capture, CaptureValue, TemplateParam, Tree, Tree[], J.RightPadded, J.RightPadded[], or J.Container). * A parameter may appear more than once — `(${arr} ? f(${arr}) : -1)`. A node * keeps its id only at its first occurrence, and only if it belongs to the * subtree being replaced; anything else is spliced under fresh ids. * @returns A Template object that can be applied to generate AST nodes * * @example * // Simple template with literal * const tmpl = template`console.log("hello")`; * const result = await tmpl.apply(node, cursor); * * @example * // Template with capture - matches captured value from pattern * const expr = capture('expr'); * const pat = pattern`foo(${expr})`; * const tmpl = template`bar(${expr})`; * * const match = await pat.match(node, cursor); * if (match) { * const result = await tmpl.apply(node, cursor, { values: match }); * } * * @example * // Property access on captures - extract subtrees * const method = capture('method'); * const pat = pattern`foo(${method})`; * // Access the 'name' property of the captured method invocation * const tmpl = template`bar(${method.name})`; * * @example * // Deep property chains * const method = capture('method'); * template`console.log(${method.name.simpleName})` * * @example * // Array element access * const invocation = capture('invocation'); * template`bar(${invocation.arguments.elements[0].element})` * * @example * // Using J.RightPadded and J.Container directly * const selectExpr = method.select; // J.RightPadded * const args = method.arguments; // J.Container * template`${selectExpr}.newMethod(${args})` */ export function template(strings: TemplateStringsArray, ...parameters: TemplateParameter[]): Template { // Convert parameters to Parameter objects (no longer need to check for mutable tree property) const processedParameters = parameters.map(param => { // Just wrap each parameter value in a Parameter object return {value: param}; }); return new Template(strings, processedParameters); } export type {JavaCoordinates};