@openrewrite/rewrite 8.96.5 → 8.97.0

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 (30) hide show
  1. package/dist/java/tree.d.ts +2 -1
  2. package/dist/java/tree.d.ts.map +1 -1
  3. package/dist/java/tree.js.map +1 -1
  4. package/dist/java/visitor.d.ts +5 -0
  5. package/dist/java/visitor.d.ts.map +1 -1
  6. package/dist/java/visitor.js +264 -231
  7. package/dist/java/visitor.js.map +1 -1
  8. package/dist/javascript/dependency-workspace.d.ts +8 -2
  9. package/dist/javascript/dependency-workspace.d.ts.map +1 -1
  10. package/dist/javascript/dependency-workspace.js +36 -4
  11. package/dist/javascript/dependency-workspace.js.map +1 -1
  12. package/dist/javascript/templating/engine.d.ts +1 -0
  13. package/dist/javascript/templating/engine.d.ts.map +1 -1
  14. package/dist/javascript/templating/engine.js +28 -4
  15. package/dist/javascript/templating/engine.js.map +1 -1
  16. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  17. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  18. package/dist/javascript/templating/placeholder-replacement.js +24 -3
  19. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  20. package/dist/javascript/visitor.d.ts.map +1 -1
  21. package/dist/javascript/visitor.js +135 -121
  22. package/dist/javascript/visitor.js.map +1 -1
  23. package/dist/rewrite-javascript-version.txt +1 -1
  24. package/package.json +1 -1
  25. package/src/java/tree.ts +2 -1
  26. package/src/java/visitor.ts +263 -230
  27. package/src/javascript/dependency-workspace.ts +35 -3
  28. package/src/javascript/templating/engine.ts +30 -3
  29. package/src/javascript/templating/placeholder-replacement.ts +24 -3
  30. package/src/javascript/visitor.ts +139 -121
