@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.
Files changed (76) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +4 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/javascript/index.d.ts +3 -0
  5. package/dist/javascript/index.d.ts.map +1 -1
  6. package/dist/javascript/index.js +3 -0
  7. package/dist/javascript/index.js.map +1 -1
  8. package/dist/javascript/package-json-parser.d.ts +0 -5
  9. package/dist/javascript/package-json-parser.d.ts.map +1 -1
  10. package/dist/javascript/package-json-parser.js +13 -25
  11. package/dist/javascript/package-json-parser.js.map +1 -1
  12. package/dist/javascript/package-manager.d.ts +131 -0
  13. package/dist/javascript/package-manager.d.ts.map +1 -0
  14. package/dist/javascript/package-manager.js +372 -0
  15. package/dist/javascript/package-manager.js.map +1 -0
  16. package/dist/javascript/recipes/index.d.ts +2 -0
  17. package/dist/javascript/recipes/index.d.ts.map +1 -0
  18. package/dist/javascript/recipes/index.js +33 -0
  19. package/dist/javascript/recipes/index.js.map +1 -0
  20. package/dist/javascript/recipes/upgrade-dependency-version.d.ts +105 -0
  21. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -0
  22. package/dist/javascript/recipes/upgrade-dependency-version.js +493 -0
  23. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -0
  24. package/dist/javascript/search/find-dependency.d.ts +32 -0
  25. package/dist/javascript/search/find-dependency.d.ts.map +1 -0
  26. package/dist/javascript/search/find-dependency.js +312 -0
  27. package/dist/javascript/search/find-dependency.js.map +1 -0
  28. package/dist/javascript/search/index.d.ts +1 -0
  29. package/dist/javascript/search/index.d.ts.map +1 -1
  30. package/dist/javascript/search/index.js +1 -0
  31. package/dist/javascript/search/index.js.map +1 -1
  32. package/dist/json/print.js +1 -1
  33. package/dist/json/print.js.map +1 -1
  34. package/dist/markers.d.ts +67 -0
  35. package/dist/markers.d.ts.map +1 -1
  36. package/dist/markers.js +101 -0
  37. package/dist/markers.js.map +1 -1
  38. package/dist/print.d.ts.map +1 -1
  39. package/dist/print.js +0 -1
  40. package/dist/print.js.map +1 -1
  41. package/dist/recipe.js +3 -3
  42. package/dist/recipe.js.map +1 -1
  43. package/dist/rpc/index.js +72 -0
  44. package/dist/rpc/index.js.map +1 -1
  45. package/dist/rpc/request/generate.js +1 -1
  46. package/dist/rpc/request/generate.js.map +1 -1
  47. package/dist/rpc/request/get-languages.d.ts.map +1 -1
  48. package/dist/rpc/request/get-languages.js +2 -1
  49. package/dist/rpc/request/get-languages.js.map +1 -1
  50. package/dist/rpc/request/visit.d.ts.map +1 -1
  51. package/dist/rpc/request/visit.js +27 -0
  52. package/dist/rpc/request/visit.js.map +1 -1
  53. package/dist/run.js +2 -2
  54. package/dist/run.js.map +1 -1
  55. package/dist/test/rewrite-test.js +1 -1
  56. package/dist/test/rewrite-test.js.map +1 -1
  57. package/dist/version.txt +1 -1
  58. package/package.json +1 -1
  59. package/src/index.ts +4 -0
  60. package/src/javascript/index.ts +3 -0
  61. package/src/javascript/package-json-parser.ts +14 -33
  62. package/src/javascript/package-manager.ts +428 -0
  63. package/src/javascript/recipes/index.ts +17 -0
  64. package/src/javascript/recipes/upgrade-dependency-version.ts +586 -0
  65. package/src/javascript/search/find-dependency.ts +303 -0
  66. package/src/javascript/search/index.ts +1 -0
  67. package/src/json/print.ts +1 -1
  68. package/src/markers.ts +146 -0
  69. package/src/print.ts +0 -1
  70. package/src/recipe.ts +3 -3
  71. package/src/rpc/index.ts +65 -1
  72. package/src/rpc/request/generate.ts +1 -1
  73. package/src/rpc/request/get-languages.ts +2 -1
  74. package/src/rpc/request/visit.ts +32 -1
  75. package/src/run.ts +2 -2
  76. 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
+ }
@@ -1,2 +1,3 @@
1
+ export * from './find-dependency';
1
2
  export * from './uses-method';
2
3
  export * from './uses-type';
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
- "org.openrewrite.java.tree.J$CompilationUnit",
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 = '';
@@ -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
  }