@openrewrite/rewrite 8.92.2 → 8.92.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/recipe.ts CHANGED
@@ -21,6 +21,12 @@ import {mapAsync} from "./util";
21
21
 
22
22
  const OPTIONS_KEY = "__recipe_options__";
23
23
 
24
+ /**
25
+ * Java's OptionDescriptor declares `type` non-nullable, so an option omitting one
26
+ * degrades to String rather than reporting no type.
27
+ */
28
+ export const DEFAULT_OPTION_TYPE = "String";
29
+
24
30
  export type Minutes = number;
25
31
 
26
32
  export abstract class Recipe {
@@ -100,6 +106,7 @@ export abstract class Recipe {
100
106
  name: key,
101
107
  value: (this as any)[key],
102
108
  required: descriptor.required ?? true,
109
+ type: descriptor.type ?? DEFAULT_OPTION_TYPE,
103
110
  ...descriptor
104
111
  })),
105
112
  preconditions: [],
@@ -154,6 +161,12 @@ export interface OptionDescriptor {
154
161
  readonly required?: boolean
155
162
  readonly example?: string
156
163
  readonly valid?: string[]
164
+
165
+ /**
166
+ * Java simple type name, for example String, Long or Boolean. TypeScript erases types at
167
+ * runtime, so this cannot be derived; defaults to {@link DEFAULT_OPTION_TYPE}.
168
+ */
169
+ readonly type?: string
157
170
  }
158
171
 
159
172
  export abstract class ScanningRecipe<P> extends Recipe {
package/src/rpc/queue.ts CHANGED
@@ -316,6 +316,60 @@ export class RpcSendQueue {
316
316
  }
317
317
  }
318
318
 