@@ -229,6 +229,7 @@ export class DependencyWorkspace {
229
229
  // Write package files
230
230
  writePackageFiles(targetDir);
231
231
 
232
+ this.markUsed(cachedWorkspaceDir);
232
233
  return targetDir;
233
234
  } catch (symlinkError) {
234
235
  // Symlink failed (e.g., cross-device, permissions) - fall through to npm install
@@ -257,6 +258,7 @@ export class DependencyWorkspace {
257
258
  // Check cache
258
259
  const cached = this.cache.get(hash);
259
260
  if (cached && fs.existsSync(cached) && this.isWorkspaceValid(cached, depsForValidation)) {
261
+ this.markUsed(cached);
260
262
  return cached;
261
263
  }
262
264
 
@@ -266,6 +268,7 @@ export class DependencyWorkspace {
266
268
  // Check if valid workspace already exists on disk (cross-VM reuse)
267
269
  if (fs.existsSync(workspaceDir) && this.isWorkspaceValid(workspaceDir, depsForValidation)) {
268
270
  this.cache.set(hash, workspaceDir);
271
+ this.markUsed(workspaceDir);
269
272
  return workspaceDir;
270
273
  }
271
274
 
@@ -390,6 +393,19 @@ export class DependencyWorkspace {
390
393
  return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
391
394
  }
392
395
 
396
+ /**
397
+ * Marks a workspace used now, so that a cleanup ages it from its last use rather than from its install, and never
398
+ * deletes one a test run or a recipe has just used.
399
+ */
400
+ private static markUsed(workspaceDir: string): void {
401
+ try {
402
+ const now = new Date();
403
+ fs.utimesSync(workspaceDir, now, now);
404
+ } catch {
405
+ // At worst, a cleanup deletes it once it is a day old, and it is installed again.
406
+ }
407
+ }
408
+
393
409
  /**
394
410
  * Checks if a workspace is valid (has node_modules and matching package.json).
395
411
  * Handles both real node_modules directories and symlinks to cached workspaces.
@@ -444,6 +460,15 @@ export class DependencyWorkspace {
444
460
  return false;
445
461
  }
446
462
  }
463
+
464
+ // And installed: a workspace whose node_modules has lost a package, as a deletion cut short leaves
465
+ // one, would otherwise be reused without it, and nothing it declares would be type attributed. Its
466
+ // devDependencies are not asked for, which npm leaves out under NODE_ENV=production.
467
+ for (const name of Object.keys(packageJsonContent.dependencies ?? {})) {
468
+ if (!fs.existsSync(path.join(nodeModules, name, 'package.json'))) {
469
+ return false;
470
+ }
471
+ }
447
472
  } catch (error) {
448
473
  return false;
449
474
  }
@@ -454,11 +479,12 @@ export class DependencyWorkspace {
454
479
 
455
480
  /**
456
481
  * Cleans up old workspace directories.
457
- * Removes workspaces older than the specified age.
482
+ * Removes workspaces unused for longer than the specified age, each moved out of its key's way before it is
483
+ * deleted, so that a deletion cut short leaves a temporary directory rather than part of a workspace.
458
484
  * Also removes all temporary directories (*.tmp-*) regardless of age,
459
485
  * as these indicate incomplete/crashed operations.
460
486
  *
461
- * @param maxAgeMs Maximum age in milliseconds (default: 24 hours)
487
+ * @param maxAgeMs Maximum age in milliseconds since a workspace was last used (default: 24 hours)
462
488
  */
463
489
  static cleanupOldWorkspaces(maxAgeMs: number = 24 * 60 * 60 * 1000): void {
464
490
  if (!fs.existsSync(this.WORKSPACE_BASE)) {
@@ -508,7 +534,12 @@ export class DependencyWorkspace {
508
534
  const age = now - stats.mtimeMs;
509
535
 
510
536
  if (age > maxAgeMs) {
511
- fs.rmSync(workspaceDir, {recursive: true, force: true});
537
+ // Out of its key's way in one rename, then deleted: a process looking the workspace up finds it
538
+ // whole or not at all. A deletion cut short, as when the RPC server is killed while it shuts
539
+ // down, then leaves a temporary directory of a process that has gone, which the next cleanup
540
+ // removes, rather than a workspace that has lost part of its node_modules and still looks valid.
541
+ const deleting = `${workspaceDir}.tmp-${process.pid}-${Date.now()}-deleting`;
542
+ fs.renameSync(workspaceDir, deleting);
512
543
  // Remove from cache
513
544
  for (const [hash, dir] of this.cache.entries()) {
514
545
  if (dir === workspaceDir) {
@@ -516,6 +547,7 @@ export class DependencyWorkspace {
516
547
  break;
517
548
  }
518
549
  }
550
+ fs.rmSync(deleting, {recursive: true, force: true});
519
551
  }
520
552
  } catch (error) {
521
553
  // Ignore errors, workspace might be in use
@@ -481,9 +481,15 @@ export class TemplateEngine {
481
481
  * Converts a Type instance to a TypeScript type string.
482
482
  *
483
483
  * @param type The Type instance
484
+ * @param enclosing The types that `type` is a type argument or element type of
484
485
  * @returns A TypeScript type string
485
486
  */
486
- private static typeToString(type: Type): string {
487
+ private static typeToString(type: Type, enclosing: Type[] = []): string {
488
+ // A recursive type, such as `type A = Array<A>`, is any where it holds itself
489
+ if (enclosing.includes(type)) {
490
+ return 'any';
491
+ }
492
+
487
493
  // Handle Type.Class and Type.ShallowClass - return their fully qualified names
488
494
  if (type.kind === Type.Kind.Class || type.kind === Type.Kind.ShallowClass) {
489
495
  const classType = type as Type.Class;
@@ -518,10 +524,18 @@ export class TemplateEngine {
518
524
  // Handle Type.Array - render component type plus []
519
525
  if (type.kind === Type.Kind.Array) {
520
526
  const arrayType = type as Type.Array;
521
- const componentTypeString = this.typeToString(arrayType.elemType);
527
+ const componentTypeString = this.typeToString(arrayType.elemType, [...enclosing, type]);
522
528
  return `${componentTypeString}[]`;
523
529
  }
524
530
 
531
+ // An array, a map or any other generic type, with its type arguments
532
+ if (type.kind === Type.Kind.Parameterized) {
533
+ const parameterized = type as Type.Parameterized;
534
+ const base = this.typeToString(parameterized.type);
535
+ return base === 'any' || parameterized.typeParameters.length === 0 ? base :
536
+ `${base}<${parameterized.typeParameters.map(t => this.typeToString(t, [...enclosing, type])).join(', ')}>`;
537
+ }
538
+
525
539
  // For other types, return 'any' as a fallback
526
540
  // TODO: Implement proper Type to string conversion for other Type.Kind values
527
541
  return 'any';
@@ -770,7 +784,8 @@ function expectsStatement(parentTree: J, childId: string): boolean {
770
784
  parentTree.kind === J.Kind.Label ||
771
785
  parentTree.kind === J.Kind.WhileLoop ||
772
786
  parentTree.kind === JS.Kind.CompilationUnit ||
773
- parentTree.kind === JS.Kind.ForInLoop;
787
+ parentTree.kind === JS.Kind.ForInLoop ||
788
+ parentTree.kind === JS.Kind.WithStatement;
774
789
  }
775
790
 
776
791
  /** Whether the code opens with `{`, which is a block where a statement goes and an object literal elsewhere. */
@@ -917,6 +932,18 @@ export class TemplateApplier {
917
932
  private wrapTree(originalTree: J, resultToUse: J) {
918
933
  const parentTree = this.cursor?.parentTree()?.value;
919
934
 
935
+ // A `for` head holds each expression of its init and update in an expression statement, as the parser does
936
+ if (parentTree?.kind === J.Kind.ForLoopControl &&
937
+ (parentTree as J.ForLoop.Control).condition?.element.id !== originalTree.id) {
938
+ return isExpression(resultToUse) && resultToUse.kind !== JS.Kind.ExpressionStatement ? {
939
+ kind: JS.Kind.ExpressionStatement,
940
+ id: randomId(),
941
+ prefix: resultToUse.prefix,
942
+ markers: wrapperMarkers(resultToUse),
943
+ expression: {...resultToUse, prefix: emptySpace}
944
+ } as JS.ExpressionStatement : resultToUse;
945
+ }
946
+
920
947
  // Only apply wrapping logic if we have parent context
921
948
  if (parentTree) {
922
949
  const parentExpectsStatement = expectsStatement(parentTree, originalTree.id);
@@ -53,6 +53,13 @@ function isObjectMember(value: unknown): value is J {
53
53
  return false;
54
54
  }
55
55
 
56
+ /** `padded` less the trailing comma it brought from the end of the list it was taken from. */
57
+ function withoutTrailingComma(padded: J.RightPadded<J>): J.RightPadded<J> {
58
+ const markers = padded.markers?.markers;
59
+ return markers?.some(m => m.kind === J.Markers.TrailingComma) ?
60
+ {...padded, markers: {...padded.markers, markers: markers.filter(m => m.kind !== J.Markers.TrailingComma)}} : padded;
61
+ }
62
+
56
63
  /**
57
64
  * Visitor that replaces placeholder nodes with actual parameter values.
58
65
  */
@@ -331,6 +338,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
331
338
  const param = this.substitutions.get(placeholderText);
332
339
  if (param) {
333
340
  let arrayToExpand: J[] | J.RightPadded<J>[] | undefined = undefined;
341
+ const member = element.kind === JS.Kind.PropertyAssignment ? this.objectMember(param.value) : undefined;
334
342
 
335
343
  // Check if it's a J.Container
336
344
  const isContainer = param.value && typeof param.value === 'object' &&
@@ -344,8 +352,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
344
352
  arrayToExpand = param.value as J[];
345
353
  }
346
354
  // One member of an object literal fills the slot of a member, not the name of a property
347
- else if (element.kind === JS.Kind.PropertyAssignment && isObjectMember(param.value)) {
348
- arrayToExpand = [param.value];
355
+ else if (member) {
356
+ arrayToExpand = [member] as J[] | J.RightPadded<J>[];
349
357
  }
350
358
  // Check if it's a CaptureValue
351
359
  else if (param.value instanceof CaptureValue) {
@@ -451,7 +459,20 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
451
459
  }
452
460
  }
453
461
 
454
- return newElements;
462
+ // Only the last element prints the trailing comma that a given or captured padding brings along
463
+ return newElements.map((padded, i) => i === newElements.length - 1 ? padded : withoutTrailingComma(padded));
464
+ }
465
+
466
+ /** The member of an object literal, padded or not, that a parameter is or is bound to, if it is one. */
467
+ private objectMember(value: any): J | J.RightPadded<J> | undefined {
468
+ if (value instanceof CaptureValue) {
469
+ value = value.resolve(this.values);
470
+ } else if (value instanceof TemplateParamImpl) {
471
+ value = this.values.get(value.name);
472
+ } else if (value instanceof CaptureImpl || (value && typeof value === 'object' && value[CAPTURE_NAME_SYMBOL])) {
473
+ value = this.values.get(value[CAPTURE_NAME_SYMBOL] || value.name);
474
+ }
475
+ return isObjectMember(value?.kind === J.Kind.RightPadded ? value.element : value) ? value : undefined;
455
476
  }
456
477
 
457
478
  /** As `visit` on a placeholder, with `merge` choosing the replacement's prefix. */