@openrewrite/rewrite 8.62.6 → 8.62.7

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 (79) hide show
  1. package/dist/javascript/parser.d.ts.map +1 -1
  2. package/dist/javascript/parser.js +9 -1
  3. package/dist/javascript/parser.js.map +1 -1
  4. package/dist/javascript/preconditions.d.ts +4 -3
  5. package/dist/javascript/preconditions.d.ts.map +1 -1
  6. package/dist/javascript/preconditions.js +11 -29
  7. package/dist/javascript/preconditions.js.map +1 -1
  8. package/dist/javascript/remove-import.d.ts +1 -0
  9. package/dist/javascript/remove-import.d.ts.map +1 -1
  10. package/dist/javascript/remove-import.js +76 -7
  11. package/dist/javascript/remove-import.js.map +1 -1
  12. package/dist/javascript/type-mapping.d.ts +4 -0
  13. package/dist/javascript/type-mapping.d.ts.map +1 -1
  14. package/dist/javascript/type-mapping.js +48 -33
  15. package/dist/javascript/type-mapping.js.map +1 -1
  16. package/dist/rpc/request/generate.d.ts +1 -1
  17. package/dist/rpc/request/generate.d.ts.map +1 -1
  18. package/dist/rpc/request/generate.js +6 -3
  19. package/dist/rpc/request/generate.js.map +1 -1
  20. package/dist/rpc/request/get-languages.d.ts +1 -1
  21. package/dist/rpc/request/get-languages.d.ts.map +1 -1
  22. package/dist/rpc/request/get-languages.js +6 -5
  23. package/dist/rpc/request/get-languages.js.map +1 -1
  24. package/dist/rpc/request/get-object.d.ts +1 -1
  25. package/dist/rpc/request/get-object.d.ts.map +1 -1
  26. package/dist/rpc/request/get-object.js +8 -4
  27. package/dist/rpc/request/get-object.js.map +1 -1
  28. package/dist/rpc/request/get-recipes.d.ts +1 -1
  29. package/dist/rpc/request/get-recipes.d.ts.map +1 -1
  30. package/dist/rpc/request/get-recipes.js +5 -3
  31. package/dist/rpc/request/get-recipes.js.map +1 -1
  32. package/dist/rpc/request/install-recipes.d.ts +1 -1
  33. package/dist/rpc/request/install-recipes.d.ts.map +1 -1
  34. package/dist/rpc/request/install-recipes.js +6 -3
  35. package/dist/rpc/request/install-recipes.js.map +1 -1
  36. package/dist/rpc/request/metrics.d.ts +42 -0
  37. package/dist/rpc/request/metrics.d.ts.map +1 -0
  38. package/dist/rpc/request/metrics.js +184 -0
  39. package/dist/rpc/request/metrics.js.map +1 -0
  40. package/dist/rpc/request/parse.d.ts +1 -1
  41. package/dist/rpc/request/parse.d.ts.map +1 -1
  42. package/dist/rpc/request/parse.js +7 -3
  43. package/dist/rpc/request/parse.js.map +1 -1
  44. package/dist/rpc/request/prepare-recipe.d.ts +1 -1
  45. package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
  46. package/dist/rpc/request/prepare-recipe.js +6 -3
  47. package/dist/rpc/request/prepare-recipe.js.map +1 -1
  48. package/dist/rpc/request/print.d.ts +1 -1
  49. package/dist/rpc/request/print.d.ts.map +1 -1
  50. package/dist/rpc/request/print.js +6 -3
  51. package/dist/rpc/request/print.js.map +1 -1
  52. package/dist/rpc/request/visit.d.ts +1 -1
  53. package/dist/rpc/request/visit.d.ts.map +1 -1
  54. package/dist/rpc/request/visit.js +8 -4
  55. package/dist/rpc/request/visit.js.map +1 -1
  56. package/dist/rpc/rewrite-rpc.d.ts +1 -0
  57. package/dist/rpc/rewrite-rpc.d.ts.map +1 -1
  58. package/dist/rpc/rewrite-rpc.js +12 -9
  59. package/dist/rpc/rewrite-rpc.js.map +1 -1
  60. package/dist/rpc/server.js +2 -0
  61. package/dist/rpc/server.js.map +1 -1
  62. package/dist/version.txt +1 -1
  63. package/package.json +1 -1
  64. package/src/javascript/parser.ts +13 -1
  65. package/src/javascript/preconditions.ts +10 -10
  66. package/src/javascript/remove-import.ts +75 -9
  67. package/src/javascript/type-mapping.ts +50 -30
  68. package/src/rpc/request/generate.ts +7 -3
  69. package/src/rpc/request/get-languages.ts +6 -5
  70. package/src/rpc/request/get-object.ts +10 -4
  71. package/src/rpc/request/get-recipes.ts +5 -3
  72. package/src/rpc/request/install-recipes.ts +6 -3
  73. package/src/rpc/request/metrics.ts +167 -0
  74. package/src/rpc/request/parse.ts +12 -6
  75. package/src/rpc/request/prepare-recipe.ts +7 -3
  76. package/src/rpc/request/print.ts +7 -3
  77. package/src/rpc/request/visit.ts +9 -4
  78. package/src/rpc/rewrite-rpc.ts +14 -9
  79. package/src/rpc/server.ts +3 -0
