@openrewrite/rewrite 8.91.4 → 8.91.6
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.
- package/dist/javascript/add-import.js +4 -6
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/index.d.ts +1 -1
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +2 -1
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/remove-import.d.ts +0 -18
- package/dist/javascript/remove-import.d.ts.map +1 -1
- package/dist/javascript/remove-import.js +50 -438
- package/dist/javascript/remove-import.js.map +1 -1
- package/dist/javascript/scope.d.ts +6 -0
- package/dist/javascript/scope.d.ts.map +1 -1
- package/dist/javascript/scope.js +87 -7
- package/dist/javascript/scope.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/rpc/index.d.ts +1 -1
- package/dist/rpc/index.d.ts.map +1 -1
- package/dist/rpc/index.js +2 -1
- package/dist/rpc/index.js.map +1 -1
- package/dist/rpc/java-recipe.d.ts +32 -22
- package/dist/rpc/java-recipe.d.ts.map +1 -1
- package/dist/rpc/java-recipe.js +91 -30
- package/dist/rpc/java-recipe.js.map +1 -1
- package/dist/rpc/request/prepare-recipe.d.ts +10 -14
- package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
- package/dist/rpc/request/prepare-recipe.js +53 -88
- package/dist/rpc/request/prepare-recipe.js.map +1 -1
- package/dist/rpc/rewrite-rpc.d.ts +5 -12
- package/dist/rpc/rewrite-rpc.d.ts.map +1 -1
- package/dist/rpc/rewrite-rpc.js +5 -12
- package/dist/rpc/rewrite-rpc.js.map +1 -1
- package/package.json +1 -1
- package/src/javascript/add-import.ts +4 -6
- package/src/javascript/index.ts +1 -1
- package/src/javascript/remove-import.ts +50 -424
- package/src/javascript/scope.ts +103 -7
- package/src/rpc/index.ts +1 -1
- package/src/rpc/java-recipe.ts +107 -33
- package/src/rpc/request/prepare-recipe.ts +55 -90
- package/src/rpc/rewrite-rpc.ts +5 -12
package/src/javascript/scope.ts
CHANGED
|
@@ -152,6 +152,69 @@ export function namesDeclaredWithin(node: unknown, cacheKey: object = node as ob
|
|
|
152
152
|
return names;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The names `node` reads and nothing within it binds, so each one reaches a binding further out.
|
|
157
|
+
* A name its parent introduces rather than reads is not one, nor is a name an inner scope rebinds:
|
|
158
|
+
* that reference reads the rebinding.
|
|
159
|
+
*/
|
|
160
|
+
export function namesReferencedWithin(node: unknown, cacheKey: object = node as object): ReadonlySet<string> {
|
|
161
|
+
if (cacheKey === null || typeof cacheKey !== 'object') {
|
|
162
|
+
return noNames;
|
|
163
|
+
}
|
|
164
|
+
const cached = referenced.get(cacheKey);
|
|
165
|
+
if (cached) {
|
|
166
|
+
return cached;
|
|
167
|
+
}
|
|
168
|
+
const names = new Set<string>();
|
|
169
|
+
collectReferences(node, undefined, undefined, names);
|
|
170
|
+
referenced.set(cacheKey, names);
|
|
171
|
+
return names;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The scopes enclosing a node, innermost first, each paired with what {@link frameBindings} reads. */
|
|
175
|
+
interface Frames {
|
|
176
|
+
node: unknown;
|
|
177
|
+
parent: unknown;
|
|
178
|
+
outer?: Frames;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Whether a scope between the name and where the walk began binds it. */
|
|
182
|
+
function shadowed(scopes: Frames | undefined, name: string): boolean {
|
|
183
|
+
for (let scope = scopes; scope; scope = scope.outer) {
|
|
184
|
+
if (frameBindings(scope.node, scope.parent).has(name)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function collectReferences(node: unknown, parent: unknown, scopes: Frames | undefined, names: Set<string>): void {
|
|
192
|
+
if (Array.isArray(node)) {
|
|
193
|
+
node.forEach(child => collectReferences(child, parent, scopes, names));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const kind = (node as { kind?: string } | undefined)?.kind;
|
|
197
|
+
if (kind === J.Kind.RightPadded || kind === J.Kind.LeftPadded) {
|
|
198
|
+
// A naming position reads against the tree parent, so padding forwards the one it was handed.
|
|
199
|
+
collectReferences((node as J.RightPadded<any>).element, parent, scopes, names);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (kind === J.Kind.Container) {
|
|
203
|
+
collectReferences((node as J.Container<any>).elements, parent, scopes, names);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!isTree(node)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const name = kind === J.Kind.Identifier ? (node as J.Identifier).simpleName : undefined;
|
|
210
|
+
if (name && reads(node as J.Identifier, parent) && !shadowed(scopes, name)) {
|
|
211
|
+
names.add(name);
|
|
212
|
+
}
|
|
213
|
+
const within = scopeKinds.has(kind!) ? {node, parent, outer: scopes} : scopes;
|
|
214
|
+
Object.entries(node as object).forEach(([key, value]) =>
|
|
215
|
+
key !== 'markers' && collectReferences(value, node, within, names));
|
|
216
|
+
}
|
|
217
|
+
|
|
155
218
|
/**
|
|
156
219
|
* Every name a binding pattern introduces. `member` is the property a name takes its value from,
|
|
157
220
|
* which only a name an object pattern binds directly has: anything deeper reads a property of a
|
|
@@ -213,8 +276,12 @@ function frameBindings(node: any, parent: any): ReadonlySet<string> {
|
|
|
213
276
|
// A function expression is the only function whose own name its body reaches: a declaration's
|
|
214
277
|
// name belongs to the enclosing block, a method's to an instance.
|
|
215
278
|
if (node?.kind === J.Kind.MethodDeclaration && parent?.kind === JS.Kind.StatementExpression) {
|
|
216
|
-
|
|
217
|
-
|
|
279
|
+
let names = selfNamed.get(node);
|
|
280
|
+
if (!names) {
|
|
281
|
+
const self = bindingNames((node as J.MethodDeclaration).name).map(bound => bound.name);
|
|
282
|
+
selfNamed.set(node, names = new Set([...self, ...ownBindings(node)]));
|
|
283
|
+
}
|
|
284
|
+
return names;
|
|
218
285
|
}
|
|
219
286
|
return ownBindings(node);
|
|
220
287
|
}
|
|
@@ -369,7 +436,9 @@ const blockScoped = new Set(['let', 'const', 'using']);
|
|
|
369
436
|
const hoisted = new WeakMap<object, string[]>();
|
|
370
437
|
const declared = new WeakMap<object, ReadonlySet<string>>();
|
|
371
438
|
const used = new WeakMap<object, ReadonlySet<string>>();
|
|
439
|
+
const referenced = new WeakMap<object, ReadonlySet<string>>();
|
|
372
440
|
const frames = new WeakMap<object, ReadonlySet<string>>();
|
|
441
|
+
const selfNamed = new WeakMap<object, ReadonlySet<string>>();
|
|
373
442
|
|
|
374
443
|
/**
|
|
375
444
|
* The names blocks under `scope` hoist out to it. A `var` or function declaration reaches the whole
|
|
@@ -496,19 +565,46 @@ export function isValueReference(cursor: Cursor, identifier: J.Identifier): bool
|
|
|
496
565
|
while (c && isPadding(c.value)) {
|
|
497
566
|
c = c.parent;
|
|
498
567
|
}
|
|
499
|
-
|
|
500
|
-
|
|
568
|
+
return references(identifier, c?.value);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** The position half of {@link isValueReference}, against a parent already found. */
|
|
572
|
+
function references(identifier: J.Identifier, parent: unknown): boolean {
|
|
573
|
+
const owner = parent as {
|
|
574
|
+
kind?: string; name?: unknown; key?: unknown; label?: unknown; select?: unknown;
|
|
575
|
+
propertyName?: unknown;
|
|
576
|
+
} | undefined;
|
|
501
577
|
|
|
502
578
|
// A call names a member of whatever it selects from. With nothing selected there is no member,
|
|
503
579
|
// and its `name` is a reference to the function being called.
|
|
504
|
-
if (
|
|
580
|
+
if (owner?.kind === J.Kind.MethodInvocation && !owner.select) {
|
|
505
581
|
return true;
|
|
506
582
|
}
|
|
507
583
|
|
|
584
|
+
// A binding element names the property it destructures and binds under `name`, so both slots
|
|
585
|
+
// name rather than reference.
|
|
586
|
+
if (owner?.kind === JS.Kind.BindingElement && holds(owner.propertyName, identifier)) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
|
|
508
590
|
// A JSX attribute keeps its prop name on `key`; the three label-bearing nodes keep theirs on
|
|
509
591
|
// `label`. Every other kind that names rather than references keeps it on `name`.
|
|
510
|
-
|
|
511
|
-
|
|
592
|
+
return !holds(owner?.kind === JS.Kind.JsxAttribute ? owner.key : (owner?.name ?? owner?.label), identifier);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Whether a node slot, padded or not, is the identifier itself. */
|
|
596
|
+
function holds(slot: unknown, identifier: J.Identifier): boolean {
|
|
597
|
+
return slot === identifier || (slot as { element?: unknown } | undefined)?.element === identifier;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* As {@link references}, for a collector rather than a renamer: a shorthand property's name slot is
|
|
602
|
+
* also the value it reads, which a rename has to expand rather than follow.
|
|
603
|
+
*/
|
|
604
|
+
function reads(identifier: J.Identifier, parent: unknown): boolean {
|
|
605
|
+
const owner = parent as { kind?: string; initializer?: unknown } | undefined;
|
|
606
|
+
return (owner?.kind === JS.Kind.PropertyAssignment && owner.initializer === undefined) ||
|
|
607
|
+
references(identifier, parent);
|
|
512
608
|
}
|
|
513
609
|
|
|
514
610
|
function isPadding(value: unknown): boolean {
|
package/src/rpc/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ export * from "./queue";
|
|
|
33
33
|
export * from "../reference";
|
|
34
34
|
export {RewriteRpc} from "./rewrite-rpc";
|
|
35
35
|
export {RpcRecipe, RpcVisitor} from "./recipe";
|
|
36
|
-
export {prepareJavaRecipe} from "./java-recipe";
|
|
36
|
+
export {DelegatingRecipe, prepareJavaRecipe} from "./java-recipe";
|
|
37
37
|
export {registerVisitor} from "./request/visitor-registry";
|
|
38
38
|
|
|
39
39
|
RpcCodecs.registerCodec(TreeKind.Checksum, {
|
package/src/rpc/java-recipe.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright
|
|
2
|
+
* Copyright 2026 the original author or authors.
|
|
3
3
|
* <p>
|
|
4
4
|
* Licensed under the Moderne Source Available License (the "License");
|
|
5
5
|
* you may not use this file except in compliance with the License.
|
|
@@ -15,23 +15,110 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import {RewriteRpc} from "./rewrite-rpc";
|
|
17
17
|
import type {RpcRecipe} from "./recipe";
|
|
18
|
+
import {Recipe, RecipeDescriptor, ScanningRecipe} from "../recipe";
|
|
19
|
+
import {TreeVisitor} from "../visitor";
|
|
20
|
+
import {ExecutionContext} from "../execution";
|
|
21
|
+
import {SourceFile} from "../tree";
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
* A stand-in for a Java recipe, composable like any other recipe (a `recipeList()` entry, a
|
|
25
|
+
* `check(...)` gate, a `RecipeSpec.recipe`). The Java host resolves it by name: from `delegatesTo`
|
|
26
|
+
* when it prepares the enclosing recipe, or by class name when it gates a visitor, like a `RecipeRef`.
|
|
27
|
+
* Only when this process runs it itself (a test, a JS-hosted run) is it prepared over the active
|
|
28
|
+
* {@link RewriteRpc} connection, on first use.
|
|
29
|
+
*/
|
|
30
|
+
export class DelegatingRecipe extends ScanningRecipe<number> {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly displayName: string;
|
|
33
|
+
readonly description: string;
|
|
34
|
+
private delegate?: {rpc: RewriteRpc, recipe: Promise<RpcRecipe>};
|
|
35
|
+
|
|
36
|
+
constructor(readonly javaRecipeName: string,
|
|
37
|
+
readonly delegatesToOptions: Record<string, any>) {
|
|
38
|
+
super();
|
|
39
|
+
this.name = javaRecipeName;
|
|
40
|
+
this.displayName = javaRecipeName;
|
|
41
|
+
this.description = `Delegates to the Java recipe \`${javaRecipeName}\`.`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Local, with an empty recipe list: the host fills the subtree in, and recipeList() prepares the delegate.
|
|
45
|
+
async descriptor(): Promise<RecipeDescriptor> {
|
|
46
|
+
return {
|
|
47
|
+
name: this.name,
|
|
48
|
+
displayName: this.displayName,
|
|
49
|
+
instanceName: this.instanceName(),
|
|
50
|
+
description: this.description,
|
|
51
|
+
tags: this.tags,
|
|
52
|
+
estimatedEffortPerOccurrence: this.estimatedEffortPerOccurrence,
|
|
53
|
+
options: Object.entries(this.delegatesToOptions).map(([name, value]) => ({
|
|
54
|
+
name,
|
|
55
|
+
value,
|
|
56
|
+
displayName: name,
|
|
57
|
+
description: "",
|
|
58
|
+
required: false
|
|
59
|
+
})),
|
|
60
|
+
preconditions: [],
|
|
61
|
+
recipeList: [],
|
|
62
|
+
dataTables: this.dataTables,
|
|
63
|
+
maintainers: [],
|
|
64
|
+
contributors: [],
|
|
65
|
+
examples: []
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async recipeList(): Promise<Recipe[]> {
|
|
70
|
+
return (await this.prepared()).recipeList();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
initialValue(_ctx: ExecutionContext): number {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async editorWithData(acc: number): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
78
|
+
return (await this.prepared()).editorWithData(acc);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async scanner(acc: number): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
82
|
+
return (await this.prepared()).scanner(acc);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async generate(acc: number, ctx: ExecutionContext): Promise<SourceFile[]> {
|
|
86
|
+
return (await this.prepared()).generate(acc, ctx);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async onComplete(ctx: ExecutionContext): Promise<void> {
|
|
90
|
+
if (this.delegate) {
|
|
91
|
+
await (await this.delegate.recipe).onComplete(ctx);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private prepared(): Promise<RpcRecipe> {
|
|
96
|
+
const rpc = RewriteRpc.get();
|
|
97
|
+
if (!rpc) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Cannot run Java recipe "${this.javaRecipeName}": no active RewriteRpc connection.\n` +
|
|
100
|
+
" • Tests: spawn one via JavaRpcTestServer.start() — see " +
|
|
101
|
+
"@openrewrite/rewrite/test/java-rpc.\n" +
|
|
102
|
+
" • Production: the Java host provides one automatically when it " +
|
|
103
|
+
"loads the TS recipe artifact."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
// Prepared once per connection; a failed prepare is dropped so the next use retries.
|
|
107
|
+
if (!this.delegate || this.delegate.rpc !== rpc) {
|
|
108
|
+
const recipe = rpc.prepareRecipe(this.javaRecipeName, this.delegatesToOptions);
|
|
109
|
+
this.delegate = {rpc, recipe};
|
|
110
|
+
recipe.catch(() => {
|
|
111
|
+
if (this.delegate?.recipe === recipe) {
|
|
112
|
+
this.delegate = undefined;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return this.delegate.recipe;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reference a Java recipe by name from a TS recipe:
|
|
35
122
|
*
|
|
36
123
|
* ```ts
|
|
37
124
|
* export function addDependency(options: AddDependencyOptions): Promise<Recipe> {
|
|
@@ -39,26 +126,13 @@ import type {RpcRecipe} from "./recipe";
|
|
|
39
126
|
* }
|
|
40
127
|
* ```
|
|
41
128
|
*
|
|
42
|
-
* There is no in-process fallback by design: a Java recipe's editor is the
|
|
43
|
-
*
|
|
44
|
-
* preconditions that need a graceful no-RPC fallback, use the {@link RecipeRef}
|
|
129
|
+
* There is no in-process fallback by design: a Java recipe's editor is the single source of
|
|
130
|
+
* truth. For preconditions that need a graceful no-RPC fallback, use the {@link RecipeRef}
|
|
45
131
|
* pattern via `usesType` / `usesMethod`, which carries a `localVisitor`.
|
|
46
|
-
*
|
|
47
|
-
* @throws Error when no active {@link RewriteRpc} connection is registered.
|
|
48
132
|
*/
|
|
49
133
|
export async function prepareJavaRecipe(
|
|
50
134
|
id: string,
|
|
51
135
|
options?: Record<string, any>,
|
|
52
|
-
): Promise<
|
|
53
|
-
|
|
54
|
-
if (!rpc) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
`Cannot prepare Java recipe "${id}": no active RewriteRpc connection.\n` +
|
|
57
|
-
" • Tests: spawn one via JavaRpcTestServer.start() — see " +
|
|
58
|
-
"@openrewrite/rewrite/test/java-rpc.\n" +
|
|
59
|
-
" • Production: the Java host provides one automatically when it " +
|
|
60
|
-
"loads the TS recipe artifact."
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
return rpc.prepareRecipe(id, options);
|
|
136
|
+
): Promise<DelegatingRecipe> {
|
|
137
|
+
return new DelegatingRecipe(id, options ?? {});
|
|
64
138
|
}
|
|
@@ -19,6 +19,7 @@ import {Recipe, RecipeDescriptor, ScanningRecipe} from "../../recipe";
|
|
|
19
19
|
import {SnowflakeId} from "@akashrajpurohit/snowflake-id";
|
|
20
20
|
import {Check, CheckArg, CompositePrecondition, RecipeRef} from "../../preconditions";
|
|
21
21
|
import {RpcRecipe} from "../recipe";
|
|
22
|
+
import {DelegatingRecipe} from "../java-recipe";
|
|
22
23
|
import {TreeVisitor} from "../../visitor";
|
|
23
24
|
import {ExecutionContext} from "../../execution";
|
|
24
25
|
import {withMetrics} from "./metrics";
|
|
@@ -42,22 +43,10 @@ export class PrepareRecipe {
|
|
|
42
43
|
context.target = request.id;
|
|
43
44
|
const recipeCtor = marketplace.findRecipe(request.id);
|
|
44
45
|
if (!recipeCtor) {
|
|
45
|
-
// A miss means the host owns this recipe (e.g. a
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const id = snowflake.generate();
|
|
50
|
-
return {
|
|
51
|
-
id: id,
|
|
52
|
-
descriptor: PrepareRecipe.delegateDescriptor(request.id),
|
|
53
|
-
editVisitor: `edit:${id}`,
|
|
54
|
-
editPreconditions: [],
|
|
55
|
-
scanPreconditions: [],
|
|
56
|
-
delegatesTo: {
|
|
57
|
-
recipeName: request.id,
|
|
58
|
-
options: request.options ?? {}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
46
|
+
// A miss means the host owns this recipe (e.g. a Java-delegate child a host
|
|
47
|
+
// re-prepares by name), so answer with a delegatesTo stand-in for it to resolve.
|
|
48
|
+
return await PrepareRecipe.prepareInstance(new DelegatingRecipe(request.id, request.options ?? {}),
|
|
49
|
+
snowflake, preparedRecipes, marketplace);
|
|
61
50
|
}
|
|
62
51
|
if (!recipeCtor[1]) {
|
|
63
52
|
throw new Error(`Recipe ${request.id} was installed without a constructor`);
|
|
@@ -70,44 +59,30 @@ export class PrepareRecipe {
|
|
|
70
59
|
}
|
|
71
60
|
|
|
72
61
|
/**
|
|
73
|
-
*
|
|
74
|
-
* {@
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
name: name,
|
|
80
|
-
displayName: name,
|
|
81
|
-
instanceName: name,
|
|
82
|
-
description: "",
|
|
83
|
-
tags: [],
|
|
84
|
-
estimatedEffortPerOccurrence: 5,
|
|
85
|
-
options: [],
|
|
86
|
-
preconditions: [],
|
|
87
|
-
recipeList: [],
|
|
88
|
-
dataTables: [],
|
|
89
|
-
maintainers: [],
|
|
90
|
-
contributors: [],
|
|
91
|
-
examples: []
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Prepares a recipe instance and recursively its whole child tree, registering every node in
|
|
97
|
-
* {@code preparedRecipes} and returning the prepared response with {@code recipeList} populated,
|
|
98
|
-
* so the host builds the tree locally instead of a PrepareRecipe round-trip per child.
|
|
99
|
-
*
|
|
100
|
-
* Mirrors the C# server's PrepareInstance. Required options are validated as each node is
|
|
101
|
-
* prepared (covering the whole tree). A child hosted on the peer (an {@link RpcRecipe}, e.g. from
|
|
102
|
-
* prepareJavaRecipe) carries only {@code delegatesTo} for the host to resolve locally; every other
|
|
103
|
-
* child carries its own {@code recipeList}. Delegating recipes forward validation to the recipe
|
|
104
|
-
* they delegate to, so they are not validated here.
|
|
62
|
+
* Prepares a recipe and, recursively, its whole child tree, registering every node in
|
|
63
|
+
* {@code preparedRecipes}; the response carries {@code recipeList} so the host builds the tree
|
|
64
|
+
* without a PrepareRecipe round-trip per child (mirrors the C# server's PrepareInstance).
|
|
65
|
+
* Required options are validated per node. A stand-in for a host recipe (see {@link #delegatesTo})
|
|
66
|
+
* carries only {@code delegatesTo} and leaves validation to the host.
|
|
105
67
|
*/
|
|
106
68
|
private static async prepareInstance(recipe: Recipe,
|
|
107
69
|
snowflake: ReturnType<typeof SnowflakeId>,
|
|
108
70
|
preparedRecipes: Map<String, Recipe>,
|
|
109
71
|
marketplace: RecipeMarketplace): Promise<PrepareRecipeResponse> {
|
|
110
72
|
const id = snowflake.generate();
|
|
73
|
+
const delegatesTo = PrepareRecipe.delegatesTo(recipe);
|
|
74
|
+
if (delegatesTo) {
|
|
75
|
+
// Registered so a peer that visits `edit:<id>` instead of honouring delegatesTo reaches it.
|
|
76
|
+
preparedRecipes.set(id, recipe);
|
|
77
|
+
return {
|
|
78
|
+
id: id,
|
|
79
|
+
descriptor: await recipe.descriptor(),
|
|
80
|
+
editVisitor: `edit:${id}`,
|
|
81
|
+
editPreconditions: [],
|
|
82
|
+
scanPreconditions: [],
|
|
83
|
+
delegatesTo
|
|
84
|
+
};
|
|
85
|
+
}
|
|
111
86
|
|
|
112
87
|
const editPreconditions: Precondition[] = [];
|
|
113
88
|
recipe = await PrepareRecipe.optimizePreconditions(recipe, "edit", editPreconditions);
|
|
@@ -117,13 +92,9 @@ export class PrepareRecipe {
|
|
|
117
92
|
preparedRecipes.set(id, recipe);
|
|
118
93
|
|
|
119
94
|
const descriptor = await recipe.descriptor();
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
for (const option of descriptor.options) {
|
|
124
|
-
if ((option.required ?? true) && option.value == null) {
|
|
125
|
-
throw new Error(`Missing required option \`${option.name}\` for recipe \`${descriptor.name}\`.`);
|
|
126
|
-
}
|
|
95
|
+
for (const option of descriptor.options) {
|
|
96
|
+
if ((option.required ?? true) && option.value == null) {
|
|
97
|
+
throw new Error(`Missing required option \`${option.name}\` for recipe \`${descriptor.name}\`.`);
|
|
127
98
|
}
|
|
128
99
|
}
|
|
129
100
|
|
|
@@ -136,49 +107,43 @@ export class PrepareRecipe {
|
|
|
136
107
|
scanPreconditions: scanPreconditions
|
|
137
108
|
};
|
|
138
109
|
|
|
139
|
-
if (isDelegating) {
|
|
140
|
-
response.delegatesTo = {
|
|
141
|
-
recipeName: (recipe as any).javaRecipeName,
|
|
142
|
-
options: (recipe as any).delegatesToOptions ?? {}
|
|
143
|
-
};
|
|
144
|
-
return response;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
110
|
const childResponses: PrepareRecipeResponse[] = [];
|
|
148
111
|
for (const child of await recipe.recipeList()) {
|
|
149
112
|
if (child instanceof RpcRecipe) {
|
|
150
|
-
//
|
|
151
|
-
// host resolves it locally, matching how the by-name fallback used to resolve it.
|
|
152
|
-
const childId = snowflake.generate();
|
|
153
|
-
const childDescriptor = await child.descriptor();
|
|
113
|
+
// Already prepared on the host: hand it back by name with the options its parent set.
|
|
154
114
|
const options: Record<string, any> = {};
|
|
155
|
-
for (const option of
|
|
115
|
+
for (const option of (await child.descriptor()).options) {
|
|
156
116
|
if (option.value != null) {
|
|
157
117
|
options[option.name] = option.value;
|
|
158
118
|
}
|
|
159
119
|
}
|
|
160
|
-
childResponses.push(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
editVisitor: `edit:${childId}`,
|
|
164
|
-
editPreconditions: [],
|
|
165
|
-
scanPreconditions: [],
|
|
166
|
-
delegatesTo: {recipeName: child.name, options}
|
|
167
|
-
});
|
|
168
|
-
} else {
|
|
169
|
-
// Register a child that was instantiated in recipeList() but never installed, so a peer
|
|
170
|
-
// that re-prepares children by name (rather than consuming recipeList) can still find it.
|
|
171
|
-
if (!marketplace.findRecipe(child.name)) {
|
|
172
|
-
await marketplace.install(child.constructor as any, []);
|
|
173
|
-
}
|
|
174
|
-
childResponses.push(await PrepareRecipe.prepareInstance(child, snowflake, preparedRecipes, marketplace));
|
|
120
|
+
childResponses.push(await PrepareRecipe.prepareInstance(new DelegatingRecipe(child.name, options),
|
|
121
|
+
snowflake, preparedRecipes, marketplace));
|
|
122
|
+
continue;
|
|
175
123
|
}
|
|
124
|
+
// Register a child that was instantiated in recipeList() but never installed, so a peer
|
|
125
|
+
// that re-prepares children by name (rather than consuming recipeList) can still find it.
|
|
126
|
+
if (!PrepareRecipe.delegatesTo(child) && !marketplace.findRecipe(child.name)) {
|
|
127
|
+
await marketplace.install(child.constructor as any, []);
|
|
128
|
+
}
|
|
129
|
+
childResponses.push(await PrepareRecipe.prepareInstance(child, snowflake, preparedRecipes, marketplace));
|
|
176
130
|
}
|
|
177
131
|
response.recipeList = childResponses;
|
|
178
132
|
|
|
179
133
|
return response;
|
|
180
134
|
}
|
|
181
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The Java recipe {@code recipe} stands in for, or {@code undefined} for a recipe this process
|
|
138
|
+
* runs itself. Duck-typed on the fields {@link DelegatingRecipe} declares, so a recipe package
|
|
139
|
+
* that loaded its own copy of this module is recognised too.
|
|
140
|
+
*/
|
|
141
|
+
private static delegatesTo(recipe: any): {recipeName: string, options: Record<string, any>} | undefined {
|
|
142
|
+
return recipe != null && typeof recipe.javaRecipeName === "string" ?
|
|
143
|
+
{recipeName: recipe.javaRecipeName, options: {...(recipe.delegatesToOptions ?? {})}} :
|
|
144
|
+
undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
182
147
|
/**
|
|
183
148
|
* For preconditions that can be evaluated on the remote peer, let the remote peer
|
|
184
149
|
* evaluate them and know that we will only have to do the visit work if the
|
|
@@ -246,13 +211,13 @@ export class PrepareRecipe {
|
|
|
246
211
|
}
|
|
247
212
|
return {op: condition.op, operands};
|
|
248
213
|
}
|
|
249
|
-
// Common case:
|
|
250
|
-
//
|
|
251
|
-
// firing an RPC at editor() time. The Java host's
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
return {visitorName:
|
|
214
|
+
// Common case: usesMethod / usesType return a lightweight RecipeRef, and
|
|
215
|
+
// prepareJavaRecipe a DelegatingRecipe, so a precondition is declared without
|
|
216
|
+
// firing an RPC at editor() time. The Java host's PreparedRecipeCache
|
|
217
|
+
// .instantiateVisitor constructs the named recipe via Jackson and uses its visitor.
|
|
218
|
+
const ref = condition instanceof RecipeRef ? condition : PrepareRecipe.delegatesTo(condition);
|
|
219
|
+
if (ref) {
|
|
220
|
+
return {visitorName: ref.recipeName, visitorOptions: {...ref.options}};
|
|
256
221
|
}
|
|
257
222
|
if (condition instanceof RpcRecipe) {
|
|
258
223
|
return {visitorName: phase === "edit" ? condition.editVisitor : condition.scanVisitor!};
|
package/src/rpc/rewrite-rpc.ts
CHANGED
|
@@ -50,18 +50,11 @@ import {GetLanguages} from "./request/get-languages";
|
|
|
50
50
|
|
|
51
51
|
export class RewriteRpc {
|
|
52
52
|
/**
|
|
53
|
-
* Key for the active {@link RewriteRpc} connection on {@link globalThis}.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* own class object and therefore its own statics. A `static` field set by
|
|
59
|
-
* the host would be invisible to such a copy, so `RewriteRpc.get()` (e.g.
|
|
60
|
-
* via `prepareJavaRecipe`) would return `undefined` and throw "no active
|
|
61
|
-
* RewriteRpc connection" — surfacing during `InstallRecipes` as the
|
|
62
|
-
* misleading "Ensure the constructor can be called without any arguments".
|
|
63
|
-
* {@link Symbol.for} resolves to the same symbol across every module copy,
|
|
64
|
-
* so all copies share the one active connection. See gh-7968.
|
|
53
|
+
* Key for the active {@link RewriteRpc} connection on {@link globalThis}. A recipe package that
|
|
54
|
+
* bundles `@openrewrite/rewrite` or resolves it from its own `node_modules` loads a separate copy
|
|
55
|
+
* of this module with its own class object and statics, so a `static` field set by the host would
|
|
56
|
+
* be invisible to it; {@link Symbol.for} resolves to the same symbol across every module copy, so
|
|
57
|
+
* all copies share the one active connection. See gh-7968.
|
|
65
58
|
*/
|
|
66
59
|
private static readonly GLOBAL_KEY: symbol = Symbol.for("org.openrewrite.rpc.RewriteRpc.global");
|
|
67
60
|
|