/* * 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 {noopVisitor, TreeVisitor} from './visitor'; import {Cursor, isSourceFile, SourceFile, Tree} from './tree'; import {Recipe} from "./recipe"; import {ExecutionContext} from "./execution"; /** * Recipe-identity placeholder for use as a precondition. * * Captures a Java recipe class name + options without instantiating the * recipe or firing an RPC. The framework introspects a * ``check(RecipeRef, editor)`` wrapper at PrepareRecipe time and emits * the recipe identity directly in * ``PrepareRecipeResponse.editPreconditions``. The Java host's * ``PreparedRecipeCache.instantiateVisitor`` constructs the recipe via * Jackson and uses its visitor. * * This avoids requiring the recipe author to do an RPC at ``editor()`` * construction time, which would otherwise block in-process unit tests * that don't have an active RPC connection. * * If ``localVisitor`` is provided, in-process callers (without an active * RPC connection) evaluate the gate against that visitor instead of * short-circuiting to "always matches". This preserves real filtering * behavior in unit tests while still letting the host evaluate the gate * over the wire when an RPC connection is present. * * Helpers in ``@openrewrite/rewrite/javascript/preconditions`` * (``usesMethod``, ``usesType``, ``hasSourcePath``, ``findMethods``, * ``findTypes``) return ``RecipeRef`` instances populated with the * matching native TS visitor where one exists. */ export class RecipeRef { constructor( readonly recipeName: string, readonly options: Readonly> = {}, readonly localVisitor?: TreeVisitor ) { } } /** * Composite of nested precondition operands joined by an operator. * * Mirrors Java's ``Preconditions.or``/``and``/``not``: a gate that * short-circuits over its operands. ``op`` is one of ``"or"`` / ``"and"`` / * ``"not"``. Operands may be ``TreeVisitor``, ``Recipe``, ``RecipeRef``, * or another ``CompositePrecondition``. * * The framework promotes the composite to a structured wire entry at * PrepareRecipe time; the Java host's ``RewriteRpc.matchAll`` rebuilds * the visitor via the matching ``Preconditions`` factory so the gate runs * locally and the visit RPC is skipped for files the gate rejects. */ export class CompositePrecondition { constructor( readonly op: "or" | "and" | "not", readonly operands: ReadonlyArray ) { } } export type CheckArg = Recipe | TreeVisitor | RecipeRef | CompositePrecondition; export async function check( checkCondition: CheckArg | Promise | boolean, v: TreeVisitor | Promise> ): Promise> { const resolvedCheck = await checkCondition; const resolvedV = await v; if (typeof resolvedCheck === 'boolean') { return resolvedCheck ? resolvedV : noopVisitor(); } return new Check(resolvedCheck, resolvedV); } /** * OR-compose precondition checks. Mirrors Java's * ``Preconditions.or(visitor...)``: the gate matches if any operand * matches. Requires at least two operands; a single-operand OR has no * value over a bare ``check``. */ export function or(...operands: CheckArg[]): CompositePrecondition { if (operands.length < 2) { throw new Error("Preconditions.or requires at least two operands"); } return new CompositePrecondition("or", operands); } /** * AND-compose precondition checks. The outer ``editPreconditions`` list * is already AND-composed by the host, so this is mainly useful as an * operand of ``or``/``not``. */ export function and(...operands: CheckArg[]): CompositePrecondition { if (operands.length < 2) { throw new Error("Preconditions.and requires at least two operands"); } return new CompositePrecondition("and", operands); } /** * Negate a precondition check. Mirrors Java's ``Preconditions.not(visitor)``: * the gate matches iff the operand does not. */ export function not(operand: CheckArg): CompositePrecondition { return new CompositePrecondition("not", [operand]); } export class Check extends TreeVisitor { constructor( readonly check: CheckArg, readonly v: TreeVisitor ) { super(); } async isAcceptable(sourceFile: SourceFile, ctx: ExecutionContext): Promise { if (this.check instanceof RecipeRef || this.check instanceof CompositePrecondition) { // RecipeRef / Composite have no in-process is_acceptable — defer to the wrapped editor. return this.v.isAcceptable(sourceFile, ctx); } return await (await this.checkVisitor()).isAcceptable(sourceFile, ctx) && await this.v.isAcceptable(sourceFile, ctx); } async visit(tree: Tree, ctx: ExecutionContext, parent?: Cursor): Promise { // if tree isn't an instanceof of SourceFile, then a precondition visitor may // not be able to do its work because it may assume we are starting from the root level if (!isSourceFile(tree)) { return parent !== undefined ? this.v.visit(tree, ctx, parent) : this.v.visit(tree, ctx); } // In-process fallback for a RecipeRef: if a localVisitor was // provided, evaluate the gate against it (preserves real filtering // for unit tests); otherwise treat as "always matches" so the // wrapped editor still runs. Wire-side optimization (skip the // visit RPC when the precondition rejects the file) lives in // optimizePreconditions and is independent of localVisitor. if (this.check instanceof RecipeRef) { if (this.check.localVisitor !== undefined) { const localResult = parent !== undefined ? await this.check.localVisitor.visit(tree, ctx, parent) : await this.check.localVisitor.visit(tree, ctx); if (localResult === (tree as unknown as T)) { return tree as unknown as R; } } return parent !== undefined ? this.v.visit(tree, ctx, parent) : this.v.visit(tree, ctx); } const matched = this.check instanceof CompositePrecondition ? await evaluateComposite(this.check, tree, ctx, parent) : await this.runLeafCheck(tree, ctx, parent); if (matched) { return parent !== undefined ? this.v.visit(tree, ctx, parent) : this.v.visit(tree, ctx); } return tree as unknown as R; } private async runLeafCheck(tree: Tree, ctx: ExecutionContext, parent?: Cursor): Promise { const checkResult = parent !== undefined ? await (await this.checkVisitor()).visit(tree, ctx, parent) : await (await this.checkVisitor()).visit(tree, ctx); return checkResult !== (tree as unknown as T); } private async checkVisitor(): Promise> { return this.check instanceof Recipe ? this.check.editor() : (this.check as TreeVisitor); } } /** * Evaluate a {@link CompositePrecondition} in-process for unit tests and * direct callers that don't have a live RPC. Mirrors Java's * ``Preconditions.or``/``and``/``not`` semantics. Returns ``true`` iff the * gate would let the wrapped visitor run. */ async function evaluateComposite( composite: CompositePrecondition, tree: Tree, ctx: ExecutionContext, parent?: Cursor ): Promise { const operands = composite.operands; switch (composite.op) { case "or": for (const operand of operands) { if (await operandMatches(operand, tree, ctx, parent)) return true; } return false; case "and": for (const operand of operands) { if (!(await operandMatches(operand, tree, ctx, parent))) return false; } return true; case "not": if (operands.length !== 1) { throw new Error("CompositePrecondition op=not requires exactly one operand"); } return !(await operandMatches(operands[0], tree, ctx, parent)); } } async function operandMatches( operand: CheckArg, tree: Tree, ctx: ExecutionContext, parent?: Cursor ): Promise { if (operand instanceof RecipeRef) { // If a localVisitor was provided, evaluate against it for real; // otherwise short-circuit to "always matches" so the wrapped // editor still runs in unit tests. The host evaluates the gate // for real once the response goes over the wire. if (operand.localVisitor === undefined) { return true; } const result = parent !== undefined ? await operand.localVisitor.visit(tree, ctx, parent) : await operand.localVisitor.visit(tree, ctx); return result !== tree; } if (operand instanceof CompositePrecondition) { return evaluateComposite(operand, tree, ctx, parent); } const visitor = operand instanceof Recipe ? await operand.editor() : operand; const result = parent !== undefined ? await visitor.visit(tree, ctx, parent) : await visitor.visit(tree, ctx); return result !== tree; }