@openrewrite/rewrite 8.75.2 → 8.75.4
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/format/format.d.ts +2 -0
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +97 -69
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -1
- package/dist/javascript/format/minimum-viable-spacing-visitor.js +1 -3
- package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -1
- package/dist/javascript/format/whitespace-reconciler.d.ts +6 -0
- package/dist/javascript/format/whitespace-reconciler.d.ts.map +1 -1
- package/dist/javascript/format/whitespace-reconciler.js +31 -1
- package/dist/javascript/format/whitespace-reconciler.js.map +1 -1
- package/dist/javascript/recipes/index.d.ts +1 -0
- package/dist/javascript/recipes/index.d.ts.map +1 -1
- package/dist/javascript/recipes/index.js +1 -0
- package/dist/javascript/recipes/index.js.map +1 -1
- package/dist/javascript/recipes/order-imports.d.ts.map +1 -1
- package/dist/javascript/recipes/order-imports.js +5 -0
- package/dist/javascript/recipes/order-imports.js.map +1 -1
- package/dist/javascript/recipes/remove-dependency.d.ts +29 -0
- package/dist/javascript/recipes/remove-dependency.d.ts.map +1 -0
- package/dist/javascript/recipes/remove-dependency.js +261 -0
- package/dist/javascript/recipes/remove-dependency.js.map +1 -0
- package/dist/javascript/remove-import.d.ts.map +1 -1
- package/dist/javascript/remove-import.js +9 -8
- package/dist/javascript/remove-import.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/rpc/request/prepare-recipe.d.ts +1 -0
- package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
- package/dist/rpc/request/prepare-recipe.js +18 -9
- package/dist/rpc/request/prepare-recipe.js.map +1 -1
- package/dist/test/rewrite-test.d.ts +1 -0
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +18 -7
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +7 -2
- package/dist/visitor.js.map +1 -1
- package/package.json +12 -11
- package/src/javascript/format/format.ts +99 -71
- package/src/javascript/format/minimum-viable-spacing-visitor.ts +1 -4
- package/src/javascript/format/whitespace-reconciler.ts +27 -2
- package/src/javascript/recipes/index.ts +1 -0
- package/src/javascript/recipes/order-imports.ts +6 -0
- package/src/javascript/recipes/remove-dependency.ts +345 -0
- package/src/javascript/remove-import.ts +10 -9
- package/src/rpc/request/prepare-recipe.ts +20 -9
- package/src/test/rewrite-test.ts +17 -3
- package/src/visitor.ts +7 -2
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 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
|
+
|
|
17
|
+
import {Option, ScanningRecipe} from "../../recipe";
|
|
18
|
+
import {ExecutionContext} from "../../execution";
|
|
19
|
+
import {TreeVisitor} from "../../visitor";
|
|
20
|
+
import {Tree} from "../../tree";
|
|
21
|
+
import {getMemberKeyName, isJson, isObject, Json, JsonVisitor} from "../../json";
|
|
22
|
+
import {isDocuments, isYaml, Yaml} from "../../yaml";
|
|
23
|
+
import {isPlainText, PlainText} from "../../text";
|
|
24
|
+
import {
|
|
25
|
+
allDependencyScopes,
|
|
26
|
+
DependencyScope,
|
|
27
|
+
findNodeResolutionResult,
|
|
28
|
+
PackageManager,
|
|
29
|
+
serializeNpmrcConfigs
|
|
30
|
+
} from "../node-resolution-result";
|
|
31
|
+
import {markupWarn} from "../../markers";
|
|
32
|
+
import {TreePrinters} from "../../print";
|
|
33
|
+
import {
|
|
34
|
+
createDependencyRecipeAccumulator,
|
|
35
|
+
createLockFileEditor,
|
|
36
|
+
DependencyRecipeAccumulator,
|
|
37
|
+
getAllLockFileNames,
|
|
38
|
+
getLockFileName,
|
|
39
|
+
parseLockFileContent,
|
|
40
|
+
runInstallIfNeeded,
|
|
41
|
+
runInstallInTempDir,
|
|
42
|
+
storeInstallResult,
|
|
43
|
+
updateNodeResolutionMarker
|
|
44
|
+
} from "../package-manager";
|
|
45
|
+
import * as path from "path";
|
|
46
|
+
|
|
47
|
+
interface ProjectUpdateInfo {
|
|
48
|
+
packageJsonPath: string;
|
|
49
|
+
originalPackageJson: string;
|
|
50
|
+
dependencyScopes: DependencyScope[];
|
|
51
|
+
packageManager: PackageManager;
|
|
52
|
+
configFiles?: Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Accumulator extends DependencyRecipeAccumulator<ProjectUpdateInfo> {
|
|
56
|
+
originalLockFiles: Map<string, string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class RemoveDependency extends ScanningRecipe<Accumulator> {
|
|
60
|
+
readonly name = "org.openrewrite.javascript.dependencies.remove-dependency";
|
|
61
|
+
readonly displayName = "Remove npm dependency";
|
|
62
|
+
readonly description = "Removes a dependency from `package.json` and updates the lock file by running the package manager.";
|
|
63
|
+
|
|
64
|
+
@Option({
|
|
65
|
+
displayName: "Package name",
|
|
66
|
+
description: "The name of the npm package to remove (e.g., `lodash`, `@types/node`)",
|
|
67
|
+
example: "lodash"
|
|
68
|
+
})
|
|
69
|
+
packageName!: string;
|
|
70
|
+
|
|
71
|
+
@Option({
|
|
72
|
+
displayName: "Scope",
|
|
73
|
+
description: "The dependency scope to remove from. If not specified, the dependency is removed from all scopes where it is found.",
|
|
74
|
+
example: "dependencies",
|
|
75
|
+
required: false
|
|
76
|
+
})
|
|
77
|
+
scope?: DependencyScope;
|
|
78
|
+
|
|
79
|
+
initialValue(_ctx: ExecutionContext): Accumulator {
|
|
80
|
+
return {
|
|
81
|
+
...createDependencyRecipeAccumulator<ProjectUpdateInfo>(),
|
|
82
|
+
originalLockFiles: new Map()
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async scanner(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
87
|
+
const recipe = this;
|
|
88
|
+
const LOCK_FILE_NAMES = getAllLockFileNames();
|
|
89
|
+
|
|
90
|
+
return new class extends TreeVisitor<Tree, ExecutionContext> {
|
|
91
|
+
protected async accept(tree: Tree, ctx: ExecutionContext): Promise<Tree | undefined> {
|
|
92
|
+
if (isJson(tree) && tree.kind === Json.Kind.Document) {
|
|
93
|
+
return this.handleJsonDocument(tree as Json.Document, ctx);
|
|
94
|
+
}
|
|
95
|
+
if (isYaml(tree) && isDocuments(tree)) {
|
|
96
|
+
return this.handleYamlDocument(tree, ctx);
|
|
97
|
+
}
|
|
98
|
+
if (isPlainText(tree)) {
|
|
99
|
+
return this.handlePlainTextDocument(tree as PlainText, ctx);
|
|
100
|
+
}
|
|
101
|
+
return tree;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async handleJsonDocument(doc: Json.Document, _ctx: ExecutionContext): Promise<Json | undefined> {
|
|
105
|
+
const basename = path.basename(doc.sourcePath);
|
|
106
|
+
|
|
107
|
+
if (LOCK_FILE_NAMES.includes(basename)) {
|
|
108
|
+
acc.originalLockFiles.set(doc.sourcePath, await TreePrinters.print(doc));
|
|
109
|
+
return doc;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!doc.sourcePath.endsWith('package.json')) {
|
|
113
|
+
return doc;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const marker = findNodeResolutionResult(doc);
|
|
117
|
+
if (!marker) {
|
|
118
|
+
return doc;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const scopesToCheck = recipe.scope ? [recipe.scope] : allDependencyScopes;
|
|
122
|
+
const foundScopes: DependencyScope[] = [];
|
|
123
|
+
|
|
124
|
+
for (const scope of scopesToCheck) {
|
|
125
|
+
const deps = marker[scope];
|
|
126
|
+
if (deps?.some(d => d.name === recipe.packageName)) {
|
|
127
|
+
foundScopes.push(scope);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (foundScopes.length === 0) {
|
|
132
|
+
return doc;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pm = marker.packageManager ?? PackageManager.Npm;
|
|
136
|
+
|
|
137
|
+
const configFiles: Record<string, string> = {};
|
|
138
|
+
const npmrcContent = serializeNpmrcConfigs(marker.npmrcConfigs);
|
|
139
|
+
if (npmrcContent) {
|
|
140
|
+
configFiles['.npmrc'] = npmrcContent;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
acc.projectsToUpdate.set(doc.sourcePath, {
|
|
144
|
+
packageJsonPath: doc.sourcePath,
|
|
145
|
+
originalPackageJson: await TreePrinters.print(doc),
|
|
146
|
+
dependencyScopes: foundScopes,
|
|
147
|
+
packageManager: pm,
|
|
148
|
+
configFiles: Object.keys(configFiles).length > 0 ? configFiles : undefined
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return doc;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async handleYamlDocument(docs: Yaml.Documents, _ctx: ExecutionContext): Promise<Yaml.Documents | undefined> {
|
|
155
|
+
const basename = path.basename(docs.sourcePath);
|
|
156
|
+
if (LOCK_FILE_NAMES.includes(basename)) {
|
|
157
|
+
acc.originalLockFiles.set(docs.sourcePath, await TreePrinters.print(docs));
|
|
158
|
+
}
|
|
159
|
+
return docs;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async handlePlainTextDocument(text: PlainText, _ctx: ExecutionContext): Promise<PlainText | undefined> {
|
|
163
|
+
const basename = path.basename(text.sourcePath);
|
|
164
|
+
if (LOCK_FILE_NAMES.includes(basename)) {
|
|
165
|
+
acc.originalLockFiles.set(text.sourcePath, await TreePrinters.print(text));
|
|
166
|
+
}
|
|
167
|
+
return text;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async editorWithData(acc: Accumulator): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
173
|
+
const recipe = this;
|
|
174
|
+
|
|
175
|
+
const jsonEditor = new class extends JsonVisitor<ExecutionContext> {
|
|
176
|
+
protected async visitDocument(doc: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
|
|
177
|
+
const sourcePath = doc.sourcePath;
|
|
178
|
+
|
|
179
|
+
if (sourcePath.endsWith('package.json')) {
|
|
180
|
+
const updateInfo = acc.projectsToUpdate.get(sourcePath);
|
|
181
|
+
if (!updateInfo) {
|
|
182
|
+
return doc;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const failureMessage = await runInstallIfNeeded(sourcePath, acc, () =>
|
|
186
|
+
recipe.runPackageManagerInstall(acc, updateInfo, ctx)
|
|
187
|
+
);
|
|
188
|
+
if (failureMessage) {
|
|
189
|
+
return markupWarn(
|
|
190
|
+
doc,
|
|
191
|
+
`Failed to remove ${recipe.packageName}`,
|
|
192
|
+
failureMessage
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const modifiedDoc = removeDependencyFromJson(
|
|
197
|
+
doc,
|
|
198
|
+
recipe.packageName,
|
|
199
|
+
updateInfo.dependencyScopes
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return updateNodeResolutionMarker(modifiedDoc, updateInfo, acc);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const lockFileName = path.basename(sourcePath);
|
|
206
|
+
if (getAllLockFileNames().includes(lockFileName)) {
|
|
207
|
+
const updatedLockContent = acc.updatedLockFiles.get(sourcePath);
|
|
208
|
+
if (updatedLockContent) {
|
|
209
|
+
const parsed = await parseLockFileContent(updatedLockContent, sourcePath, lockFileName) as Json.Document;
|
|
210
|
+
return {
|
|
211
|
+
...doc,
|
|
212
|
+
value: parsed.value,
|
|
213
|
+
eof: parsed.eof
|
|
214
|
+
} as Json.Document;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return doc;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return createLockFileEditor(jsonEditor, acc);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async runPackageManagerInstall(
|
|
226
|
+
acc: Accumulator,
|
|
227
|
+
updateInfo: ProjectUpdateInfo,
|
|
228
|
+
_ctx: ExecutionContext
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
const modifiedPackageJson = this.createModifiedPackageJson(
|
|
231
|
+
updateInfo.originalPackageJson,
|
|
232
|
+
updateInfo.dependencyScopes
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const lockFileName = getLockFileName(updateInfo.packageManager);
|
|
236
|
+
const packageJsonDir = path.dirname(updateInfo.packageJsonPath);
|
|
237
|
+
const lockFilePath = packageJsonDir === '.'
|
|
238
|
+
? lockFileName
|
|
239
|
+
: path.join(packageJsonDir, lockFileName);
|
|
240
|
+
|
|
241
|
+
const originalLockFileContent = acc.originalLockFiles.get(lockFilePath);
|
|
242
|
+
|
|
243
|
+
const result = await runInstallInTempDir(
|
|
244
|
+
updateInfo.packageManager,
|
|
245
|
+
modifiedPackageJson,
|
|
246
|
+
{
|
|
247
|
+
originalLockFileContent,
|
|
248
|
+
configFiles: updateInfo.configFiles
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
storeInstallResult(result, acc, updateInfo, modifiedPackageJson);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private createModifiedPackageJson(
|
|
256
|
+
originalContent: string,
|
|
257
|
+
scopes: DependencyScope[]
|
|
258
|
+
): string {
|
|
259
|
+
const packageJson = JSON.parse(originalContent);
|
|
260
|
+
|
|
261
|
+
for (const scope of scopes) {
|
|
262
|
+
if (packageJson[scope] && packageJson[scope][this.packageName]) {
|
|
263
|
+
delete packageJson[scope][this.packageName];
|
|
264
|
+
|
|
265
|
+
if (Object.keys(packageJson[scope]).length === 0) {
|
|
266
|
+
delete packageJson[scope];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return JSON.stringify(packageJson, null, 2);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function removeDependencyFromJson(
|
|
276
|
+
doc: Json.Document,
|
|
277
|
+
packageName: string,
|
|
278
|
+
scopes: DependencyScope[]
|
|
279
|
+
): Json.Document {
|
|
280
|
+
if (!isObject(doc.value)) {
|
|
281
|
+
return doc;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const targetScopes = new Set<string>(scopes);
|
|
285
|
+
const rootObj = doc.value;
|
|
286
|
+
let changed = false;
|
|
287
|
+
const result: Json.RightPadded<Json>[] = [];
|
|
288
|
+
|
|
289
|
+
for (const rp of rootObj.members) {
|
|
290
|
+
const member = rp.element as Json.Member;
|
|
291
|
+
const keyName = getMemberKeyName(member);
|
|
292
|
+
|
|
293
|
+
if (!keyName || !targetScopes.has(keyName) || !isObject(member.value)) {
|
|
294
|
+
result.push(rp);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const scopeObj = member.value;
|
|
299
|
+
const filtered = scopeObj.members.filter(depRp =>
|
|
300
|
+
getMemberKeyName(depRp.element as Json.Member) !== packageName
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (filtered.length === scopeObj.members.length) {
|
|
304
|
+
result.push(rp);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
changed = true;
|
|
309
|
+
|
|
310
|
+
if (filtered.length === 0) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const lastOriginalAfter = scopeObj.members[scopeObj.members.length - 1].after;
|
|
315
|
+
filtered[filtered.length - 1] = {
|
|
316
|
+
...filtered[filtered.length - 1],
|
|
317
|
+
after: lastOriginalAfter
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
result.push({
|
|
321
|
+
...rp,
|
|
322
|
+
element: {
|
|
323
|
+
...member,
|
|
324
|
+
value: {...scopeObj, members: filtered} as Json.Object
|
|
325
|
+
}
|
|
326
|
+
} as Json.RightPadded<Json>);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!changed) {
|
|
330
|
+
return doc;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (result.length > 0 && result.length < rootObj.members.length) {
|
|
334
|
+
const originalLastAfter = rootObj.members[rootObj.members.length - 1].after;
|
|
335
|
+
result[result.length - 1] = {
|
|
336
|
+
...result[result.length - 1],
|
|
337
|
+
after: originalLastAfter
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
...doc,
|
|
343
|
+
value: {...rootObj, members: result} as Json.Object
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {JavaScriptVisitor} from "./visitor";
|
|
2
2
|
import {J} from "../java";
|
|
3
3
|
import {JS, JSX} from "./tree";
|
|
4
|
-
import {mapAsync} from "../util";
|
|
4
|
+
import {mapAsync, updateIfChanged} from "../util";
|
|
5
5
|
import {ElementRemovalFormatter} from "../java";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -146,7 +146,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
146
146
|
return this.produceJavaScript(compilationUnit, p, async draft => {
|
|
147
147
|
const formatter = new ElementRemovalFormatter<J>(true); // Preserve file headers from first import
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
const newStatements = await mapAsync(compilationUnit.statements, async (stmt) => {
|
|
150
150
|
const statement = stmt.element;
|
|
151
151
|
|
|
152
152
|
// Handle ES6 imports
|
|
@@ -159,7 +159,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const finalResult = formatter.processKept(result) as JS.Import;
|
|
162
|
-
return
|
|
162
|
+
return updateIfChanged(stmt, {element: finalResult});
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// Handle CommonJS require statements
|
|
@@ -174,7 +174,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
const finalResult = formatter.processKept(result) as J.VariableDeclarations;
|
|
177
|
-
return
|
|
177
|
+
return updateIfChanged(stmt, {element: finalResult});
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// Handle JS.ScopedVariableDeclarations (multi-variable var/let/const)
|
|
@@ -194,7 +194,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
194
194
|
varFormatter.markRemoved(varDecl);
|
|
195
195
|
} else {
|
|
196
196
|
const formattedVarDecl = varFormatter.processKept(result as J.VariableDeclarations);
|
|
197
|
-
filteredVariables.push(
|
|
197
|
+
filteredVariables.push(updateIfChanged(v, {element: formattedVarDecl}));
|
|
198
198
|
}
|
|
199
199
|
} else {
|
|
200
200
|
filteredVariables.push(v);
|
|
@@ -210,20 +210,21 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
|
|
|
210
210
|
? formatter.processKept({...scopedVarDecl, variables: filteredVariables})
|
|
211
211
|
: formatter.processKept(statement);
|
|
212
212
|
|
|
213
|
-
return
|
|
213
|
+
return updateIfChanged(stmt, {element: finalElement});
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
// For any other statement type, apply prefix from removed elements
|
|
217
217
|
if (statement) {
|
|
218
218
|
const finalStatement = formatter.processKept(statement);
|
|
219
|
-
return
|
|
219
|
+
return updateIfChanged(stmt, {element: finalStatement});
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
return stmt;
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
draft.statements = newStatements.some(s => s === undefined)
|
|
226
|
+
? newStatements.filter(s => s !== undefined)
|
|
227
|
+
: newStatements;
|
|
227
228
|
draft.eof = await this.visitSpace(compilationUnit.eof, p);
|
|
228
229
|
});
|
|
229
230
|
}
|
|
@@ -58,6 +58,8 @@ export class PrepareRecipe {
|
|
|
58
58
|
|
|
59
59
|
preparedRecipes.set(id, recipe);
|
|
60
60
|
|
|
61
|
+
await this.installSubRecipes(recipe, marketplace);
|
|
62
|
+
|
|
61
63
|
const result = {
|
|
62
64
|
id: id,
|
|
63
65
|
descriptor: await recipe.descriptor(),
|
|
@@ -73,6 +75,15 @@ export class PrepareRecipe {
|
|
|
73
75
|
);
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
private static async installSubRecipes(recipe: Recipe, marketplace: RecipeMarketplace) {
|
|
79
|
+
for (const subRecipe of await recipe.recipeList()) {
|
|
80
|
+
if (!marketplace.findRecipe(subRecipe.name)) {
|
|
81
|
+
await marketplace.install(subRecipe.constructor as any, []);
|
|
82
|
+
await this.installSubRecipes(subRecipe, marketplace);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
/**
|
|
77
88
|
* For preconditions that can be evaluated on the remote peer, let the remote peer
|
|
78
89
|
* evaluate them and know that we will only have to do the visit work if the
|
|
@@ -110,22 +121,22 @@ export class PrepareRecipe {
|
|
|
110
121
|
}
|
|
111
122
|
)
|
|
112
123
|
}
|
|
113
|
-
this.visitorTypePrecondition(preconditions, visitor.v);
|
|
124
|
+
await this.visitorTypePrecondition(preconditions, visitor.v);
|
|
114
125
|
} else {
|
|
115
|
-
this.visitorTypePrecondition(preconditions, visitor!);
|
|
126
|
+
await this.visitorTypePrecondition(preconditions, visitor!);
|
|
116
127
|
}
|
|
117
128
|
return recipe;
|
|
118
129
|
}
|
|
119
130
|
|
|
120
|
-
private static visitorTypePrecondition(preconditions: Precondition[], v: TreeVisitor<any, ExecutionContext>): Precondition[] {
|
|
131
|
+
private static async visitorTypePrecondition(preconditions: Precondition[], v: TreeVisitor<any, ExecutionContext>): Promise<Precondition[]> {
|
|
121
132
|
let treeType: string | undefined;
|
|
122
133
|
|
|
123
|
-
// Use
|
|
124
|
-
const {JsonVisitor} =
|
|
125
|
-
const {JavaScriptVisitor} =
|
|
126
|
-
const {JavaVisitor} =
|
|
127
|
-
const {PlainTextVisitor} =
|
|
128
|
-
const {YamlVisitor} =
|
|
134
|
+
// Use dynamic import to defer loading and avoid circular dependencies
|
|
135
|
+
const {JsonVisitor} = await import("../../json/index.js");
|
|
136
|
+
const {JavaScriptVisitor} = await import("../../javascript/index.js");
|
|
137
|
+
const {JavaVisitor} = await import("../../java/index.js");
|
|
138
|
+
const {PlainTextVisitor} = await import("../../text/index.js");
|
|
139
|
+
const {YamlVisitor} = await import("../../yaml/index.js");
|
|
129
140
|
|
|
130
141
|
if (v instanceof JsonVisitor) {
|
|
131
142
|
treeType = "org.openrewrite.json.tree.Json";
|
package/src/test/rewrite-test.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import {expect} from "vitest";
|
|
16
17
|
import {Recipe} from "../recipe";
|
|
17
18
|
import {ExecutionContext} from "../execution";
|
|
18
19
|
import {noopVisitor, TreeVisitor} from "../visitor";
|
|
@@ -40,6 +41,7 @@ export interface SourceSpec<T extends SourceFile> {
|
|
|
40
41
|
|
|
41
42
|
export class RecipeSpec {
|
|
42
43
|
checkParsePrintIdempotence: boolean = true
|
|
44
|
+
allowEmptyDiff: boolean = false
|
|
43
45
|
|
|
44
46
|
recipe: Recipe = new NoopRecipe()
|
|
45
47
|
|
|
@@ -162,9 +164,21 @@ export class RecipeSpec {
|
|
|
162
164
|
|
|
163
165
|
if (!spec.after) {
|
|
164
166
|
if (after && after !== result?.before) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
const actual = await TreePrinters.print(after);
|
|
168
|
+
const expected = dedent(spec.before!);
|
|
169
|
+
if (actual === expected) {
|
|
170
|
+
if (!this.allowEmptyDiff) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
"An empty diff was generated. The recipe incorrectly " +
|
|
173
|
+
"changed the AST without changing the printed output."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
throw new Error(
|
|
178
|
+
"Expected no change but recipe modified the file.\n" +
|
|
179
|
+
`Expected:\n${expected}\n\nActual:\n${actual}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
168
182
|
}
|
|
169
183
|
if (spec.afterRecipe) {
|
|
170
184
|
await spec.afterRecipe(matchingSpec![1]);
|
package/src/visitor.ts
CHANGED
|
@@ -145,13 +145,18 @@ export abstract class TreeVisitor<T extends Tree, P> {
|
|
|
145
145
|
const newMarkers = await this.visitMarkers(before.markers, p);
|
|
146
146
|
|
|
147
147
|
if (recipe) {
|
|
148
|
-
// Remove markers before Mutative drafting to avoid cycles, then restore after
|
|
148
|
+
// Remove markers before Mutative drafting to avoid cycles, then restore after.
|
|
149
|
+
// The spread cost is paid unconditionally, but it enables the identity check below.
|
|
149
150
|
const withoutMarkers = { ...before, markers: emptyMarkers };
|
|
150
151
|
const result = await produceAsync(withoutMarkers, recipe);
|
|
151
152
|
if (result === undefined) {
|
|
152
153
|
return undefined;
|
|
153
154
|
}
|
|
154
|
-
//
|
|
155
|
+
// Mutative's produceAsync returns the same reference when no draft mutations occurred
|
|
156
|
+
// (structural sharing), so reference equality is a reliable no-change check.
|
|
157
|
+
if (result === withoutMarkers && newMarkers === before.markers) {
|
|
158
|
+
return before;
|
|
159
|
+
}
|
|
155
160
|
return { ...result, markers: newMarkers } as T;
|
|
156
161
|
}
|
|
157
162
|
|