319
+ /**
320
+ * Collapses repeated strings decoded from RPC messages to a single instance. Shared across every
321
+ * {@link RpcReceiveQueue} of a connection so the same discriminators, enum values and whitespace
322
+ * are deduplicated across the whole run rather than only within one received object.
323
+ */
324
+ export class StringInternTable {
325
+ private readonly strings = new Map<string, string>();
326
+
327
+ constructor(private readonly maxEntries = 1 << 16,
328
+ private readonly maxValueLength = 200) {
329
+ }
330
+
331
+ /**
332
+ * Interns a `kind`/`valueType` discriminator. This set is bounded by the number of LST node
333
+ * types, so it is always interned and never subject to the cap.
334
+ */
335
+ internType(value: string): string {
336
+ const existing = this.strings.get(value);
337
+ if (existing !== undefined) {
338
+ return existing;
339
+ }
340
+ this.strings.set(value, value);
341
+ return value;
342
+ }
343
+
344
+ /**
345
+ * Interns a scalar string value, which is unbounded in principle. Only short strings are
346
+ * interned (whitespace and short tokens dominate the duplication), and the table stops growing
347
+ * at a cap so it can never turn into a leak; past either limit the original is returned as-is.
348
+ */
349
+ internValue(value: string): string {
350
+ if (value.length > this.maxValueLength) {
351
+ return value;
352
+ }
353
+ const existing = this.strings.get(value);
354
+ if (existing !== undefined) {
355
+ return existing;
356
+ }
357
+ if (this.strings.size >= this.maxEntries) {
358
+ return value;
359
+ }
360
+ this.strings.set(value, value);
361
+ return value;
362
+ }
363
+
364
+ clear(): void {
365
+ this.strings.clear();
366
+ }
367
+
368
+ get size(): number {
369
+ return this.strings.size;
370
+ }
371
+ }
372
+
319
373
  export class RpcReceiveQueue {
320
374
  private batch: RpcObjectData[] = [];
321
375
  private batchIndex = 0;
@@ -325,7 +379,8 @@ export class RpcReceiveQueue {
325
379
  private readonly sourceFileType: string | undefined,
326
380
  private readonly pull: () => Promise<RpcObjectData[]>,
327
381
  private readonly logger: rpc.Logger | undefined,
328
- private readonly trace: boolean) {
382
+ private readonly trace: boolean,
383
+ private readonly internedStrings: StringInternTable = new StringInternTable()) {
329
384
  }
330
385
 
331
386
  /**
@@ -432,7 +487,9 @@ export class RpcReceiveQueue {
432
487
  } else if ((codec = RpcCodecs.forInstance(before, this.sourceFileType))) {
433
488
  after = await codec.rpcReceive(before, this);
434
489
  } else if (message.value !== undefined) {
435
- after = message.valueType ? {kind: message.valueType, ...message.value} : message.value;
490
+ after = message.valueType ?
491
+ {kind: this.internedStrings.internType(message.valueType), ...message.value} :
492
+ typeof message.value === "string" ? this.internedStrings.internValue(message.value) : message.value;
436
493
  } else if (message.state === RpcObjectState.ADD && message.valueType) {
437
494
  throw new Error(
438
495
  `No RPC codec registered on the TypeScript side for '${message.valueType}'. ` +
@@ -515,7 +572,7 @@ export class RpcReceiveQueue {
515
572
  if (codec?.rpcNew) {
516
573
  return codec.rpcNew();
517
574
  }
518
- return {kind: type} as T;
575
+ return {kind: this.internedStrings.internType(type)} as T;
519
576
  }
520
577
  }
521
578
 
@@ -40,7 +40,7 @@ import {
40
40
  import {DataTableStore} from "../data-table";
41
41
  import {RecipeMarketplace} from "../marketplace";
42
42
  import {initializeMetricsCsv, setCacheSizeProvider} from "./request/metrics";
43
- import {RpcObjectData, RpcObjectState, RpcReceiveQueue} from "./queue";
43
+ import {RpcObjectData, RpcObjectState, RpcReceiveQueue, StringInternTable} from "./queue";
44
44
  import {RpcRecipe} from "./recipe";
45
45
  import {ExecutionContext} from "../execution";
46
46
  import {InstallRecipes, InstallRecipesResponse} from "./request/install-recipes";
@@ -68,6 +68,11 @@ export class RewriteRpc {
68
68
  readonly remoteRefs: Map<number, any> = new Map();
69
69
  readonly localRefs: ReferenceMap = new ReferenceMap();
70
70
 
71
+ // One table for the whole connection so repeated discriminators, enum values and whitespace
72
+ // decoded across the many getObject calls of a run collapse to a single string instance rather
73
+ // than one per received object.
74
+ private readonly internedStrings = new StringInternTable();
75
+
71
76
  // Ref high-water per source file, captured before it is first visited so an Evict rolls
72
77
  // back exactly the refs it introduced. `send` = localRefs snapshot, `recvMax` = max remoteRefs key.
73
78
  readonly refCheckpoints: Map<string, { send: number, recvMax: number }> = new Map();
@@ -158,6 +163,7 @@ export class RewriteRpc {
158
163
  this.remoteRefs.clear();
159
164
  this.localRefs.clear();
160
165
  this.refCheckpoints.clear();
166
+ this.internedStrings.clear();
161
167
  preparedRecipes.clear();
162
168
  this.remoteLanguages = undefined;
163
169
  };
@@ -262,7 +268,7 @@ export class RewriteRpc {
262
268
  nextPage = requestPage();
263
269
  }
264
270
  return page;
265
- }, this.logger, this.traceGetObject.receive);
271
+ }, this.logger, this.traceGetObject.receive, this.internedStrings);
266
272
 
267
273
  let remoteObject: P;
268
274
  try {
package/src/uuid.ts CHANGED
@@ -35,12 +35,19 @@ function fallbackRandomId(): UUID {
35
35
  }
36
36
 
37
37
  /**
38
- * Generate a random UUID v4.
39
- *
40
38
  * Uses native crypto.randomUUID() on Node 14.17.0+, falls back to
41
39
  * crypto.randomBytes() on older versions. The implementation is
42
40
  * selected once at module load time to avoid per-call overhead.
43
41
  */
44
- export const randomId: () => UUID = typeof crypto.randomUUID === 'function'
42
+ const generateId: () => UUID = typeof crypto.randomUUID === 'function'
45
43
  ? crypto.randomUUID
46
44
  : fallbackRandomId;
45
+
46
+ /**
47
+ * Generate a random UUID v4.
48
+ */
49
+ export const randomId: () => UUID = () =>
50
+ // Both randomUUID() and concatenation yield an unflattened cons-string. An id is only ever
51
+ // compared or serialized, so nothing forces a flatten and each would be retained as a rope of
52
+ // tiny heap nodes for the life of the LST; the round-trip flattens it to a sequential string.
53
+ Buffer.from(generateId(), 'latin1').toString('latin1');