@openrewrite/rewrite 8.91.3 → 8.91.5
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/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/parser.d.ts +3 -2
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +154 -114
- package/dist/javascript/parser.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/javascript/tsconfig.d.ts +32 -0
- package/dist/javascript/tsconfig.d.ts.map +1 -0
- package/dist/javascript/tsconfig.js +205 -0
- package/dist/javascript/tsconfig.js.map +1 -0
- 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/index.ts +1 -1
- package/src/javascript/parser.ts +104 -58
- package/src/javascript/remove-import.ts +50 -424
- package/src/javascript/scope.ts +103 -7
- package/src/javascript/tsconfig.ts +177 -0
- 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 {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 the original author or authors.
|
|
3
|
+
* <p>
|
|
4
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
* <p>
|
|
8
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
9
|
+
* <p>
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import ts from "typescript";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a project gets to decide: where a module specifier resolves to, and which ambient
|
|
21
|
+
* declarations are in scope. The shape of the types themselves stays with the parser — `target`
|
|
22
|
+
* and `lib` bound the global scope, `strict` adds a `null` to every nullable type — which pins
|
|
23
|
+
* the widest view it can rather than the one a project compiles against.
|
|
24
|
+
*/
|
|
25
|
+
const RESOLUTION_OPTIONS: string[] = [
|
|
26
|
+
"baseUrl",
|
|
27
|
+
"paths",
|
|
28
|
+
// Internal companion to `paths`: the directory its entries resolve against.
|
|
29
|
+
"pathsBasePath",
|
|
30
|
+
"rootDirs",
|
|
31
|
+
"types",
|
|
32
|
+
"typeRoots",
|
|
33
|
+
"moduleResolution",
|
|
34
|
+
"moduleSuffixes",
|
|
35
|
+
"customConditions",
|
|
36
|
+
"resolvePackageJsonExports",
|
|
37
|
+
"resolvePackageJsonImports",
|
|
38
|
+
"resolveJsonModule",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Config file names in precedence order. TypeScript reads jsconfig.json for JavaScript
|
|
43
|
+
* projects, which is where a JS-only codebase states its `paths`.
|
|
44
|
+
*/
|
|
45
|
+
const CONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
|
|
46
|
+
|
|
47
|
+
export interface ResolvedTsConfig {
|
|
48
|
+
/**
|
|
49
|
+
* Absolute path of the config file the options came from. Undefined when no project
|
|
50
|
+
* config applied, in which case {@link options} are the parser's defaults.
|
|
51
|
+
*/
|
|
52
|
+
readonly configFilePath?: string;
|
|
53
|
+
readonly options: ts.CompilerOptions;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Finds the config file governing each file and merges its resolution options over a set of
|
|
58
|
+
* parser defaults, so that type-aware recipes see the same modules `tsc` would.
|
|
59
|
+
*/
|
|
60
|
+
export class TsConfigResolver {
|
|
61
|
+
private readonly configByDir = new Map<string, string | undefined>();
|
|
62
|
+
private readonly optionsByConfig = new Map<string, ts.CompilerOptions | undefined>();
|
|
63
|
+
private readonly root?: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param defaults Options to fall back on, and to merge a project config over.
|
|
67
|
+
* @param relativeTo Root of the project being parsed, bounding the search. Without one,
|
|
68
|
+
* walking up from the process working directory picks up whatever config sits above it.
|
|
69
|
+
*/
|
|
70
|
+
constructor(private readonly defaults: ts.CompilerOptions, relativeTo?: string) {
|
|
71
|
+
this.root = relativeTo ? path.resolve(relativeTo) : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
forFile(filePath: string): ResolvedTsConfig {
|
|
75
|
+
const configFilePath = this.findConfig(path.dirname(path.resolve(filePath)));
|
|
76
|
+
const options = configFilePath === undefined ? undefined : this.optionsFor(configFilePath);
|
|
77
|
+
return options === undefined ? {options: this.defaults} : {configFilePath, options};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The nearest config file at or below the project root, searching upwards. */
|
|
81
|
+
private findConfig(from: string): string | undefined {
|
|
82
|
+
if (!this.root || !isAtOrUnder(from, this.root)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const searched: string[] = [];
|
|
87
|
+
const memoize = (found: string | undefined) => {
|
|
88
|
+
for (const dir of searched) {
|
|
89
|
+
this.configByDir.set(dir, found);
|
|
90
|
+
}
|
|
91
|
+
return found;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (let dir = from; ; dir = path.dirname(dir)) {
|
|
95
|
+
if (this.configByDir.has(dir)) {
|
|
96
|
+
return memoize(this.configByDir.get(dir));
|
|
97
|
+
}
|
|
98
|
+
searched.push(dir);
|
|
99
|
+
|
|
100
|
+
for (const configFileName of CONFIG_FILE_NAMES) {
|
|
101
|
+
const candidate = path.join(dir, configFileName);
|
|
102
|
+
if (ts.sys.fileExists(candidate)) {
|
|
103
|
+
return memoize(candidate);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (dir === this.root || path.dirname(dir) === dir) {
|
|
107
|
+
return memoize(undefined);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private optionsFor(configFilePath: string): ts.CompilerOptions | undefined {
|
|
113
|
+
if (!this.optionsByConfig.has(configFilePath)) {
|
|
114
|
+
this.optionsByConfig.set(configFilePath, this.merge(configFilePath));
|
|
115
|
+
}
|
|
116
|
+
return this.optionsByConfig.get(configFilePath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Undefined where the config cannot be read, leaving the caller on its defaults. */
|
|
120
|
+
private merge(configFilePath: string): ts.CompilerOptions | undefined {
|
|
121
|
+
let projectOptions: ts.CompilerOptions;
|
|
122
|
+
try {
|
|
123
|
+
const {config, error} = ts.readConfigFile(configFilePath, ts.sys.readFile);
|
|
124
|
+
if (error || !config) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
// `readDirectory` answers empty because only `options` is read here; left to `ts.sys` it
|
|
128
|
+
// walks the whole tree under the config to build a file list, once per config found.
|
|
129
|
+
const parseConfigHost: ts.ParseConfigHost = {...ts.sys, readDirectory: () => []};
|
|
130
|
+
projectOptions = ts.parseJsonConfigFileContent(
|
|
131
|
+
config, parseConfigHost, path.dirname(configFilePath), undefined, configFilePath).options;
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A project's `baseUrl` stands alone, including where it states none: `baseUrl` takes
|
|
137
|
+
// precedence over `pathsBasePath`, so a default rooted at the repository would resolve a
|
|
138
|
+
// nested project's `paths` against the wrong directory.
|
|
139
|
+
const {baseUrl: _, ...defaults} = this.defaults;
|
|
140
|
+
|
|
141
|
+
const resolution: ts.CompilerOptions = {};
|
|
142
|
+
for (const option of RESOLUTION_OPTIONS) {
|
|
143
|
+
if (projectOptions[option] !== undefined) {
|
|
144
|
+
resolution[option] = projectOptions[option];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (resolution.moduleResolution === undefined) {
|
|
148
|
+
const paired = pairedModuleResolution(projectOptions);
|
|
149
|
+
if (paired !== undefined) {
|
|
150
|
+
resolution.moduleResolution = paired;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {...defaults, ...resolution};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The resolution TypeScript infers for a project that states only its `module`. Only the
|
|
159
|
+
* inference is adopted, since a module kind such as `node16` classifies files as CJS or ESM,
|
|
160
|
+
* and an ESM-only import from a CJS file is then the error TS1479, costing the file its LST.
|
|
161
|
+
* `Classic` is declined as well, searching no `node_modules`, so the default stands in.
|
|
162
|
+
*/
|
|
163
|
+
function pairedModuleResolution(projectOptions: ts.CompilerOptions): ts.ModuleResolutionKind | undefined {
|
|
164
|
+
const infer = (ts as Partial<{ getEmitModuleResolutionKind(options: ts.CompilerOptions): ts.ModuleResolutionKind }>)
|
|
165
|
+
.getEmitModuleResolutionKind;
|
|
166
|
+
if (projectOptions.module === undefined || !infer) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const inferred = infer(projectOptions);
|
|
170
|
+
return inferred === ts.ModuleResolutionKind.Classic ? undefined : inferred;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isAtOrUnder(dir: string, root: string): boolean {
|
|
174
|
+
const relative = path.relative(root, dir);
|
|
175
|
+
return relative === "" ||
|
|
176
|
+
(relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative));
|
|
177
|
+
}
|
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
|
}
|