@@ -25,6 +25,9 @@ class NonDraftableType {
25
25
  export class JavaScriptTypeMapping {
26
26
  private readonly typeCache: Map<string | number, Type> = new Map();
27
27
  private readonly regExpSymbol: ts.Symbol | undefined;
28
+ private readonly stringWrapperType: ts.Type | undefined;
29
+ private readonly numberWrapperType: ts.Type | undefined;
30
+ private readonly booleanWrapperType: ts.Type | undefined;
28
31
 
29
32
  constructor(
30
33
  private readonly checker: ts.TypeChecker
@@ -35,6 +38,24 @@ export class JavaScriptTypeMapping {
35
38
  ts.SymbolFlags.Type,
36
39
  false
37
40
  );
41
+
42
+ // Resolve global wrapper types for primitives from TypeScript's lib
43
+ const stringSymbol = checker.resolveName("String", undefined, ts.SymbolFlags.Type, false);
44
+ const numberSymbol = checker.resolveName("Number", undefined, ts.SymbolFlags.Type, false);
45
+ const booleanSymbol = checker.resolveName("Boolean", undefined, ts.SymbolFlags.Type, false);
46
+
47
+ // Store the TypeScript types; conversion to Type happens on-demand
48
+ if (stringSymbol) {
49
+ this.stringWrapperType = checker.getDeclaredTypeOfSymbol(stringSymbol);
50
+ }
51
+
52
+ if (numberSymbol) {
53
+ this.numberWrapperType = checker.getDeclaredTypeOfSymbol(numberSymbol);
54
+ }
55
+
56
+ if (booleanSymbol) {
57
+ this.booleanWrapperType = checker.getDeclaredTypeOfSymbol(booleanSymbol);
58
+ }
38
59
  }
39
60
 
40
61
  type(node: ts.Node): Type | undefined {
@@ -171,6 +192,19 @@ export class JavaScriptTypeMapping {
171
192
  }) as Type.Method;
172
193
  }
173
194
 
195
+ private wrapperType(declaringType: (Type.FullyQualified & Type.Primitive) | Type.FullyQualified) {
196
+ if (declaringType == Type.Primitive.String && this.stringWrapperType) {
197
+ return this.getType(this.stringWrapperType) as Type.FullyQualified;
198
+ } else if ((declaringType == Type.Primitive.Double || declaringType == Type.Primitive.Long) && this.numberWrapperType) {
199
+ return this.getType(this.numberWrapperType) as Type.FullyQualified;
200
+ } else if (declaringType == Type.Primitive.Boolean && this.booleanWrapperType) {
201
+ return this.getType(this.booleanWrapperType) as Type.FullyQualified;
202
+ } else {
203
+ // This should not really happen, but we'll fallback to unknown if needed
204
+ return Type.unknownType as Type.FullyQualified;
205
+ }
206
+ }
207
+
174
208
  methodType(node: ts.Node): Type.Method | undefined {
175
209
 
176
210
  let signature: ts.Signature | undefined;
@@ -289,27 +323,12 @@ export class JavaScriptTypeMapping {
289
323
  } else {
290
324
  declaringType = mappedType as Type.FullyQualified;
291
325
  }
326
+ } else if (mappedType && mappedType.kind === Type.Kind.Primitive) {
327
+ // Box the primitive to its wrapper type
328
+ declaringType = this.wrapperType(mappedType as Type.Primitive);
292
329
  } else {
293
- // Handle primitive types and other non-class types
294
- if (mappedType) {
295
- if ((mappedType as any).keyword === 'String') {
296
- declaringType = {
297
- kind: Type.Kind.Class,
298
- fullyQualifiedName: 'String'
299
- } as Type.FullyQualified;
300
- } else if ((mappedType as any).keyword === 'Number') {
301
- declaringType = {
302
- kind: Type.Kind.Class,
303
- fullyQualifiedName: 'Number'
304
- } as Type.FullyQualified;
305
- } else {
306
- // Fallback for other types
307
- declaringType = mappedType as Type.FullyQualified;
308
- }
309
- } else {
310
- // Default to unknown if we can't determine the type
311
- declaringType = Type.unknownType as Type.FullyQualified;
312
- }
330
+ // Default to unknown if we can't determine the type
331
+ declaringType = Type.unknownType as Type.FullyQualified;
313
332
  }
314
333
 
315
334
  // For string methods like 'hello'.split(), ensure we have a proper declaring type for primitives
@@ -317,15 +336,11 @@ export class JavaScriptTypeMapping {
317
336
  // If the expression type is a primitive string, use String as declaring type
318
337
  const typeString = this.checker.typeToString(exprType);
319
338
  if (typeString === 'string' || exprType.flags & ts.TypeFlags.String || exprType.flags & ts.TypeFlags.StringLiteral) {
320
- declaringType = {
321
- kind: Type.Kind.Class,
322
- fullyQualifiedName: 'string'
323
- } as Type.FullyQualified;
339
+ declaringType = this.wrapperType(Type.Primitive.String);
324
340
  } else if (typeString === 'number' || exprType.flags & ts.TypeFlags.Number || exprType.flags & ts.TypeFlags.NumberLiteral) {
325
- declaringType = {
326
- kind: Type.Kind.Class,
327
- fullyQualifiedName: 'number'
328
- } as Type.FullyQualified;
341
+ declaringType = this.wrapperType(Type.Primitive.Double);
342
+ } else if (typeString === 'boolean' || exprType.flags & ts.TypeFlags.Boolean || exprType.flags & ts.TypeFlags.BooleanLiteral) {
343
+ declaringType = this.wrapperType(Type.Primitive.Boolean);
329
344
  } else {
330
345
  // Fallback for other primitive types or unknown
331
346
  declaringType = Type.unknownType as Type.FullyQualified;
@@ -338,10 +353,10 @@ export class JavaScriptTypeMapping {
338
353
  // Check if this is an import first
339
354
  const symbol = this.checker.getSymbolAtLocation(node.expression);
340
355
  let moduleSpecifier: string | undefined;
356
+ let aliasedSymbol: ts.Symbol | undefined;
341
357
 
342
358
  if (symbol) {
343
359
  // Check if this is an aliased symbol (i.e., an import)
344
- let aliasedSymbol: ts.Symbol | undefined;
345
360
  if (symbol.flags & ts.SymbolFlags.Alias) {
346
361
  aliasedSymbol = this.checker.getAliasedSymbol(symbol);
347
362
  }
@@ -383,7 +398,12 @@ export class JavaScriptTypeMapping {
383
398
  kind: Type.Kind.Class,
384
399
  fullyQualifiedName: moduleSpecifier
385
400
  } as Type.FullyQualified;
386
- methodName = '<default>';
401
+ // For aliased imports, use the original function name from the aliased symbol
402
+ if (aliasedSymbol && aliasedSymbol.name) {
403
+ methodName = aliasedSymbol.name;
404
+ } else {
405
+ methodName = '<default>';
406
+ }
387
407
  }
388
408
  } else {
389
409
  // Fall back to the original logic for non-imported functions
@@ -17,6 +17,7 @@ import * as rpc from "vscode-jsonrpc/node";
17
17
  import {Recipe, ScanningRecipe} from "../../recipe";
18
18
  import {Cursor, rootCursor} from "../../tree";
19
19
  import {ExecutionContext} from "../../execution";
20
+ import {withMetrics} from "./metrics";
20
21
 
21
22
  export interface GenerateResponse {
22
23
  ids: string[]
@@ -31,8 +32,11 @@ export class Generate {
31
32
  localObjects: Map<string, any>,
32
33
  preparedRecipes: Map<String, Recipe>,
33
34
  recipeCursors: WeakMap<Recipe, Cursor>,
34
- getObject: (id: string) => any): void {
35
- connection.onRequest(new rpc.RequestType<Generate, GenerateResponse, Error>("Generate"), async (request) => {
35
+ getObject: (id: string) => any,
36
+ metricsCsv?: string): void {
37
+ const target = { target: '' };
38
+ connection.onRequest(new rpc.RequestType<Generate, GenerateResponse, Error>("Generate"), withMetrics<Generate, GenerateResponse>("Generate", target, metricsCsv)(async (request) => {
39
+ target.target = request.id;
36
40
  const recipe = preparedRecipes.get(request.id);
37
41
  const response = {
38
42
  ids: [],
@@ -57,6 +61,6 @@ export class Generate {
57
61
 
58
62
  }
59
63
  return response;
60
- });
64
+ }));
61
65
  }
62
66
  }
@@ -1,16 +1,17 @@
1
1
  import * as rpc from "vscode-jsonrpc/node";
2
+ import {withMetrics0} from "./metrics";
2
3
 
3
4
  export class GetLanguages {
4
- static handle(connection: rpc.MessageConnection): void {
5
- connection.onRequest(new rpc.RequestType0<string[], Error>("GetLanguages"), async () => {
5
+ static handle(connection: rpc.MessageConnection, metricsCsv?: string): void {
6
+ const target = {target: ''};
7
+ connection.onRequest(new rpc.RequestType0<string[], Error>("GetLanguages"), withMetrics0<string[]>("GetLanguages", target, metricsCsv)(async () => {
6
8
  // Include all languages you want this server to support receiving from a remote peer
7
- const languages: string[] = [
9
+ return [
8
10
  "org.openrewrite.text.PlainText",
9
11
  "org.openrewrite.json.tree.Json$Document",
10
12
  "org.openrewrite.java.tree.J$CompilationUnit",
11
13
  "org.openrewrite.javascript.tree.JS$CompilationUnit",
12
14
  ];
13
- return languages;
14
- });
15
+ }));
15
16
  }
16
17
  }
@@ -16,6 +16,7 @@
16
16
  import * as rpc from "vscode-jsonrpc/node";
17
17
  import {RpcObjectData, RpcObjectState, RpcSendQueue} from "../queue";
18
18
  import {ReferenceMap} from "../../reference";
19
+ import {withMetrics, extractSourcePath} from "./metrics";
19
20
 
20
21
  export class GetObject {
21
22
  constructor(private readonly id: string,
@@ -28,11 +29,13 @@ export class GetObject {
28
29
  localObjects: Map<string, any | ((input: string) => any)>,
29
30
  localRefs: ReferenceMap,
30
31
  batchSize: number,
31
- trace: boolean
32
+ trace: boolean,
33
+ metricsCsv?: string
32
34
  ): void {
33
35
  const pendingData = new Map<string, RpcObjectData[]>();
36
+ const target = { target: '' };
34
37
 
35
- connection.onRequest(new rpc.RequestType<GetObject, any, Error>("GetObject"), async request => {
38
+ connection.onRequest(new rpc.RequestType<GetObject, any, Error>("GetObject"), withMetrics<GetObject, any>("GetObject", target, metricsCsv)(async request => {
36
39
  let objId = request.id;
37
40
  if (!localObjects.has(objId)) {
38
41
  return [
@@ -47,9 +50,12 @@ export class GetObject {
47
50
  localObjects.set(objId, obj);
48
51
  }
49
52
 
53
+ const obj = localObjects.get(objId);
54
+ target.target = extractSourcePath(obj);
55
+
50
56
  let allData = pendingData.get(objId);
51
57
  if (!allData) {
52
- const after = localObjects.get(objId);
58
+ const after = obj;
53
59
  const before = remoteObjects.get(objId);
54
60
 
55
61
  allData = await new RpcSendQueue(localRefs, request.sourceFileType, trace).generate(after, before);
@@ -66,6 +72,6 @@ export class GetObject {
66
72
  }
67
73
 
68
74
  return batch;
69
- });
75
+ }));
70
76
  }
71
77
  }
@@ -15,15 +15,17 @@
15
15
  */
16
16
  import * as rpc from "vscode-jsonrpc/node";
17
17
  import {RecipeDescriptor, RecipeRegistry} from "../../recipe";
18
+ import {withMetrics0} from "./metrics";
18
19
 
19
20
  export class GetRecipes {
20
- static handle(connection: rpc.MessageConnection, registry: RecipeRegistry): void {
21
- connection.onRequest(new rpc.RequestType0<({ name: string } & RecipeDescriptor)[], Error>("GetRecipes"), async () => {
21
+ static handle(connection: rpc.MessageConnection, registry: RecipeRegistry, metricsCsv?: string): void {
22
+ const target = { target: '' };
23
+ connection.onRequest(new rpc.RequestType0<({ name: string } & RecipeDescriptor)[], Error>("GetRecipes"), withMetrics0<({ name: string } & RecipeDescriptor)[]>("GetRecipes", target, metricsCsv)(async () => {
22
24
  const recipes = [];
23
25
  for (const [_name, recipe] of registry.all.entries()) {
24
26
  recipes.push(await new recipe().descriptor());
25
27
  }
26
28
  return recipes;
27
- });
29
+ }));
28
30
  }
29
31
  }
@@ -18,6 +18,7 @@ import {RecipeRegistry} from "../../recipe";
18
18
  import * as path from "path";
19
19
  import * as fs from "fs";
20
20
  import {spawn, ChildProcess} from "child_process";
21
+ import {withMetrics} from "./metrics";
21
22
 
22
23
  export interface InstallRecipesResponse {
23
24
  recipesInstalled: number
@@ -68,8 +69,10 @@ export class InstallRecipes {
68
69
  }
69
70
 
70
71
  static handle(connection: rpc.MessageConnection, installDir: string, registry: RecipeRegistry,
71
- logger?: rpc.Logger): void {
72
- connection.onRequest(new rpc.RequestType<InstallRecipes, InstallRecipesResponse, Error>("InstallRecipes"), async (request) => {
72
+ logger?: rpc.Logger, metricsCsv?: string): void {
73
+ const target = { target: '' };
74
+ connection.onRequest(new rpc.RequestType<InstallRecipes, InstallRecipesResponse, Error>("InstallRecipes"), withMetrics<InstallRecipes, InstallRecipesResponse>("InstallRecipes", target, metricsCsv)(async (request) => {
75
+ target.target = typeof request.recipes === "object" ? request.recipes.packageName : request.recipes;
73
76
  const beforeInstall = registry.all.size;
74
77
  let resolvedPath;
75
78
  let recipesName = request.recipes;
@@ -129,7 +132,7 @@ export class InstallRecipes {
129
132
  }
130
133
 
131
134
  return {recipesInstalled: registry.all.size - beforeInstall};
132
- });
135
+ }));
133
136
  }
134
137
  }
135
138
 
@@ -0,0 +1,167 @@
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 * as fs from 'fs';
17
+ import * as rpc from "vscode-jsonrpc/node";
18
+ import {Cursor, isSourceFile, SourceFile} from "../../tree";
19
+
20
+ const CSV_HEADER = 'request,target,durationMs,memoryUsedBytes,memoryMaxBytes';
21
+
22
+ /**
23
+ * Extracts the sourcePath from a tree object, either directly from a SourceFile
24
+ * or by finding the nearest SourceFile via a cursor.
25
+ *
26
+ * @param tree The tree object to extract sourcePath from
27
+ * @param cursor Optional cursor to find the nearest SourceFile
28
+ * @returns The sourcePath or empty string if not found
29
+ */
30
+ export function extractSourcePath(tree: any, cursor?: Cursor): string {
31
+ if (isSourceFile(tree)) {
32
+ return (tree as SourceFile).sourcePath || '';
33
+ }
34
+
35
+ if (cursor) {
36
+ const sourceFile = cursor.firstEnclosing(t => isSourceFile(t));
37
+ if (sourceFile) {
38
+ return (sourceFile as SourceFile).sourcePath || '';
39
+ }
40
+ }
41
+
42
+ return '';
43
+ }
44
+
45
+ /**
46
+ * Initializes the metrics CSV file with a header if it doesn't already exist or is empty.
47
+ * If the file already contains the correct header, this function is a no-op.
48
+ *
49
+ * @param metricsCsv The path to the CSV file for recording metrics
50
+ * @param logger Optional logger for warnings
51
+ */
52
+ export function initializeMetricsCsv(metricsCsv?: string, logger?: rpc.Logger): void {
53
+ if (!metricsCsv) {
54
+ return;
55
+ }
56
+
57
+ try {
58
+ // Check if file exists and has content
59
+ if (fs.existsSync(metricsCsv)) {
60
+ const content = fs.readFileSync(metricsCsv, 'utf8');
61
+ const firstLine = content.split('\n')[0];
62
+
63
+ // If file already has the correct header, skip initialization
64
+ if (firstLine.trim() === CSV_HEADER) {
65
+ return;
66
+ }
67
+
68
+ // File exists but has incorrect header - warn and reset
69
+ if (firstLine.trim()) {
70
+ logger?.warn(`Metrics CSV file ${metricsCsv} has incorrect header. Expected '${CSV_HEADER}' but found '${firstLine.trim()}'. Resetting file.`);
71
+ }
72
+ }
73
+
74
+ // Write header to new or empty file (overwrites existing file with incorrect header)
75
+ fs.writeFileSync(metricsCsv, CSV_HEADER + '\n');
76
+ } catch (err) {
77
+ console.error('Failed to initialize metrics CSV:', err);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Internal function to wrap a handler with metrics recording.
83
+ */
84
+ async function wrapWithMetrics<R>(
85
+ handler: () => Promise<R>,
86
+ target: { target: string },
87
+ request: string,
88
+ metricsCsv?: string
89
+ ): Promise<R> {
90
+ if (!metricsCsv) {
91
+ // No metrics recording requested, just execute the handler
92
+ return handler();
93
+ }
94
+
95
+ const startTime = Date.now();
96
+
97
+ try {
98
+ const result = await handler();
99
+ recordMetrics(metricsCsv, target.target, request, startTime);
100
+ return result;
101
+ } catch (error) {
102
+ recordMetrics(metricsCsv, target.target, request, startTime);
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Wraps an RPC request handler to record performance metrics to a CSV file.
109
+ *
110
+ * @param request The request type name (e.g., "Visit", "GetObject")
111
+ * @param target A mutable object containing the target identifier for metrics
112
+ * @param metricsCsv Optional path to the CSV file for recording metrics
113
+ * @returns A function that wraps a request handler with metrics recording
114
+ */
115
+ export function withMetrics<P, R>(
116
+ request: string,
117
+ target: { target: string },
118
+ metricsCsv?: string
119
+ ): (handler: (request: P) => Promise<R>) => (request: P) => Promise<R> {
120
+ return (handler: (requestParam: P) => Promise<R>) => {
121
+ return async (requestParam: P): Promise<R> => {
122
+ return wrapWithMetrics(() => handler(requestParam), target, request, metricsCsv);
123
+ };
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Wraps an RPC request handler without parameters (RequestType0) to record performance metrics.
129
+ *
130
+ * @param request The request type name (e.g., "GetLanguages", "GetRecipes")
131
+ * @param target A mutable object containing the target identifier for metrics
132
+ * @param metricsCsv Optional path to the CSV file for recording metrics
133
+ * @returns A function that wraps a request handler with metrics recording
134
+ */
135
+ export function withMetrics0<R>(
136
+ request: string,
137
+ target: { target: string },
138
+ metricsCsv?: string
139
+ ): (handler: () => Promise<R>) => (token: any) => Promise<R> {
140
+ return (handler: () => Promise<R>) => {
141
+ return async (_: any): Promise<R> => {
142
+ return wrapWithMetrics(handler, target, request, metricsCsv);
143
+ };
144
+ };
145
+ }
146
+
147
+ function recordMetrics(
148
+ metricsCsv: string,
149
+ target: string,
150
+ request: string,
151
+ startTime: number
152
+ ): void {
153
+ const endTime = Date.now();
154
+ const memEnd = process.memoryUsage();
155
+ const durationMs = endTime - startTime;
156
+
157
+ const memoryUsedBytes = memEnd.heapUsed;
158
+ const memoryMaxBytes = memEnd.heapTotal;
159
+
160
+ const csvRow = `${request},${target},${durationMs},${memoryUsedBytes},${memoryMaxBytes}\n`;
161
+
162
+ try {
163
+ fs.appendFileSync(metricsCsv, csvRow);
164
+ } catch (err) {
165
+ console.error('Failed to write metrics to CSV:', err);
166
+ }
167
+ }
@@ -15,11 +15,11 @@
15
15
  */
16
16
  import * as rpc from "vscode-jsonrpc/node";
17
17
  import {ExecutionContext} from "../../execution";
18
- import {UUID} from "node:crypto";
19
18
  import {ParserInput, Parsers} from "../../parser";
20
- import {randomId} from "../../uuid";
19
+ import {randomId, UUID} from "../../uuid";
21
20
  import {produce} from "immer";
22
21
  import {SourceFile} from "../../tree";
22
+ import {withMetrics} from "./metrics";
23
23
 
24
24
  export class Parse {
25
25
  constructor(private readonly inputs: ParserInput[],
@@ -27,8 +27,14 @@ export class Parse {
27
27
  }
28
28
 
29
29
  static handle(connection: rpc.MessageConnection,
30
- localObjects: Map<string, ((input: string) => any) | any>): void {
31
- connection.onRequest(new rpc.RequestType<Parse, UUID[], Error>("Parse"), async (request) => {
30
+ localObjects: Map<string, ((input: string) => any) | any>,
31
+ metricsCsv?: string): void {
32
+ const target = { target: '' };
33
+ connection.onRequest(new rpc.RequestType<Parse, UUID[], Error>("Parse"), withMetrics<Parse, UUID[]>("Parse", target, metricsCsv)(async (request) => {
34
+ // Set target to comma-separated list of file paths
35
+ target.target = request.inputs.map(input =>
36
+ typeof input === 'string' ? input : input.sourcePath
37
+ ).join(',');
32
38
  let parser = Parsers.createParser("javascript", {
33
39
  ctx: new ExecutionContext(),
34
40
  relativeTo: request.relativeTo
@@ -38,7 +44,7 @@ export class Parse {
38
44
  return [];
39
45
  }
40
46
  const generator = parser.parse(...request.inputs);
41
- const result: string[] = [];
47
+ const result: UUID[] = [];
42
48
 
43
49
  for (let i = 0; i < request.inputs.length; i++) {
44
50
  const id = randomId();
@@ -50,6 +56,6 @@ export class Parse {
50
56
  }
51
57
 
52
58
  return result;
53
- });
59
+ }));
54
60
  }
55
61
  }
@@ -21,6 +21,7 @@ import {Check} from "../../preconditions";
21
21
  import {RpcRecipe} from "../recipe";
22
22
  import {TreeVisitor} from "../../visitor";
23
23
  import {ExecutionContext} from "../../execution";
24
+ import {withMetrics} from "./metrics";
24
25
 
25
26
  export class PrepareRecipe {
26
27
  constructor(private readonly id: string, private readonly options?: any) {
@@ -28,9 +29,12 @@ export class PrepareRecipe {
28
29
 
29
30
  static handle(connection: MessageConnection,
30
31
  registry: RecipeRegistry,
31
- preparedRecipes: Map<String, Recipe>) {
32
+ preparedRecipes: Map<String, Recipe>,
33
+ metricsCsv?: string) {
32
34
  const snowflake = SnowflakeId();
33
- connection.onRequest(new rpc.RequestType<PrepareRecipe, PrepareRecipeResponse, Error>("PrepareRecipe"), async (request) => {
35
+ const target = { target: '' };
36
+ connection.onRequest(new rpc.RequestType<PrepareRecipe, PrepareRecipeResponse, Error>("PrepareRecipe"), withMetrics<PrepareRecipe, PrepareRecipeResponse>("PrepareRecipe", target, metricsCsv)(async (request) => {
37
+ target.target = request.id;
34
38
  const id = snowflake.generate();
35
39
  const recipeCtor = registry.all.get(request.id);
36
40
  if (!recipeCtor) {
@@ -54,7 +58,7 @@ export class PrepareRecipe {
54
58
  scanVisitor: recipe instanceof ScanningRecipe ? `scan:${id}` : undefined,
55
59
  scanPreconditions: scanPreconditions
56
60
  }
57
- });
61
+ }));
58
62
  }
59
63
 
60
64
  /**
@@ -17,6 +17,7 @@ import * as rpc from "vscode-jsonrpc/node";
17
17
  import {isSourceFile, Tree} from "../../tree";
18
18
  import {MarkerPrinter as PrintMarkerPrinter, printer, PrintOutputCapture} from "../../print";
19
19
  import {UUID} from "../../uuid";
20
+ import {withMetrics, extractSourcePath} from "./metrics";
20
21
 
21
22
  export const enum MarkerPrinter {
22
23
  DEFAULT = "DEFAULT",
@@ -29,15 +30,18 @@ export class Print {
29
30
  }
30
31
 
31
32
  static handle(connection: rpc.MessageConnection,
32
- getObject: (id: string, sourceFileType?: string) => any): void {
33
- connection.onRequest(new rpc.RequestType<Print, string, Error>("Print"), async request => {
33
+ getObject: (id: string, sourceFileType?: string) => any,
34
+ metricsCsv?: string): void {
35
+ const target = { target: '' };
36
+ connection.onRequest(new rpc.RequestType<Print, string, Error>("Print"), withMetrics<Print, string>("Print", target, metricsCsv)(async request => {
34
37
  const tree: Tree = await getObject(request.treeId.toString(), request.sourceFileType);
38
+ target.target = extractSourcePath(tree);
35
39
  const out = new PrintOutputCapture(PrintMarkerPrinter[request.markerPrinter]);
36
40
  if (isSourceFile(tree)) {
37
41
  return await printer(tree).print(tree, out);
38
42
  } else {
39
43
  return await printer(request.sourceFileType).print(tree, out);
40
44
  }
41
- });
45
+ }));
42
46
  }
43
47
  }
@@ -18,6 +18,7 @@ import {Recipe, ScanningRecipe} from "../../recipe";
18
18
  import {Cursor, rootCursor, Tree} from "../../tree";
19
19
  import {TreeVisitor} from "../../visitor";
20
20
  import {ExecutionContext} from "../../execution";
21
+ import {withMetrics, extractSourcePath} from "./metrics";
21
22
 
22
23
  export interface VisitResponse {
23
24
  modified: boolean
@@ -37,14 +38,18 @@ export class Visit {
37
38
  preparedRecipes: Map<String, Recipe>,
38
39
  recipeCursors: WeakMap<Recipe, Cursor>,
39
40
  getObject: (id: string, sourceFileType?: string) => any,
40
- getCursor: (cursorIds: string[] | undefined, sourceFileType?: string) => Promise<Cursor>): void {
41
- connection.onRequest(new rpc.RequestType<Visit, VisitResponse, Error>("Visit"), async (request) => {
41
+ getCursor: (cursorIds: string[] | undefined, sourceFileType?: string) => Promise<Cursor>,
42
+ metricsCsv?: string): void {
43
+ const target = { target: '' };
44
+ connection.onRequest(new rpc.RequestType<Visit, VisitResponse, Error>("Visit"), withMetrics<Visit, VisitResponse>("Visit", target, metricsCsv)(async (request) => {
42
45
  const p = await getObject(request.p, undefined);
43
46
  const before: Tree = await getObject(request.treeId, request.sourceFileType);
47
+ const cursor = await getCursor(request.cursor, request.sourceFileType);
48
+ target.target = extractSourcePath(before, cursor);
44
49
  localObjects.set(before.id.toString(), before);
45
50
 
46
51
  const visitor = await Visit.instantiateVisitor(request, preparedRecipes, recipeCursors, p);
47
- const after = await visitor.visit(before, p, await getCursor(request.cursor, request.sourceFileType));
52
+ const after = await visitor.visit(before, p, cursor);
48
53
  if (!after) {
49
54
  localObjects.delete(before.id.toString());
50
55
  } else if (after !== before) {
@@ -52,7 +57,7 @@ export class Visit {
52
57
  }
53
58
 
54
59
  return {modified: before !== after};
55
- });
60
+ }));
56
61
  }
57
62
 
58
63
  private static async instantiateVisitor(request: Visit,
@@ -29,6 +29,7 @@ import {
29
29
  Visit,
30
30
  VisitResponse
31
31
  } from "./request";
32
+ import {initializeMetricsCsv} from "./request/metrics";
32
33
  import {RpcObjectData, RpcObjectState, RpcReceiveQueue} from "./queue";
33
34
  import {RpcRecipe} from "./recipe";
34
35
  import {ExecutionContext} from "../execution";
@@ -61,10 +62,14 @@ export class RewriteRpc {
61
62
  batchSize?: number,
62
63
  registry?: RecipeRegistry,
63
64
  logger?: rpc.Logger,
65
+ metricsCsv?: string,
64
66
  traceGetObjectOutput?: boolean,
65
67
  traceGetObjectInput?: Writable,
66
68
  recipeInstallDir?: string
67
69
  }) {
70
+ // Initialize metrics CSV file if configured
71
+ initializeMetricsCsv(options.metricsCsv, options.logger);
72
+
68
73
  const preparedRecipes: Map<String, Recipe> = new Map();
69
74
  const recipeCursors: WeakMap<Recipe, Cursor> = new WeakMap()
70
75
 
@@ -74,16 +79,16 @@ export class RewriteRpc {
74
79
 
75
80
  const registry = options.registry || new RecipeRegistry();
76
81
 
77
- Visit.handle(this.connection, this.localObjects, preparedRecipes, recipeCursors, getObject, getCursor);
78
- Generate.handle(this.connection, this.localObjects, preparedRecipes, recipeCursors, getObject);
82
+ Visit.handle(this.connection, this.localObjects, preparedRecipes, recipeCursors, getObject, getCursor, options.metricsCsv);
83
+ Generate.handle(this.connection, this.localObjects, preparedRecipes, recipeCursors, getObject, options.metricsCsv);
79
84
  GetObject.handle(this.connection, this.remoteObjects, this.localObjects,
80
- this.localRefs, options?.batchSize || 200, !!options?.traceGetObjectOutput);
81
- GetRecipes.handle(this.connection, registry);
82
- GetLanguages.handle(this.connection);
83
- PrepareRecipe.handle(this.connection, registry, preparedRecipes);
84
- Parse.handle(this.connection, this.localObjects);
85
- Print.handle(this.connection, getObject);
86
- InstallRecipes.handle(this.connection, options.recipeInstallDir ?? ".rewrite", registry, options.logger);
85
+ this.localRefs, options?.batchSize || 200, !!options?.traceGetObjectOutput, options.metricsCsv);
86
+ GetRecipes.handle(this.connection, registry, options.metricsCsv);
87
+ GetLanguages.handle(this.connection, options.metricsCsv);
88
+ PrepareRecipe.handle(this.connection, registry, preparedRecipes, options.metricsCsv);
89
+ Parse.handle(this.connection, this.localObjects, options.metricsCsv);
90
+ Print.handle(this.connection, getObject, options.metricsCsv);
91
+ InstallRecipes.handle(this.connection, options.recipeInstallDir ?? ".rewrite", registry, options.logger, options.metricsCsv);
87
92
 
88
93
  this.connection.listen();
89
94
  }