/* * 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, SourceFile, Tree} from "./tree"; import {ExecutionContext} from "./execution"; import {DataTableDescriptor} from "./data-table"; import {mapAsync} from "./util"; const OPTIONS_KEY = "__recipe_options__"; export type Minutes = number; export abstract class Recipe { constructor(options?: {}) { if (options) { Object.assign(this, options); } } /** * A unique name for the recipe consisting of a dot separated sequence of category * names in which the recipe should appear followed by a name. For example, * "org.openrewrite.typescript.find-methods-by-pattern". */ readonly abstract name: string /** * A human-readable display name for the recipe, initial capped with no period. * For example, "Find text". The display name can be assumed to be rendered in * documentation and other places where markdown is understood, so it is possible * to use stylistic markers like backticks to indicate types. For example, * "Find uses of `console.log`". * * @language Markdown */ readonly abstract displayName: string /** * A human-readable description for the recipe, consisting of one or more full * sentences ending with a period. * * "Find methods by pattern." is an example. The description can be assumed to be rendered in * documentation and other places where markdown is understood, so it is possible * to use stylistic markers like backticks to indicate types. For example, * "Find uses of `console.log`.". * * @language Markdown. */ readonly abstract description: string readonly tags: string[] = [] readonly estimatedEffortPerOccurrence: Minutes = 5 readonly dataTables: DataTableDescriptor[] = [] async recipeList(): Promise { return [] } /** * A human-readable display name for this recipe instance, including some descriptive * text about the recipe options that are supplied, if any. The name must be * initial capped with no period. For example, "Find text "hello world"". * * For consistency, when surrounding option descriptive text in quotes to visually differentiate * it from the text before it, use single ``. * * Override to provide meaningful recipe instance names for recipes with complex sets of options. * * @return A name that describes this recipe instance. */ instanceName(): string { return this.displayName } async descriptor(): Promise { const optionsRecord: Record = (this as any).constructor[OPTIONS_KEY] || {} return { name: this.name, displayName: this.displayName, instanceName: this.instanceName(), description: this.description, tags: this.tags, estimatedEffortPerOccurrence: this.estimatedEffortPerOccurrence, options: Object.entries(optionsRecord).map(([key, descriptor]) => ({ name: key, value: (this as any)[key], required: descriptor.required ?? true, ...descriptor })), preconditions: [], recipeList: await mapAsync(await this.recipeList(), async r => r.descriptor()), dataTables: this.dataTables, maintainers: [], contributors: [], examples: [] } } /** * Returns the visitor that performs the transformation. This method is called by the * recipe framework during execution and must be overridden by concrete recipe implementations. * * @returns A visitor that performs the recipe's transformation */ async editor(): Promise> { return noopVisitor() } /** * At the end of a recipe run, the recipe scheduler will call this method to allow the * recipe to perform any cleanup or finalization tasks. This method is guaranteed to be called * only once per run. * * @param _ctx The recipe run execution context. */ async onComplete(_ctx: ExecutionContext): Promise { } } export interface RecipeDescriptor { readonly name: string readonly displayName: string readonly instanceName: string readonly description: string readonly tags: string[] readonly estimatedEffortPerOccurrence: Minutes readonly options: ({ name: string, value?: any } & OptionDescriptor)[] readonly preconditions: RecipeDescriptor[] readonly recipeList: RecipeDescriptor[] readonly dataTables: DataTableDescriptor[] readonly maintainers: any[] readonly contributors: any[] readonly examples: any[] } export interface OptionDescriptor { readonly displayName: string readonly description: string readonly required?: boolean readonly example?: string readonly valid?: string[] } export abstract class ScanningRecipe

extends Recipe { private readonly recipeAccMessage = Symbol("org.openrewrite.recipe.acc"); accumulator(cursor: Cursor, ctx: ExecutionContext): P { const ms = cursor.root.messages; if (!ms.has(this.recipeAccMessage)) { ms.set(this.recipeAccMessage, this.initialValue(ctx)); } return ms.get(this.recipeAccMessage); } abstract initialValue(ctx: ExecutionContext): P async editor(): Promise> { const editorWithContext = (cursor: Cursor, ctx: ExecutionContext) => this.editorWithData(this.accumulator(cursor, ctx)); return new class extends TreeVisitor { private delegate?: TreeVisitor async isAcceptable(sourceFile: SourceFile, ctx: ExecutionContext): Promise { return (await this.delegateForCtx(ctx)).isAcceptable(sourceFile, ctx); } async visit(tree: Tree, ctx: ExecutionContext, parent?: Cursor): Promise { return (await this.delegateForCtx(ctx, parent)).visit(tree, ctx, parent); } private async delegateForCtx(ctx: ExecutionContext, parent?: Cursor) { if (!this.delegate) { this.delegate = await editorWithContext(parent ?? this.cursor, ctx); } return this.delegate; } } } async editorWithData(acc: P): Promise> { return noopVisitor(); } async generate(acc: P, ctx: ExecutionContext): Promise { return []; } async scanner(acc: P): Promise> { return noopVisitor(); } } /** * Do not permit overriding of editor() */ Object.freeze(ScanningRecipe.prototype.editor); export function Option(descriptor: OptionDescriptor) { return function (target: any, propertyKey: string) { // Ensure the constructor has options storage. if (!target.constructor.hasOwnProperty(OPTIONS_KEY)) { Object.defineProperty(target.constructor, OPTIONS_KEY, { value: {}, writable: true, configurable: true, }); } // Register the option metadata under the property key. target.constructor[OPTIONS_KEY][propertyKey] = descriptor; }; } /** * Mark a property as transient, meaning it should not be part of the serialized form of * a recipe. * * @param target * @param propertyKey * @constructor */ export function Transient(target: any, propertyKey: string) { // Get the property descriptor, if any, then redefine it as non-enumerable. const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || { configurable: true, writable: true, }; descriptor.enumerable = false; Object.defineProperty(target, propertyKey, descriptor); }