/* * 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, ExecutionContext, Recipe, TreeVisitor} from '../..'; import {J, Statement} from '../../java'; import {PostMatchContext, PreMatchContext, RewriteConfig, RewriteRule} from './types'; import {MatchResult, Pattern} from './pattern'; import {Template} from './template'; import {JavaScriptVisitor} from '../visitor'; /** * Implementation of a replacement rule. */ class RewriteRuleImpl implements RewriteRule { constructor( private readonly before: Pattern[], private readonly after: Template | ((match: MatchResult) => Template), private readonly preMatch?: (node: J, context: PreMatchContext) => boolean | Promise, private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise ) { } async tryOn(cursor: Cursor, node: J): Promise { // Evaluate preMatch before attempting any pattern matching if (this.preMatch) { const preMatchResult = await this.preMatch(node, { cursor }); if (!preMatchResult) { return undefined; // Early exit - don't attempt pattern matching } } for (const pattern of this.before) { // Pass cursor to pattern.match() for context-aware capture constraints const match = await pattern.match(node, cursor); if (match) { // Evaluate postMatch after structural match succeeds if (this.postMatch) { const postMatchResult = await this.postMatch(node, { cursor, captures: match }); if (!postMatchResult) { continue; // Pattern matched but postMatch failed, try next pattern } } // Apply transformation let result: J | undefined; if (typeof this.after === 'function') { // Call the function to get a template, then apply it const template = this.after(match); result = await template.apply(node, cursor, { values: match }); } else { // Use template.apply() as before result = await this.after.apply(node, cursor, { values: match }); } if (result) { return result; } } } // Return undefined if no patterns match or all postMatch checks failed return undefined; } andThen(next: RewriteRule): RewriteRule { const first = this; return new (class extends RewriteRuleImpl { constructor() { // Pass empty patterns and a function that will never be called // since we override tryOn super([], () => undefined as unknown as Template); } async tryOn(cursor: Cursor, node: J): Promise { const firstResult = await first.tryOn(cursor, node); if (firstResult !== undefined) { const secondResult = await next.tryOn(cursor, firstResult); return secondResult ?? firstResult; } return undefined; } })(); } orElse(alternative: RewriteRule): RewriteRule { const first = this; return new (class extends RewriteRuleImpl { constructor() { // Pass empty patterns and a function that will never be called // since we override tryOn super([], () => undefined as unknown as Template); } async tryOn(cursor: Cursor, node: J): Promise { const firstResult = await first.tryOn(cursor, node); if (firstResult !== undefined) { return firstResult; } return await alternative.tryOn(cursor, node); } })(); } } /** * Creates a replacement rule using a capture context and configuration. * * @param builderFn Function that takes a capture context and returns before/after configuration * @returns A replacement rule that can be applied to AST nodes * * @example * // Single pattern * const swapOperands = rewrite(() => { * const { left, right } = { left: capture(), right: capture() }; * return { * before: pattern`${left} + ${right}`, * after: template`${right} + ${left}` * }; * }); * * @example * // Multiple patterns * const normalizeComparisons = rewrite(() => { * const { left, right } = { left: capture(), right: capture() }; * return { * before: [ * pattern`${left} == ${right}`, * pattern`${left} === ${right}` * ], * after: template`${left} === ${right}` * }; * }); * * @example * // Using in a visitor - IMPORTANT: use `|| node` to handle undefined when no match * class MyVisitor extends JavaScriptVisitor { * override async visitBinary(binary: J.Binary, p: any): Promise { * const rule = rewrite(() => ({ * before: pattern`${capture('a')} + ${capture('b')}`, * after: template`${capture('b')} + ${capture('a')}` * })); * // tryOn() returns undefined if no pattern matches, so always use || node * return await rule.tryOn(this.cursor, binary) || binary; * } * } */ export function rewrite( builderFn: () => RewriteConfig ): RewriteRule { const config = builderFn(); // Ensure we have valid before and after properties if (!config.before || !config.after) { throw new Error('Builder function must return an object with before and after properties'); } return new RewriteRuleImpl( Array.isArray(config.before) ? config.before : [config.before], config.after, config.preMatch, config.postMatch ); } /** * Creates a RewriteRule from a Recipe by using its editor visitor. * * This allows recipes to be used in the same chaining pattern as other rewrite rules, * enabling composition with `andThen()`. * * @param recipe The recipe whose editor will be used to transform nodes * @param ctx The execution context to pass to the recipe's editor * @returns A RewriteRule that applies the recipe's editor to nodes * * @example * ```typescript * class MyRecipe extends Recipe { * name = "my.recipe"; * displayName = "My Recipe"; * description = "Transforms code."; * * async editor(): Promise> { * return new MyVisitor(); * } * } * * // In a visitor: * override async visitBinary(binary: J.Binary, p: ExecutionContext): Promise { * const rule1 = rewrite(() => ({ * before: pattern`${capture('a')} + ${capture('b')}`, * after: template`${capture('b')} + ${capture('a')}` * })); * * const rule2 = fromRecipe(new MyRecipe(), p); * * // Chain the pattern-based rule with the recipe * const combined = rule1.andThen(rule2); * return await combined.tryOn(this.cursor, binary) || binary; * } * ``` */ export const fromRecipe = (recipe: Recipe, ctx: ExecutionContext): RewriteRule => { return new (class extends RewriteRuleImpl { constructor() { // Pass empty patterns and a function that will never be called // since we override tryOn super([], () => undefined as unknown as Template); } async tryOn(cursor: Cursor, tree: J): Promise { const visitor = await recipe.editor(); const result = await visitor.visit(tree, ctx, cursor); // Return undefined if the visitor didn't change the node return result !== tree ? result : undefined; } })(); } /** * Registers an after-visitor that will flatten a block's statements into its parent block. * * When a rewrite template produces a J.Block containing multiple statements, but you want * those statements to be inserted directly into the parent block (not nested), use this * function to register a follow-up visitor that performs the flattening. * * @param visitor The current visitor instance (to register the after-visitor) * @param block The block whose statements should be flattened into its parent * @returns The block (for chaining in return statements) * * @example * ```typescript * override async visitReturn(ret: J.Return, ctx: ExecutionContext): Promise { * const result = await rewrite(() => ({ * before: pattern`return #{cond} || #{arr}.some(#{cb})`, * after: template`{ * if (#{cond}) return true; * for (const item of #{arr}) { * if (await #{cb}(item)) return true; * } * return false; * }` * })).tryOn(this.cursor, ret); * * if (result && result.kind === J.Kind.Block) { * return flattenBlock(this, result as J.Block); * } * return result ?? ret; * } * ``` */ export function flattenBlock

( visitor: TreeVisitor, block: J.Block ): J.Block { // Create a visitor that will flatten this specific block when found in a parent block const flattenVisitor = new class extends JavaScriptVisitor

{ protected override async visitBlock(parentBlock: J.Block, p: P): Promise { let modified = false; const newStatements: typeof parentBlock.statements = []; for (const stmt of parentBlock.statements) { // Check if this statement is the block we want to flatten if (stmt.element === block || stmt.element.id === block.id) { // Splice in the inner block's statements for (let i = 0; i < block.statements.length; i++) { const innerStmt = block.statements[i]; if (i === 0) { // First statement inherits the outer statement's padding newStatements.push({ ...innerStmt, element: { ...innerStmt.element, prefix: stmt.element.prefix // Use the original statement's prefix } as Statement }); } else { newStatements.push(innerStmt); } } modified = true; } else { newStatements.push(stmt); } } if (modified) { return { ...parentBlock, statements: newStatements } as J.Block; } return super.visitBlock(parentBlock, p); } }(); // Register the flatten visitor to run after the main visitor completes visitor.afterVisit.push(flattenVisitor); return block; }