@openrewrite/rewrite 8.68.0-20251204-054843 → 8.68.0-20251204-145030
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/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/javascript/index.d.ts +3 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +3 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/package-json-parser.d.ts +0 -5
- package/dist/javascript/package-json-parser.d.ts.map +1 -1
- package/dist/javascript/package-json-parser.js +13 -25
- package/dist/javascript/package-json-parser.js.map +1 -1
- package/dist/javascript/package-manager.d.ts +131 -0
- package/dist/javascript/package-manager.d.ts.map +1 -0
- package/dist/javascript/package-manager.js +372 -0
- package/dist/javascript/package-manager.js.map +1 -0
- package/dist/javascript/recipes/index.d.ts +2 -0
- package/dist/javascript/recipes/index.d.ts.map +1 -0
- package/dist/javascript/recipes/index.js +33 -0
- package/dist/javascript/recipes/index.js.map +1 -0
- package/dist/javascript/recipes/upgrade-dependency-version.d.ts +105 -0
- package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -0
- package/dist/javascript/recipes/upgrade-dependency-version.js +493 -0
- package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -0
- package/dist/javascript/search/find-dependency.d.ts +32 -0
- package/dist/javascript/search/find-dependency.d.ts.map +1 -0
- package/dist/javascript/search/find-dependency.js +312 -0
- package/dist/javascript/search/find-dependency.js.map +1 -0
- package/dist/javascript/search/index.d.ts +1 -0
- package/dist/javascript/search/index.d.ts.map +1 -1
- package/dist/javascript/search/index.js +1 -0
- package/dist/javascript/search/index.js.map +1 -1
- package/dist/json/print.js +1 -1
- package/dist/json/print.js.map +1 -1
- package/dist/markers.d.ts +67 -0
- package/dist/markers.d.ts.map +1 -1
- package/dist/markers.js +101 -0
- package/dist/markers.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +0 -1
- package/dist/print.js.map +1 -1
- package/dist/recipe.js +3 -3
- package/dist/recipe.js.map +1 -1
- package/dist/rpc/index.js +72 -0
- package/dist/rpc/index.js.map +1 -1
- package/dist/rpc/request/generate.js +1 -1
- package/dist/rpc/request/generate.js.map +1 -1
- package/dist/rpc/request/get-languages.d.ts.map +1 -1
- package/dist/rpc/request/get-languages.js +2 -1
- package/dist/rpc/request/get-languages.js.map +1 -1
- package/dist/rpc/request/visit.d.ts.map +1 -1
- package/dist/rpc/request/visit.js +27 -0
- package/dist/rpc/request/visit.js.map +1 -1
- package/dist/run.js +2 -2
- package/dist/run.js.map +1 -1
- package/dist/test/rewrite-test.js +1 -1
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/javascript/index.ts +3 -0
- package/src/javascript/package-json-parser.ts +14 -33
- package/src/javascript/package-manager.ts +428 -0
- package/src/javascript/recipes/index.ts +17 -0
- package/src/javascript/recipes/upgrade-dependency-version.ts +586 -0
- package/src/javascript/search/find-dependency.ts +303 -0
- package/src/javascript/search/index.ts +1 -0
- package/src/json/print.ts +1 -1
- package/src/markers.ts +146 -0
- package/src/print.ts +0 -1
- package/src/recipe.ts +3 -3
- package/src/rpc/index.ts +65 -1
- package/src/rpc/request/generate.ts +1 -1
- package/src/rpc/request/get-languages.ts +2 -1
- package/src/rpc/request/visit.ts +32 -1
- package/src/run.ts +2 -2
- package/src/test/rewrite-test.ts +1 -1
|
@@ -0,0 +1,303 @@
|
|
|
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/moderate-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, Recipe} from "../../recipe";
|
|
18
|
+
import {ExecutionContext} from "../../execution";
|
|
19
|
+
import {TreeVisitor} from "../../visitor";
|
|
20
|
+
import {Json, JsonVisitor} from "../../json";
|
|
21
|
+
import {foundSearchResult} from "../../markers";
|
|
22
|
+
import {
|
|
23
|
+
Dependency,
|
|
24
|
+
findNodeResolutionResult,
|
|
25
|
+
NodeResolutionResult,
|
|
26
|
+
ResolvedDependency
|
|
27
|
+
} from "../node-resolution-result";
|
|
28
|
+
import * as semver from "semver";
|
|
29
|
+
import * as picomatch from "picomatch";
|
|
30
|
+
|
|
31
|
+
/** Dependency section names in package.json */
|
|
32
|
+
const DEPENDENCY_SECTIONS = new Set([
|
|
33
|
+
'dependencies',
|
|
34
|
+
'devDependencies',
|
|
35
|
+
'peerDependencies',
|
|
36
|
+
'optionalDependencies'
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Finds npm/Node.js dependencies declared in package.json.
|
|
41
|
+
* This recipe is commonly used as a precondition to limit the scope of other recipes
|
|
42
|
+
* to projects that use a specific dependency.
|
|
43
|
+
*
|
|
44
|
+
* The search result marker is placed on the specific dependency entry in package.json,
|
|
45
|
+
* allowing users to see exactly where the dependency is declared.
|
|
46
|
+
*
|
|
47
|
+
* When `onlyDirect` is false, this recipe also marks direct dependencies that
|
|
48
|
+
* transitively depend on the target package, helping answer "which of my dependencies
|
|
49
|
+
* brings in package X?".
|
|
50
|
+
*/
|
|
51
|
+
export interface FindDependencyOptions {
|
|
52
|
+
packageName: string;
|
|
53
|
+
version?: string;
|
|
54
|
+
onlyDirect?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class FindDependency extends Recipe {
|
|
58
|
+
readonly name = "org.openrewrite.javascript.dependencies.find-dependency";
|
|
59
|
+
readonly displayName = "Find Node.js dependency";
|
|
60
|
+
readonly description = "Finds dependencies in a project's `package.json`. " +
|
|
61
|
+
"Can find both direct dependencies and dependencies that transitively include the target package. " +
|
|
62
|
+
"This recipe is commonly used as a precondition for other recipes.";
|
|
63
|
+
|
|
64
|
+
@Option({
|
|
65
|
+
displayName: "Package name",
|
|
66
|
+
description: "The name of the npm package to find. Supports glob patterns.",
|
|
67
|
+
example: "lodash"
|
|
68
|
+
})
|
|
69
|
+
packageName!: string;
|
|
70
|
+
|
|
71
|
+
@Option({
|
|
72
|
+
displayName: "Version",
|
|
73
|
+
description: "An exact version number or semver selector used to select the version number. " +
|
|
74
|
+
"Leave empty to match any version.",
|
|
75
|
+
example: "^18.0.0",
|
|
76
|
+
required: false
|
|
77
|
+
})
|
|
78
|
+
version?: string;
|
|
79
|
+
|
|
80
|
+
@Option({
|
|
81
|
+
displayName: "Only direct dependencies",
|
|
82
|
+
description: "If true (default), only matches dependencies that directly match the package name. " +
|
|
83
|
+
"If false, also marks direct dependencies that have the target package as a transitive dependency.",
|
|
84
|
+
example: "true",
|
|
85
|
+
required: false
|
|
86
|
+
})
|
|
87
|
+
onlyDirect?: boolean;
|
|
88
|
+
|
|
89
|
+
constructor(options: FindDependencyOptions) {
|
|
90
|
+
super(options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override instanceName(): string {
|
|
94
|
+
return `${this.displayName} \`${this.packageName}${this.version ? '@' + this.version : ''}\``;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
98
|
+
const packageName = this.packageName;
|
|
99
|
+
const version = this.version;
|
|
100
|
+
// Default to true if not specified (only search direct dependencies)
|
|
101
|
+
const onlyDirect = this.onlyDirect ?? true;
|
|
102
|
+
|
|
103
|
+
// Create a picomatch matcher for the package name pattern
|
|
104
|
+
// For patterns without '/', use { contains: true } so that '*jest*' matches '@types/jest'
|
|
105
|
+
// (by default, '*' doesn't match '/' in glob patterns, but for package names this is more intuitive)
|
|
106
|
+
const matchOptions = packageName.includes('/') ? {} : { contains: true };
|
|
107
|
+
const matcher: picomatch.Matcher = picomatch.default
|
|
108
|
+
? picomatch.default(packageName, matchOptions)
|
|
109
|
+
: (picomatch as any)(packageName, matchOptions);
|
|
110
|
+
|
|
111
|
+
return new class extends JsonVisitor<ExecutionContext> {
|
|
112
|
+
private resolution: NodeResolutionResult | undefined;
|
|
113
|
+
private isPackageJson: boolean = false;
|
|
114
|
+
|
|
115
|
+
protected override async visitDocument(document: Json.Document, ctx: ExecutionContext): Promise<Json | undefined> {
|
|
116
|
+
// Only process package.json files, not package-lock.json or other JSON files
|
|
117
|
+
const sourcePath = document.sourcePath;
|
|
118
|
+
this.isPackageJson = sourcePath.endsWith('package.json');
|
|
119
|
+
|
|
120
|
+
if (this.isPackageJson) {
|
|
121
|
+
this.resolution = findNodeResolutionResult(document);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return this.isPackageJson && this.resolution ? super.visitDocument(document, ctx) : document;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
protected override async visitMember(member: Json.Member, ctx: ExecutionContext): Promise<Json | undefined> {
|
|
128
|
+
// Check if we're inside a dependency section
|
|
129
|
+
const parentSection = this.getParentDependencySection();
|
|
130
|
+
if (!parentSection) {
|
|
131
|
+
return super.visitMember(member, ctx);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Get the package name from the member key
|
|
135
|
+
const depName = this.getMemberKeyName(member);
|
|
136
|
+
if (!depName) {
|
|
137
|
+
return super.visitMember(member, ctx);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Find the dependency in the resolution result
|
|
141
|
+
const dep = this.findDependencyByName(depName, parentSection);
|
|
142
|
+
if (!dep) {
|
|
143
|
+
return super.visitMember(member, ctx);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if this dependency matches directly
|
|
147
|
+
if (matcher(depName) && versionMatches(dep, version)) {
|
|
148
|
+
return this.markDependency(member, ctx);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// If not only direct, check if this dependency has the target as a transitive dependency
|
|
152
|
+
if (!onlyDirect && dep.resolved) {
|
|
153
|
+
if (hasTransitiveDependency(dep.resolved, matcher, version, new Set())) {
|
|
154
|
+
return this.markDependency(member, ctx);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return super.visitMember(member, ctx);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Marks the dependency key with a search result marker.
|
|
163
|
+
*/
|
|
164
|
+
private async markDependency(member: Json.Member, ctx: ExecutionContext): Promise<Json.Member> {
|
|
165
|
+
const visitedMember = await super.visitMember(member, ctx) as Json.Member;
|
|
166
|
+
const markedKey = foundSearchResult(visitedMember.key.element);
|
|
167
|
+
return {
|
|
168
|
+
...visitedMember,
|
|
169
|
+
key: {
|
|
170
|
+
...visitedMember.key,
|
|
171
|
+
element: markedKey
|
|
172
|
+
}
|
|
173
|
+
} as Json.Member;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Checks if the current member's parent is a dependency section object.
|
|
178
|
+
* Returns the section name if so, undefined otherwise.
|
|
179
|
+
*/
|
|
180
|
+
private getParentDependencySection(): string | undefined {
|
|
181
|
+
// Walk up the cursor to find the parent member that contains this dependency
|
|
182
|
+
// Structure: Document > Object > Member("dependencies") > Object > Member("lodash")
|
|
183
|
+
let cursor = this.cursor.parent;
|
|
184
|
+
while (cursor) {
|
|
185
|
+
const tree = cursor.value;
|
|
186
|
+
if (tree && typeof tree === 'object' && 'kind' in tree) {
|
|
187
|
+
if (tree.kind === Json.Kind.Member) {
|
|
188
|
+
const memberKey = this.getMemberKeyName(tree as Json.Member);
|
|
189
|
+
if (memberKey && DEPENDENCY_SECTIONS.has(memberKey)) {
|
|
190
|
+
return memberKey;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
cursor = cursor.parent;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Extracts the key name from a Json.Member
|
|
201
|
+
*/
|
|
202
|
+
private getMemberKeyName(member: Json.Member): string | undefined {
|
|
203
|
+
const key = member.key.element;
|
|
204
|
+
if (key.kind === Json.Kind.Literal) {
|
|
205
|
+
// Remove quotes from string literal
|
|
206
|
+
const source = (key as Json.Literal).source;
|
|
207
|
+
if (source.startsWith('"') && source.endsWith('"')) {
|
|
208
|
+
return source.slice(1, -1);
|
|
209
|
+
}
|
|
210
|
+
return source;
|
|
211
|
+
} else if (key.kind === Json.Kind.Identifier) {
|
|
212
|
+
return (key as Json.Identifier).name;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Finds a dependency by name in the appropriate section of the resolution result.
|
|
219
|
+
*/
|
|
220
|
+
private findDependencyByName(name: string, section: string): Dependency | undefined {
|
|
221
|
+
if (!this.resolution) return undefined;
|
|
222
|
+
|
|
223
|
+
let deps: Dependency[] | undefined;
|
|
224
|
+
switch (section) {
|
|
225
|
+
case 'dependencies':
|
|
226
|
+
deps = this.resolution.dependencies;
|
|
227
|
+
break;
|
|
228
|
+
case 'devDependencies':
|
|
229
|
+
deps = this.resolution.devDependencies;
|
|
230
|
+
break;
|
|
231
|
+
case 'peerDependencies':
|
|
232
|
+
deps = this.resolution.peerDependencies;
|
|
233
|
+
break;
|
|
234
|
+
case 'optionalDependencies':
|
|
235
|
+
deps = this.resolution.optionalDependencies;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return deps?.find(d => d.name === name);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Recursively checks if a resolved dependency has the target package as a transitive dependency.
|
|
247
|
+
*/
|
|
248
|
+
function hasTransitiveDependency(
|
|
249
|
+
resolved: ResolvedDependency,
|
|
250
|
+
matcher: picomatch.Matcher,
|
|
251
|
+
version: string | undefined,
|
|
252
|
+
visited: Set<string>
|
|
253
|
+
): boolean {
|
|
254
|
+
// Avoid cycles
|
|
255
|
+
const key = `${resolved.name}@${resolved.version}`;
|
|
256
|
+
if (visited.has(key)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
visited.add(key);
|
|
260
|
+
|
|
261
|
+
// Check all dependency types
|
|
262
|
+
const allDeps = [
|
|
263
|
+
...(resolved.dependencies || []),
|
|
264
|
+
...(resolved.devDependencies || []),
|
|
265
|
+
...(resolved.peerDependencies || []),
|
|
266
|
+
...(resolved.optionalDependencies || [])
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
for (const dep of allDeps) {
|
|
270
|
+
// Check if this dependency matches the target
|
|
271
|
+
if (matcher(dep.name) && versionMatches(dep, version)) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Recursively check transitive dependencies
|
|
276
|
+
if (dep.resolved && hasTransitiveDependency(dep.resolved, matcher, version, visited)) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function versionMatches(dep: Dependency, version: string | undefined): boolean {
|
|
285
|
+
if (!version) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
const resolved = dep.resolved;
|
|
289
|
+
if (!resolved) {
|
|
290
|
+
// If no resolved version available, we can't validate the version
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return versionMatchesResolved(resolved, version);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function versionMatchesResolved(resolved: ResolvedDependency, version: string | undefined): boolean {
|
|
297
|
+
if (!version) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
const actualVersion = resolved.version;
|
|
301
|
+
// Use semver.satisfies to check if the actual version matches the constraint
|
|
302
|
+
return semver.satisfies(actualVersion, version);
|
|
303
|
+
}
|
package/src/json/print.ts
CHANGED
|
@@ -125,7 +125,7 @@ class JsonPrinter extends JsonVisitor<PrintOutputCapture> {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
private jsonMarkerWrapper = (out: string): string => `/*~~${out}${out ? "~~" : ""}
|
|
128
|
+
private jsonMarkerWrapper = (out: string): string => `/*~~${out}${out ? "~~" : ""}>*/`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
TreePrinters.register(Json.Kind.Document, () => new JsonPrinter());
|
package/src/markers.ts
CHANGED
|
@@ -22,6 +22,12 @@ export const MarkersKind = {
|
|
|
22
22
|
SearchResult: "org.openrewrite.marker.SearchResult",
|
|
23
23
|
ParseExceptionResult: "org.openrewrite.ParseExceptionResult",
|
|
24
24
|
|
|
25
|
+
// Markup markers for errors, warnings, info, and debug messages
|
|
26
|
+
MarkupError: "org.openrewrite.marker.Markup$Error",
|
|
27
|
+
MarkupWarn: "org.openrewrite.marker.Markup$Warn",
|
|
28
|
+
MarkupInfo: "org.openrewrite.marker.Markup$Info",
|
|
29
|
+
MarkupDebug: "org.openrewrite.marker.Markup$Debug",
|
|
30
|
+
|
|
25
31
|
/**
|
|
26
32
|
* A generic marker that is sent/received as a bare map because the type hasn't been
|
|
27
33
|
* defined in both Java and JavaScript.
|
|
@@ -65,6 +71,60 @@ export function findMarker<T extends Marker>(
|
|
|
65
71
|
);
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Replaces a marker in a Markers collection with a new marker.
|
|
76
|
+
* If the old marker is not found, the new marker is added.
|
|
77
|
+
*
|
|
78
|
+
* @param markers The markers collection to update
|
|
79
|
+
* @param oldMarker The marker to replace (matched by id)
|
|
80
|
+
* @param newMarker The new marker to insert
|
|
81
|
+
* @returns A new Markers object with the replacement applied
|
|
82
|
+
*/
|
|
83
|
+
export function replaceMarker(markers: Markers, oldMarker: Marker, newMarker: Marker): Markers {
|
|
84
|
+
const newMarkers = markers.markers.map(m =>
|
|
85
|
+
m.id === oldMarker.id ? newMarker : m
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// If old marker wasn't found, add the new one
|
|
89
|
+
if (!markers.markers.some(m => m.id === oldMarker.id)) {
|
|
90
|
+
newMarkers.push(newMarker);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...markers,
|
|
95
|
+
markers: newMarkers
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Replaces the first marker with the same kind as the new marker, or adds it if not found.
|
|
101
|
+
* This is useful when there's typically only one marker of each kind.
|
|
102
|
+
*
|
|
103
|
+
* @param markers The markers collection to update
|
|
104
|
+
* @param newMarker The new marker to insert (its kind is used to find the marker to replace)
|
|
105
|
+
* @returns A new Markers object with the replacement applied
|
|
106
|
+
*/
|
|
107
|
+
export function replaceMarkerByKind(markers: Markers, newMarker: Marker): Markers {
|
|
108
|
+
let found = false;
|
|
109
|
+
const newMarkers = markers.markers.map(m => {
|
|
110
|
+
if (!found && m.kind === newMarker.kind) {
|
|
111
|
+
found = true;
|
|
112
|
+
return newMarker;
|
|
113
|
+
}
|
|
114
|
+
return m;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// If marker with kind wasn't found, add the new one
|
|
118
|
+
if (!found) {
|
|
119
|
+
newMarkers.push(newMarker);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
...markers,
|
|
124
|
+
markers: newMarkers
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
68
128
|
export const emptyMarkers: Markers = asRef({
|
|
69
129
|
kind: MarkersKind.Markers,
|
|
70
130
|
id: randomId(),
|
|
@@ -101,3 +161,89 @@ export interface ParseExceptionResult extends Marker {
|
|
|
101
161
|
readonly message: string
|
|
102
162
|
readonly treeType?: string;
|
|
103
163
|
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Base interface for Markup markers that attach messages to AST nodes.
|
|
167
|
+
* Used for errors, warnings, info, and debug messages.
|
|
168
|
+
*/
|
|
169
|
+
export interface Markup extends Marker {
|
|
170
|
+
readonly message: string;
|
|
171
|
+
readonly detail?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface MarkupError extends Markup {
|
|
175
|
+
readonly kind: typeof MarkersKind.MarkupError;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface MarkupWarn extends Markup {
|
|
179
|
+
readonly kind: typeof MarkersKind.MarkupWarn;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface MarkupInfo extends Markup {
|
|
183
|
+
readonly kind: typeof MarkersKind.MarkupInfo;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface MarkupDebug extends Markup {
|
|
187
|
+
readonly kind: typeof MarkersKind.MarkupDebug;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Attaches an error marker to a tree node.
|
|
192
|
+
*/
|
|
193
|
+
export function markupError<T extends { markers: Markers }>(t: T, message: string, detail?: string): T {
|
|
194
|
+
return addMarkup(t, {
|
|
195
|
+
kind: MarkersKind.MarkupError,
|
|
196
|
+
id: randomId(),
|
|
197
|
+
message,
|
|
198
|
+
detail
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Attaches a warning marker to a tree node.
|
|
204
|
+
*/
|
|
205
|
+
export function markupWarn<T extends { markers: Markers }>(t: T, message: string, detail?: string): T {
|
|
206
|
+
return addMarkup(t, {
|
|
207
|
+
kind: MarkersKind.MarkupWarn,
|
|
208
|
+
id: randomId(),
|
|
209
|
+
message,
|
|
210
|
+
detail
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Attaches an info marker to a tree node.
|
|
216
|
+
*/
|
|
217
|
+
export function markupInfo<T extends { markers: Markers }>(t: T, message: string, detail?: string): T {
|
|
218
|
+
return addMarkup(t, {
|
|
219
|
+
kind: MarkersKind.MarkupInfo,
|
|
220
|
+
id: randomId(),
|
|
221
|
+
message,
|
|
222
|
+
detail
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Attaches a debug marker to a tree node.
|
|
228
|
+
*/
|
|
229
|
+
export function markupDebug<T extends { markers: Markers }>(t: T, message: string, detail?: string): T {
|
|
230
|
+
return addMarkup(t, {
|
|
231
|
+
kind: MarkersKind.MarkupDebug,
|
|
232
|
+
id: randomId(),
|
|
233
|
+
message,
|
|
234
|
+
detail
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Helper to add a markup marker to a tree node.
|
|
240
|
+
*/
|
|
241
|
+
function addMarkup<T extends { markers: Markers }>(t: T, markup: Markup): T {
|
|
242
|
+
return {
|
|
243
|
+
...t,
|
|
244
|
+
markers: {
|
|
245
|
+
...t.markers,
|
|
246
|
+
markers: [...t.markers.markers, markup]
|
|
247
|
+
}
|
|
248
|
+
} as T;
|
|
249
|
+
}
|
package/src/print.ts
CHANGED
|
@@ -35,7 +35,6 @@ export namespace MarkerPrinter {
|
|
|
35
35
|
let searchResult = marker as SearchResult;
|
|
36
36
|
return commentWrapper(searchResult.description == null ? "" : "(" + searchResult.description + ")");
|
|
37
37
|
} else if (marker.kind.startsWith("org.openrewrite.marker.Markup$")) {
|
|
38
|
-
// TODO add markup marker types
|
|
39
38
|
return commentWrapper("(" + (marker as any).message + ")");
|
|
40
39
|
}
|
|
41
40
|
return "";
|
package/src/recipe.ts
CHANGED
|
@@ -171,12 +171,12 @@ export abstract class ScanningRecipe<P> extends Recipe {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
async visit<R extends Tree>(tree: Tree, ctx: ExecutionContext, parent?: Cursor): Promise<R | undefined> {
|
|
174
|
-
return (await this.delegateForCtx(ctx)).visit(tree, ctx, parent);
|
|
174
|
+
return (await this.delegateForCtx(ctx, parent)).visit(tree, ctx, parent);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
private async delegateForCtx(ctx: ExecutionContext) {
|
|
177
|
+
private async delegateForCtx(ctx: ExecutionContext, parent?: Cursor) {
|
|
178
178
|
if (!this.delegate) {
|
|
179
|
-
this.delegate = await editorWithContext(this.cursor, ctx);
|
|
179
|
+
this.delegate = await editorWithContext(parent ?? this.cursor, ctx);
|
|
180
180
|
}
|
|
181
181
|
return this.delegate;
|
|
182
182
|
}
|
package/src/rpc/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import {Checksum, FileAttributes, TreeKind} from "../tree";
|
|
17
17
|
import {RpcCodecs, RpcReceiveQueue, RpcSendQueue} from "./queue";
|
|
18
18
|
import {createDraft, finishDraft} from "immer";
|
|
19
|
-
import {Markers, MarkersKind, SearchResult} from "../markers";
|
|
19
|
+
import {Markers, MarkersKind, SearchResult, MarkupError, MarkupWarn, MarkupInfo, MarkupDebug} from "../markers";
|
|
20
20
|
|
|
21
21
|
export * from "./queue"
|
|
22
22
|
export * from "../reference"
|
|
@@ -88,3 +88,67 @@ RpcCodecs.registerCodec(MarkersKind.SearchResult, {
|
|
|
88
88
|
await q.getAndSend(after, a => a.description);
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
RpcCodecs.registerCodec(MarkersKind.MarkupError, {
|
|
93
|
+
async rpcReceive(before: MarkupError, q: RpcReceiveQueue): Promise<MarkupError> {
|
|
94
|
+
const draft = createDraft(before);
|
|
95
|
+
draft.id = await q.receive(before.id);
|
|
96
|
+
draft.message = await q.receive(before.message);
|
|
97
|
+
draft.detail = await q.receive(before.detail);
|
|
98
|
+
return finishDraft(draft);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async rpcSend(after: MarkupError, q: RpcSendQueue): Promise<void> {
|
|
102
|
+
await q.getAndSend(after, a => a.id);
|
|
103
|
+
await q.getAndSend(after, a => a.message);
|
|
104
|
+
await q.getAndSend(after, a => a.detail);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
RpcCodecs.registerCodec(MarkersKind.MarkupWarn, {
|
|
109
|
+
async rpcReceive(before: MarkupWarn, q: RpcReceiveQueue): Promise<MarkupWarn> {
|
|
110
|
+
const draft = createDraft(before);
|
|
111
|
+
draft.id = await q.receive(before.id);
|
|
112
|
+
draft.message = await q.receive(before.message);
|
|
113
|
+
draft.detail = await q.receive(before.detail);
|
|
114
|
+
return finishDraft(draft);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async rpcSend(after: MarkupWarn, q: RpcSendQueue): Promise<void> {
|
|
118
|
+
await q.getAndSend(after, a => a.id);
|
|
119
|
+
await q.getAndSend(after, a => a.message);
|
|
120
|
+
await q.getAndSend(after, a => a.detail);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
RpcCodecs.registerCodec(MarkersKind.MarkupInfo, {
|
|
125
|
+
async rpcReceive(before: MarkupInfo, q: RpcReceiveQueue): Promise<MarkupInfo> {
|
|
126
|
+
const draft = createDraft(before);
|
|
127
|
+
draft.id = await q.receive(before.id);
|
|
128
|
+
draft.message = await q.receive(before.message);
|
|
129
|
+
draft.detail = await q.receive(before.detail);
|
|
130
|
+
return finishDraft(draft);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async rpcSend(after: MarkupInfo, q: RpcSendQueue): Promise<void> {
|
|
134
|
+
await q.getAndSend(after, a => a.id);
|
|
135
|
+
await q.getAndSend(after, a => a.message);
|
|
136
|
+
await q.getAndSend(after, a => a.detail);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
RpcCodecs.registerCodec(MarkersKind.MarkupDebug, {
|
|
141
|
+
async rpcReceive(before: MarkupDebug, q: RpcReceiveQueue): Promise<MarkupDebug> {
|
|
142
|
+
const draft = createDraft(before);
|
|
143
|
+
draft.id = await q.receive(before.id);
|
|
144
|
+
draft.message = await q.receive(before.message);
|
|
145
|
+
draft.detail = await q.receive(before.detail);
|
|
146
|
+
return finishDraft(draft);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async rpcSend(after: MarkupDebug, q: RpcSendQueue): Promise<void> {
|
|
150
|
+
await q.getAndSend(after, a => a.id);
|
|
151
|
+
await q.getAndSend(after, a => a.message);
|
|
152
|
+
await q.getAndSend(after, a => a.detail);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
@@ -53,7 +53,7 @@ export class Generate {
|
|
|
53
53
|
cursor = rootCursor();
|
|
54
54
|
recipeCursors.set(recipe, cursor);
|
|
55
55
|
}
|
|
56
|
-
const ctx = getObject(request.p) as ExecutionContext;
|
|
56
|
+
const ctx = await getObject(request.p) as ExecutionContext;
|
|
57
57
|
const acc = recipe.accumulator(cursor, ctx);
|
|
58
58
|
const generated = await recipe.generate(acc, ctx)
|
|
59
59
|
|
|
@@ -13,7 +13,8 @@ export class GetLanguages {
|
|
|
13
13
|
const languages = [
|
|
14
14
|
"org.openrewrite.text.PlainText",
|
|
15
15
|
"org.openrewrite.json.tree.Json$Document",
|
|
16
|
-
|
|
16
|
+
// TODO Support for Javadoc comments is not yet implemented
|
|
17
|
+
// "org.openrewrite.java.tree.J$CompilationUnit",
|
|
17
18
|
"org.openrewrite.javascript.tree.JS$CompilationUnit",
|
|
18
19
|
];
|
|
19
20
|
context.target = '';
|
package/src/rpc/request/visit.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as rpc from "vscode-jsonrpc/node";
|
|
17
17
|
import {Recipe, ScanningRecipe} from "../../recipe";
|
|
18
|
-
import {Cursor, rootCursor, Tree} from "../../tree";
|
|
18
|
+
import {Cursor, rootCursor, SourceFile, Tree} from "../../tree";
|
|
19
19
|
import {TreeVisitor} from "../../visitor";
|
|
20
20
|
import {ExecutionContext} from "../../execution";
|
|
21
21
|
import {withMetrics, extractSourcePath} from "./metrics";
|
|
@@ -24,6 +24,10 @@ export interface VisitResponse {
|
|
|
24
24
|
modified: boolean
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Tracks the last phase (scan or edit) for each recipe to detect cycle transitions
|
|
28
|
+
type RecipePhase = 'scan' | 'edit';
|
|
29
|
+
const recipePhases: WeakMap<Recipe, RecipePhase> = new WeakMap();
|
|
30
|
+
|
|
27
31
|
export class Visit {
|
|
28
32
|
constructor(private readonly visitor: string,
|
|
29
33
|
private readonly sourceFileType: string,
|
|
@@ -77,13 +81,27 @@ export class Visit {
|
|
|
77
81
|
if (!recipe) {
|
|
78
82
|
throw new Error(`No scanning recipe found for key: ${recipeKey}`);
|
|
79
83
|
}
|
|
84
|
+
// If we're transitioning from edit back to scan, this is a new cycle.
|
|
85
|
+
// Clear the cursor so a fresh accumulator is created.
|
|
86
|
+
if (recipePhases.get(recipe) === 'edit') {
|
|
87
|
+
recipeCursors.delete(recipe);
|
|
88
|
+
}
|
|
89
|
+
recipePhases.set(recipe, 'scan');
|
|
90
|
+
|
|
80
91
|
let cursor = recipeCursors.get(recipe);
|
|
81
92
|
if (!cursor) {
|
|
82
93
|
cursor = rootCursor();
|
|
83
94
|
recipeCursors.set(recipe, cursor);
|
|
84
95
|
}
|
|
85
96
|
const acc = recipe.accumulator(cursor, p);
|
|
97
|
+
|
|
86
98
|
return new class extends TreeVisitor<any, ExecutionContext> {
|
|
99
|
+
// Delegate isAcceptable to the scanner visitor
|
|
100
|
+
// This ensures we only process source files the scanner can handle
|
|
101
|
+
async isAcceptable(sourceFile: SourceFile, ctx: ExecutionContext): Promise<boolean> {
|
|
102
|
+
return (await recipe.scanner(acc)).isAcceptable(sourceFile, ctx);
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
protected async preVisit(tree: any, ctx: ExecutionContext): Promise<any> {
|
|
88
106
|
await (await recipe.scanner(acc)).visit(tree, ctx);
|
|
89
107
|
this.stopAfterPreVisit();
|
|
@@ -96,6 +114,19 @@ export class Visit {
|
|
|
96
114
|
if (!recipe) {
|
|
97
115
|
throw new Error(`No editing recipe found for key: ${recipeKey}`);
|
|
98
116
|
}
|
|
117
|
+
recipePhases.set(recipe, 'edit');
|
|
118
|
+
|
|
119
|
+
// For ScanningRecipe, we need to use the same cursor that was used during scanning
|
|
120
|
+
// to retrieve the accumulator that was stored there
|
|
121
|
+
if (recipe instanceof ScanningRecipe) {
|
|
122
|
+
let cursor = recipeCursors.get(recipe);
|
|
123
|
+
if (!cursor) {
|
|
124
|
+
cursor = rootCursor();
|
|
125
|
+
recipeCursors.set(recipe, cursor);
|
|
126
|
+
}
|
|
127
|
+
const acc = recipe.accumulator(cursor, p);
|
|
128
|
+
return recipe.editorWithData(acc);
|
|
129
|
+
}
|
|
99
130
|
return await recipe.editor();
|
|
100
131
|
} else {
|
|
101
132
|
return Reflect.construct(
|
package/src/run.ts
CHANGED
|
@@ -82,14 +82,14 @@ export async function scheduleRun(recipe: Recipe, before: SourceFile[], ctx: Exe
|
|
|
82
82
|
const changeset: Result[] = [];
|
|
83
83
|
|
|
84
84
|
for (const b of before) {
|
|
85
|
-
const editedB = await recurseRecipeList(recipe, b, async (recipe, b2) => (await recipe.editor()).visit(b2, ctx));
|
|
85
|
+
const editedB = await recurseRecipeList(recipe, b, async (recipe, b2) => (await recipe.editor()).visit(b2, ctx, cursor));
|
|
86
86
|
if (editedB !== b) {
|
|
87
87
|
changeset.push(new Result(b, editedB));
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
for (const g of generated) {
|
|
92
|
-
const editedG = await recurseRecipeList(recipe, g, async (recipe, g2) => (await recipe.editor()).visit(g2, ctx));
|
|
92
|
+
const editedG = await recurseRecipeList(recipe, g, async (recipe, g2) => (await recipe.editor()).visit(g2, ctx, cursor));
|
|
93
93
|
if (editedG) {
|
|
94
94
|
changeset.push(new Result(undefined, editedG));
|
|
95
95
|
}
|