@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.
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +9 -1
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/preconditions.d.ts +4 -3
- package/dist/javascript/preconditions.d.ts.map +1 -1
- package/dist/javascript/preconditions.js +11 -29
- package/dist/javascript/preconditions.js.map +1 -1
- package/dist/javascript/remove-import.d.ts +1 -0
- package/dist/javascript/remove-import.d.ts.map +1 -1
- package/dist/javascript/remove-import.js +76 -7
- package/dist/javascript/remove-import.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts +4 -0
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +48 -33
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/rpc/request/generate.d.ts +1 -1
- package/dist/rpc/request/generate.d.ts.map +1 -1
- package/dist/rpc/request/generate.js +6 -3
- package/dist/rpc/request/generate.js.map +1 -1
- package/dist/rpc/request/get-languages.d.ts +1 -1
- package/dist/rpc/request/get-languages.d.ts.map +1 -1
- package/dist/rpc/request/get-languages.js +6 -5
- package/dist/rpc/request/get-languages.js.map +1 -1
- package/dist/rpc/request/get-object.d.ts +1 -1
- package/dist/rpc/request/get-object.d.ts.map +1 -1
- package/dist/rpc/request/get-object.js +8 -4
- package/dist/rpc/request/get-object.js.map +1 -1
- package/dist/rpc/request/get-recipes.d.ts +1 -1
- package/dist/rpc/request/get-recipes.d.ts.map +1 -1
- package/dist/rpc/request/get-recipes.js +5 -3
- package/dist/rpc/request/get-recipes.js.map +1 -1
- package/dist/rpc/request/install-recipes.d.ts +1 -1
- package/dist/rpc/request/install-recipes.d.ts.map +1 -1
- package/dist/rpc/request/install-recipes.js +6 -3
- package/dist/rpc/request/install-recipes.js.map +1 -1
- package/dist/rpc/request/metrics.d.ts +42 -0
- package/dist/rpc/request/metrics.d.ts.map +1 -0
- package/dist/rpc/request/metrics.js +184 -0
- package/dist/rpc/request/metrics.js.map +1 -0
- package/dist/rpc/request/parse.d.ts +1 -1
- package/dist/rpc/request/parse.d.ts.map +1 -1
- package/dist/rpc/request/parse.js +7 -3
- package/dist/rpc/request/parse.js.map +1 -1
- package/dist/rpc/request/prepare-recipe.d.ts +1 -1
- package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
- package/dist/rpc/request/prepare-recipe.js +6 -3
- package/dist/rpc/request/prepare-recipe.js.map +1 -1
- package/dist/rpc/request/print.d.ts +1 -1
- package/dist/rpc/request/print.d.ts.map +1 -1
- package/dist/rpc/request/print.js +6 -3
- package/dist/rpc/request/print.js.map +1 -1
- package/dist/rpc/request/visit.d.ts +1 -1
- package/dist/rpc/request/visit.d.ts.map +1 -1
- package/dist/rpc/request/visit.js +8 -4
- package/dist/rpc/request/visit.js.map +1 -1
- package/dist/rpc/rewrite-rpc.d.ts +1 -0
- package/dist/rpc/rewrite-rpc.d.ts.map +1 -1
- package/dist/rpc/rewrite-rpc.js +12 -9
- package/dist/rpc/rewrite-rpc.js.map +1 -1
- package/dist/rpc/server.js +2 -0
- package/dist/rpc/server.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/javascript/parser.ts +13 -1
- package/src/javascript/preconditions.ts +10 -10
- package/src/javascript/remove-import.ts +75 -9
- package/src/javascript/type-mapping.ts +50 -30
- package/src/rpc/request/generate.ts +7 -3
- package/src/rpc/request/get-languages.ts +6 -5
- package/src/rpc/request/get-object.ts +10 -4
- package/src/rpc/request/get-recipes.ts +5 -3
- package/src/rpc/request/install-recipes.ts +6 -3
- package/src/rpc/request/metrics.ts +167 -0
- package/src/rpc/request/parse.ts +12 -6
- package/src/rpc/request/prepare-recipe.ts +7 -3
- package/src/rpc/request/print.ts +7 -3
- package/src/rpc/request/visit.ts +9 -4
- package/src/rpc/rewrite-rpc.ts +14 -9
- 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
|
-
//
|
|
294
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/rpc/request/parse.ts
CHANGED
|
@@ -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
|
|
31
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
/**
|
package/src/rpc/request/print.ts
CHANGED
|
@@ -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
|
|
33
|
-
|
|
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
|
}
|
package/src/rpc/request/visit.ts
CHANGED
|
@@ -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
|
|
41
|
-
|
|
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,
|
|
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,
|
package/src/rpc/rewrite-rpc.ts
CHANGED
|
@@ -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
|
}
|