/*
* 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