@openrewrite/rewrite 8.81.9 → 8.81.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,8 +18,66 @@ import {Cursor, isSourceFile, SourceFile, Tree} from './tree';
18
18
  import {Recipe} from "./recipe";
19
19
  import {ExecutionContext} from "./execution";
20
20
 
21
+ /**
22
+ * Recipe-identity placeholder for use as a precondition.
23
+ *
24
+ * Captures a Java recipe class name + options without instantiating the
25
+ * recipe or firing an RPC. The framework introspects a
26
+ * ``check(RecipeRef, editor)`` wrapper at PrepareRecipe time and emits
27
+ * the recipe identity directly in
28
+ * ``PrepareRecipeResponse.editPreconditions``. The Java host's
29
+ * ``PreparedRecipeCache.instantiateVisitor`` constructs the recipe via
30
+ * Jackson and uses its visitor.
31
+ *
32
+ * This avoids requiring the recipe author to do an RPC at ``editor()``
33
+ * construction time, which would otherwise block in-process unit tests
34
+ * that don't have an active RPC connection.
35
+ *
36
+ * If ``localVisitor`` is provided, in-process callers (without an active
37
+ * RPC connection) evaluate the gate against that visitor instead of
38
+ * short-circuiting to "always matches". This preserves real filtering
39
+ * behavior in unit tests while still letting the host evaluate the gate
40
+ * over the wire when an RPC connection is present.
41
+ *
42
+ * Helpers in ``@openrewrite/rewrite/javascript/preconditions``
43
+ * (``usesMethod``, ``usesType``, ``hasSourcePath``, ``findMethods``,
44
+ * ``findTypes``) return ``RecipeRef`` instances populated with the
45
+ * matching native TS visitor where one exists.
46
+ */
47
+ export class RecipeRef {
48
+ constructor(
49
+ readonly recipeName: string,
50
+ readonly options: Readonly<Record<string, any>> = {},
51
+ readonly localVisitor?: TreeVisitor<any, ExecutionContext>
52
+ ) {
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Composite of nested precondition operands joined by an operator.
58
+ *
59
+ * Mirrors Java's ``Preconditions.or``/``and``/``not``: a gate that
60
+ * short-circuits over its operands. ``op`` is one of ``"or"`` / ``"and"`` /
61
+ * ``"not"``. Operands may be ``TreeVisitor``, ``Recipe``, ``RecipeRef``,
62
+ * or another ``CompositePrecondition``.
63
+ *
64
+ * The framework promotes the composite to a structured wire entry at
65
+ * PrepareRecipe time; the Java host's ``RewriteRpc.matchAll`` rebuilds
66
+ * the visitor via the matching ``Preconditions`` factory so the gate runs
67
+ * locally and the visit RPC is skipped for files the gate rejects.
68
+ */
69
+ export class CompositePrecondition {
70
+ constructor(
71
+ readonly op: "or" | "and" | "not",
72
+ readonly operands: ReadonlyArray<CheckArg>
73
+ ) {
74
+ }
75
+ }
76
+
77
+ export type CheckArg = Recipe | TreeVisitor<any, ExecutionContext> | RecipeRef | CompositePrecondition;
78
+
21
79
  export async function check<T extends Tree>(
22
- checkCondition: Recipe | Promise<Recipe> | TreeVisitor<T, ExecutionContext> | Promise<TreeVisitor<T, ExecutionContext>> | boolean,
80
+ checkCondition: CheckArg | Promise<CheckArg> | boolean,
23
81
  v: TreeVisitor<T, ExecutionContext> | Promise<TreeVisitor<T, ExecutionContext>>
24
82
  ): Promise<TreeVisitor<T, ExecutionContext>> {
25
83
  const resolvedCheck = await checkCondition;
@@ -31,15 +89,52 @@ export async function check<T extends Tree>(
31
89
  return new Check(resolvedCheck, resolvedV);
32
90
  }
33
91
 
92
+ /**
93
+ * OR-compose precondition checks. Mirrors Java's
94
+ * ``Preconditions.or(visitor...)``: the gate matches if any operand
95
+ * matches. Requires at least two operands; a single-operand OR has no
96
+ * value over a bare ``check``.
97
+ */
98
+ export function or(...operands: CheckArg[]): CompositePrecondition {
99
+ if (operands.length < 2) {
100
+ throw new Error("Preconditions.or requires at least two operands");
101
+ }
102
+ return new CompositePrecondition("or", operands);
103
+ }
104
+
105
+ /**
106
+ * AND-compose precondition checks. The outer ``editPreconditions`` list
107
+ * is already AND-composed by the host, so this is mainly useful as an
108
+ * operand of ``or``/``not``.
109
+ */
110
+ export function and(...operands: CheckArg[]): CompositePrecondition {
111
+ if (operands.length < 2) {
112
+ throw new Error("Preconditions.and requires at least two operands");
113
+ }
114
+ return new CompositePrecondition("and", operands);
115
+ }
116
+
117
+ /**
118
+ * Negate a precondition check. Mirrors Java's ``Preconditions.not(visitor)``:
119
+ * the gate matches iff the operand does not.
120
+ */
121
+ export function not(operand: CheckArg): CompositePrecondition {
122
+ return new CompositePrecondition("not", [operand]);
123
+ }
124
+
34
125
  export class Check<T extends Tree> extends TreeVisitor<T, ExecutionContext> {
35
126
  constructor(
36
- readonly check: TreeVisitor<T, ExecutionContext> | Recipe,
127
+ readonly check: CheckArg,
37
128
  readonly v: TreeVisitor<T, ExecutionContext>
38
129
  ) {
39
130
  super();
40
131
  }
41
132
 
42
133
  async isAcceptable(sourceFile: SourceFile, ctx: ExecutionContext): Promise<boolean> {
134
+ if (this.check instanceof RecipeRef || this.check instanceof CompositePrecondition) {
135
+ // RecipeRef / Composite have no in-process is_acceptable — defer to the wrapped editor.
136
+ return this.v.isAcceptable(sourceFile, ctx);
137
+ }
43
138
  return await (await this.checkVisitor()).isAcceptable(sourceFile, ctx) &&
44
139
  await this.v.isAcceptable(sourceFile, ctx);
45
140
  }
@@ -53,12 +148,31 @@ export class Check<T extends Tree> extends TreeVisitor<T, ExecutionContext> {
53
148
  : this.v.visit<R>(tree, ctx);
54
149
  }
55
150
 
56
- const checkResult = parent !== undefined
57
- ? await (await this.checkVisitor()).visit(tree, ctx, parent)
58
- : await (await this.checkVisitor()).visit(tree, ctx);
151
+ // In-process fallback for a RecipeRef: if a localVisitor was
152
+ // provided, evaluate the gate against it (preserves real filtering
153
+ // for unit tests); otherwise treat as "always matches" so the
154
+ // wrapped editor still runs. Wire-side optimization (skip the
155
+ // visit RPC when the precondition rejects the file) lives in
156
+ // optimizePreconditions and is independent of localVisitor.
157
+ if (this.check instanceof RecipeRef) {
158
+ if (this.check.localVisitor !== undefined) {
159
+ const localResult = parent !== undefined
160
+ ? await this.check.localVisitor.visit(tree, ctx, parent)
161
+ : await this.check.localVisitor.visit(tree, ctx);
162
+ if (localResult === (tree as unknown as T)) {
163
+ return tree as unknown as R;
164
+ }
165
+ }
166
+ return parent !== undefined
167
+ ? this.v.visit<R>(tree, ctx, parent)
168
+ : this.v.visit<R>(tree, ctx);
169
+ }
170
+
171
+ const matched = this.check instanceof CompositePrecondition
172
+ ? await evaluateComposite(this.check, tree, ctx, parent)
173
+ : await this.runLeafCheck(tree, ctx, parent);
59
174
 
60
- // If check visitor modified the tree (returned something different), run the main visitor
61
- if (checkResult !== (tree as unknown as T)) {
175
+ if (matched) {
62
176
  return parent !== undefined
63
177
  ? this.v.visit<R>(tree, ctx, parent)
64
178
  : this.v.visit<R>(tree, ctx);
@@ -67,7 +181,75 @@ export class Check<T extends Tree> extends TreeVisitor<T, ExecutionContext> {
67
181
  return tree as unknown as R;
68
182
  }
69
183
 
184
+ private async runLeafCheck(tree: Tree, ctx: ExecutionContext, parent?: Cursor): Promise<boolean> {
185
+ const checkResult = parent !== undefined
186
+ ? await (await this.checkVisitor()).visit(tree, ctx, parent)
187
+ : await (await this.checkVisitor()).visit(tree, ctx);
188
+ return checkResult !== (tree as unknown as T);
189
+ }
190
+
70
191
  private async checkVisitor(): Promise<TreeVisitor<any, ExecutionContext>> {
71
- return this.check instanceof Recipe ? this.check.editor() : this.check;
192
+ return this.check instanceof Recipe ? this.check.editor() : (this.check as TreeVisitor<any, ExecutionContext>);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Evaluate a {@link CompositePrecondition} in-process for unit tests and
198
+ * direct callers that don't have a live RPC. Mirrors Java's
199
+ * ``Preconditions.or``/``and``/``not`` semantics. Returns ``true`` iff the
200
+ * gate would let the wrapped visitor run.
201
+ */
202
+ async function evaluateComposite(
203
+ composite: CompositePrecondition,
204
+ tree: Tree,
205
+ ctx: ExecutionContext,
206
+ parent?: Cursor
207
+ ): Promise<boolean> {
208
+ const operands = composite.operands;
209
+ switch (composite.op) {
210
+ case "or":
211
+ for (const operand of operands) {
212
+ if (await operandMatches(operand, tree, ctx, parent)) return true;
213
+ }
214
+ return false;
215
+ case "and":
216
+ for (const operand of operands) {
217
+ if (!(await operandMatches(operand, tree, ctx, parent))) return false;
218
+ }
219
+ return true;
220
+ case "not":
221
+ if (operands.length !== 1) {
222
+ throw new Error("CompositePrecondition op=not requires exactly one operand");
223
+ }
224
+ return !(await operandMatches(operands[0], tree, ctx, parent));
225
+ }
226
+ }
227
+
228
+ async function operandMatches(
229
+ operand: CheckArg,
230
+ tree: Tree,
231
+ ctx: ExecutionContext,
232
+ parent?: Cursor
233
+ ): Promise<boolean> {
234
+ if (operand instanceof RecipeRef) {
235
+ // If a localVisitor was provided, evaluate against it for real;
236
+ // otherwise short-circuit to "always matches" so the wrapped
237
+ // editor still runs in unit tests. The host evaluates the gate
238
+ // for real once the response goes over the wire.
239
+ if (operand.localVisitor === undefined) {
240
+ return true;
241
+ }
242
+ const result = parent !== undefined
243
+ ? await operand.localVisitor.visit(tree, ctx, parent)
244
+ : await operand.localVisitor.visit(tree, ctx);
245
+ return result !== tree;
246
+ }
247
+ if (operand instanceof CompositePrecondition) {
248
+ return evaluateComposite(operand, tree, ctx, parent);
72
249
  }
250
+ const visitor = operand instanceof Recipe ? await operand.editor() : operand;
251
+ const result = parent !== undefined
252
+ ? await visitor.visit(tree, ctx, parent)
253
+ : await visitor.visit(tree, ctx);
254
+ return result !== tree;
73
255
  }
@@ -17,7 +17,7 @@ import * as rpc from "vscode-jsonrpc/node";
17
17
  import {MessageConnection} from "vscode-jsonrpc/node";
18
18
  import {Recipe, RecipeDescriptor, ScanningRecipe} from "../../recipe";
19
19
  import {SnowflakeId} from "@akashrajpurohit/snowflake-id";
20
- import {Check} from "../../preconditions";
20
+ import {Check, CheckArg, CompositePrecondition, RecipeRef} from "../../preconditions";
21
21
  import {RpcRecipe} from "../recipe";
22
22
  import {TreeVisitor} from "../../visitor";
23
23
  import {ExecutionContext} from "../../execution";
@@ -109,8 +109,9 @@ export class PrepareRecipe {
109
109
  }
110
110
 
111
111
  if (visitor! instanceof Check) {
112
- if (visitor.check instanceof RpcRecipe) {
113
- preconditions.push({visitorName: phase === "edit" ? visitor.check.editVisitor : visitor.check.scanVisitor!});
112
+ const wireEntry = this.conditionWireEntry(visitor.check, phase);
113
+ if (wireEntry) {
114
+ preconditions.push(wireEntry);
114
115
  recipe = Object.assign(
115
116
  Object.create(Object.getPrototypeOf(recipe)),
116
117
  recipe,
@@ -135,6 +136,42 @@ export class PrepareRecipe {
135
136
  return recipe;
136
137
  }
137
138
 
139
+ /**
140
+ * Translate a precondition condition (operand) to a wire entry.
141
+ *
142
+ * Mirrors the Java {@code PrepareRecipeResponse.Precondition} schema:
143
+ * leaves carry {@code visitorName} (+ optional options); composites
144
+ * carry {@code op} ({@code "or"}/{@code "and"}/{@code "not"}) and a
145
+ * nested {@code operands} list. Returns {@code undefined} when the
146
+ * condition can't be serialized — the caller leaves the wrapper
147
+ * intact so the gate runs in-process as a fallback.
148
+ */
149
+ private static conditionWireEntry(condition: CheckArg, phase: "edit" | "scan"): Precondition | undefined {
150
+ if (condition instanceof CompositePrecondition) {
151
+ const operands: Precondition[] = [];
152
+ for (const operand of condition.operands) {
153
+ const entry = this.conditionWireEntry(operand, phase);
154
+ if (entry === undefined) {
155
+ return undefined;
156
+ }
157
+ operands.push(entry);
158
+ }
159
+ return {op: condition.op, operands};
160
+ }
161
+ // Common case: helpers like usesMethod / usesType return a lightweight
162
+ // RecipeRef so the recipe author can declare a precondition without
163
+ // firing an RPC at editor() time. The Java host's
164
+ // PreparedRecipeCache.instantiateVisitor constructs the named recipe
165
+ // via Jackson and uses its visitor.
166
+ if (condition instanceof RecipeRef) {
167
+ return {visitorName: condition.recipeName, visitorOptions: {...condition.options}};
168
+ }
169
+ if (condition instanceof RpcRecipe) {
170
+ return {visitorName: phase === "edit" ? condition.editVisitor : condition.scanVisitor!};
171
+ }
172
+ return undefined;
173
+ }
174
+
138
175
  private static async visitorTypePrecondition(preconditions: Precondition[], v: TreeVisitor<any, ExecutionContext>): Promise<Precondition[]> {
139
176
  let treeType: string | undefined;
140
177
 
@@ -183,7 +220,20 @@ export interface PrepareRecipeResponse {
183
220
  delegatesTo?: DelegatesTo
184
221
  }
185
222
 
223
+ /**
224
+ * Either a leaf (a single visitor identified by {@code visitorName} +
225
+ * optional {@code visitorOptions}) or a composite of nested preconditions
226
+ * joined by {@code op} ({@code "or"} / {@code "and"} / {@code "not"}).
227
+ *
228
+ * When {@code op} is undefined the entry is a leaf and {@code visitorName}
229
+ * is required; when {@code op} is set, {@code operands} carries the
230
+ * children and the visitor fields are ignored. The composite form mirrors
231
+ * Java's {@code Preconditions.or}/{@code and}/{@code not} so remote
232
+ * languages can express the same gate shapes the Java side does.
233
+ */
186
234
  export interface Precondition {
187
- visitorName: string
235
+ visitorName?: string
188
236
  visitorOptions?: {}
237
+ op?: "or" | "and" | "not"
238
+ operands?: Precondition[]
189
239
  }