@openrewrite/rewrite 8.96.4 → 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 (135) hide show
  1. package/dist/execution.d.ts +2 -0
  2. package/dist/execution.d.ts.map +1 -1
  3. package/dist/execution.js +4 -0
  4. package/dist/execution.js.map +1 -1
  5. package/dist/heavyweight.d.ts +8 -0
  6. package/dist/heavyweight.d.ts.map +1 -0
  7. package/dist/heavyweight.js +45 -0
  8. package/dist/heavyweight.js.map +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/java/tree.d.ts +2 -1
  14. package/dist/java/tree.d.ts.map +1 -1
  15. package/dist/java/tree.js.map +1 -1
  16. package/dist/java/visitor.d.ts +5 -0
  17. package/dist/java/visitor.d.ts.map +1 -1
  18. package/dist/java/visitor.js +264 -217
  19. package/dist/java/visitor.js.map +1 -1
  20. package/dist/javascript/add-import.d.ts +2 -1
  21. package/dist/javascript/add-import.d.ts.map +1 -1
  22. package/dist/javascript/add-import.js +14 -2
  23. package/dist/javascript/add-import.js.map +1 -1
  24. package/dist/javascript/amd.d.ts.map +1 -1
  25. package/dist/javascript/amd.js +2 -1
  26. package/dist/javascript/amd.js.map +1 -1
  27. package/dist/javascript/cleanup/add-parse-int-radix.js +1 -1
  28. package/dist/javascript/cleanup/add-parse-int-radix.js.map +1 -1
  29. package/dist/javascript/dependency-workspace.d.ts +8 -2
  30. package/dist/javascript/dependency-workspace.d.ts.map +1 -1
  31. package/dist/javascript/dependency-workspace.js +36 -4
  32. package/dist/javascript/dependency-workspace.js.map +1 -1
  33. package/dist/javascript/format/format.d.ts +10 -0
  34. package/dist/javascript/format/format.d.ts.map +1 -1
  35. package/dist/javascript/format/format.js +10 -2
  36. package/dist/javascript/format/format.js.map +1 -1
  37. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
  38. package/dist/javascript/format/tabs-and-indents-visitor.js +4 -0
  39. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  40. package/dist/javascript/format/whitespace-reconciler.d.ts.map +1 -1
  41. package/dist/javascript/format/whitespace-reconciler.js +4 -0
  42. package/dist/javascript/format/whitespace-reconciler.js.map +1 -1
  43. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  44. package/dist/javascript/node-resolution-result.js +2 -0
  45. package/dist/javascript/node-resolution-result.js.map +1 -1
  46. package/dist/javascript/parser-utils.d.ts.map +1 -1
  47. package/dist/javascript/parser-utils.js +9 -6
  48. package/dist/javascript/parser-utils.js.map +1 -1
  49. package/dist/javascript/templating/engine.d.ts +1 -0
  50. package/dist/javascript/templating/engine.d.ts.map +1 -1
  51. package/dist/javascript/templating/engine.js +39 -10
  52. package/dist/javascript/templating/engine.js.map +1 -1
  53. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  54. package/dist/javascript/templating/pattern.js +8 -0
  55. package/dist/javascript/templating/pattern.js.map +1 -1
  56. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  57. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  58. package/dist/javascript/templating/placeholder-replacement.js +37 -1
  59. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  60. package/dist/javascript/visitor.d.ts.map +1 -1
  61. package/dist/javascript/visitor.js +153 -118
  62. package/dist/javascript/visitor.js.map +1 -1
  63. package/dist/recipe.d.ts +5 -0
  64. package/dist/recipe.d.ts.map +1 -1
  65. package/dist/recipe.js +5 -0
  66. package/dist/recipe.js.map +1 -1
  67. package/dist/rewrite-javascript-version.txt +1 -1
  68. package/dist/run.d.ts +7 -1
  69. package/dist/run.d.ts.map +1 -1
  70. package/dist/run.js +40 -2
  71. package/dist/run.js.map +1 -1
  72. package/dist/test/accumulator-contents.d.ts +15 -0
  73. package/dist/test/accumulator-contents.d.ts.map +1 -0
  74. package/dist/test/accumulator-contents.js +185 -0
  75. package/dist/test/accumulator-contents.js.map +1 -0
  76. package/dist/test/empty-sources.d.ts +14 -0
  77. package/dist/test/empty-sources.d.ts.map +1 -0
  78. package/dist/test/empty-sources.js +98 -0
  79. package/dist/test/empty-sources.js.map +1 -0
  80. package/dist/test/formatting-scope.d.ts +18 -0
  81. package/dist/test/formatting-scope.d.ts.map +1 -0
  82. package/dist/test/formatting-scope.js +244 -0
  83. package/dist/test/formatting-scope.js.map +1 -0
  84. package/dist/test/probes.d.ts +25 -0
  85. package/dist/test/probes.d.ts.map +1 -0
  86. package/dist/test/probes.js +189 -0
  87. package/dist/test/probes.js.map +1 -0
  88. package/dist/test/rewrite-test.d.ts +27 -0
  89. package/dist/test/rewrite-test.d.ts.map +1 -1
  90. package/dist/test/rewrite-test.js +191 -21
  91. package/dist/test/rewrite-test.js.map +1 -1
  92. package/dist/test/tree-shapes.d.ts +15 -0
  93. package/dist/test/tree-shapes.d.ts.map +1 -0
  94. package/dist/test/tree-shapes.js +288 -0
  95. package/dist/test/tree-shapes.js.map +1 -0
  96. package/dist/test/unparsed-sources.d.ts +17 -0
  97. package/dist/test/unparsed-sources.d.ts.map +1 -0
  98. package/dist/test/unparsed-sources.js +113 -0
  99. package/dist/test/unparsed-sources.js.map +1 -0
  100. package/dist/tree.d.ts.map +1 -1
  101. package/dist/tree.js +2 -0
  102. package/dist/tree.js.map +1 -1
  103. package/dist/visitor.d.ts.map +1 -1
  104. package/dist/visitor.js +2 -0
  105. package/dist/visitor.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/execution.ts +6 -0
  108. package/src/heavyweight.ts +42 -0
  109. package/src/index.ts +1 -0
  110. package/src/java/tree.ts +2 -1
  111. package/src/java/visitor.ts +263 -216
  112. package/src/javascript/add-import.ts +11 -2
  113. package/src/javascript/amd.ts +4 -2
  114. package/src/javascript/cleanup/add-parse-int-radix.ts +2 -2
  115. package/src/javascript/dependency-workspace.ts +35 -3
  116. package/src/javascript/format/format.ts +18 -0
  117. package/src/javascript/format/tabs-and-indents-visitor.ts +4 -0
  118. package/src/javascript/format/whitespace-reconciler.ts +4 -0
  119. package/src/javascript/node-resolution-result.ts +3 -0
  120. package/src/javascript/parser-utils.ts +10 -6
  121. package/src/javascript/templating/engine.ts +41 -9
  122. package/src/javascript/templating/pattern.ts +10 -0
  123. package/src/javascript/templating/placeholder-replacement.ts +38 -1
  124. package/src/javascript/visitor.ts +166 -118
  125. package/src/recipe.ts +5 -0
  126. package/src/run.ts +30 -2
  127. package/src/test/accumulator-contents.ts +194 -0
  128. package/src/test/empty-sources.ts +99 -0
  129. package/src/test/formatting-scope.ts +231 -0
  130. package/src/test/probes.ts +156 -0
  131. package/src/test/rewrite-test.ts +177 -24
  132. package/src/test/tree-shapes.ts +332 -0
  133. package/src/test/unparsed-sources.ts +138 -0
  134. package/src/tree.ts +3 -0
  135. package/src/visitor.ts +3 -0
@@ -12,6 +12,7 @@ import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, i
12
12
  import {create as produce, Draft} from "mutative";
13
13
  import {autoFormat} from "./format";
14
14
  import {getPrettierStyle} from "./format/prettier-format";
15
+ import ts from "typescript";
15
16
 
16
17
  export type QuoteChar = "'" | '"';
17
18
 
@@ -562,7 +563,8 @@ function claimedName(v: unknown): string | undefined {
562
563
  /**
563
564
  * The parser lifts surrogate pairs out of `valueSource` into `unicodeEscapes` and, when it does,
564
565
  * sets `value` to the quoted source (`parser.ts` `mapLiteral`), so neither field alone is the
565
- * module name. Reuniting them and stripping the quotes yields it for either shape of literal.
566
+ * module name. Reuniting them and reading the quoted text as the language does yields it for
567
+ * either shape of literal.
566
568
  */
567
569
  export function moduleNameOf(module: string | J.Literal): string {
568
570
  if (typeof module === 'string') {
@@ -579,6 +581,13 @@ export function moduleNameOf(module: string | J.Literal): string {
579
581
  cut = escape.valueSourceIndex;
580
582
  }
581
583
  restored += source.slice(cut);
584
+ // an escape such as `\u00dc` names the character it spells
585
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, restored);
586
+ const token = scanner.scan();
587
+ if ((token === ts.SyntaxKind.StringLiteral || token === ts.SyntaxKind.NoSubstitutionTemplateLiteral) &&
588
+ scanner.getTokenEnd() === restored.length) {
589
+ return scanner.getTokenValue();
590
+ }
582
591
  const quote = restored.charAt(0);
583
592
  return (quote === "'" || quote === '"' || quote === '`') && restored.endsWith(quote) && restored.length > 1
584
593
  ? restored.slice(1, -1)
@@ -1583,7 +1592,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1583
1592
  value: this.module,
1584
1593
  valueSource,
1585
1594
  unicodeEscapes: this.moduleUnicodeEscapes,
1586
- type: undefined
1595
+ type: Type.Primitive.String
1587
1596
  };
1588
1597
  }
1589
1598
 
@@ -25,7 +25,8 @@ import {
25
25
  rightPadded,
26
26
  space,
27
27
  Statement,
28
- TrailingComma
28
+ TrailingComma,
29
+ Type
29
30
  } from "../java";
30
31
  import {JS} from "./tree";
31
32
  import {moduleNameOf} from "./add-import";
@@ -428,7 +429,8 @@ function dependencyLiteral(module: string, quote: string): J.Literal {
428
429
  prefix: emptySpace,
429
430
  markers: emptyMarkers,
430
431
  value: module,
431
- valueSource: `${quote}${module}${quote}`
432
+ valueSource: `${quote}${module}${quote}`,
433
+ type: Type.Primitive.String
432
434
  };
433
435
  }
434
436
 
@@ -18,7 +18,7 @@ import {Recipe} from "../../recipe";
18
18
  import {TreeVisitor} from "../../visitor";
19
19
  import {ExecutionContext} from "../../execution";
20
20
  import {JavaScriptVisitor} from "../visitor";
21
- import {J, singleSpace} from "../../java";
21
+ import {J, singleSpace, Type} from "../../java";
22
22
  import {emptyMarkers} from "../../markers";
23
23
  import {randomId} from "../../uuid";
24
24
 
@@ -73,7 +73,7 @@ export class AddParseIntRadix extends Recipe {
73
73
  value: 10,
74
74
  valueSource: '10',
75
75
  unicodeEscapes: undefined,
76
- type: undefined
76
+ type: Type.Primitive.Double
77
77
  };
78
78
 
79
79
  return {
@@ -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
@@ -26,6 +26,7 @@ import {TabsAndIndentsVisitor} from "./tabs-and-indents-visitor";
26
26
  import {NormalizeWhitespaceVisitor} from "./normalize-whitespace-visitor";
27
27
  import {MinimumViableSpacingVisitor} from "./minimum-viable-spacing-visitor";
28
28
  import {applyPrettierFormatting, getPrettierStyle} from "./prettier-format";
29
+ import {AsyncLocalStorage} from "node:async_hooks";
29
30
 
30
31
  export {TabsAndIndentsVisitor} from "./tabs-and-indents-visitor";
31
32
  export {NormalizeWhitespaceVisitor} from "./normalize-whitespace-visitor";
@@ -56,6 +57,22 @@ export const autoIndent = async <J2 extends J, P>(j: J2, p: P, parent?: Cursor):
56
57
  return await new TabsAndIndentsVisitor(style).visit(j, p, parent) as J2;
57
58
  }
58
59
 
60
+ /**
61
+ * Hears about each request to auto-format a tree made while it listens, so that a test harness can check that a
62
+ * recipe formats only what it changed.
63
+ */
64
+ export interface FormattingListener {
65
+ /** AutoformatVisitor calls this before it formats `tree`, stopping after `stopAfter` when it is given. */
66
+ onFormat(tree: Tree, stopAfter: Tree | undefined, cursor: Cursor | undefined): void;
67
+ }
68
+
69
+ const formattingListener = new AsyncLocalStorage<FormattingListener>();
70
+
71
+ /** Runs `run`, telling `listener` about the formatting it requests. */
72
+ export function listenToFormatting<T>(listener: FormattingListener, run: () => Promise<T>): Promise<T> {
73
+ return formattingListener.run(listener, run);
74
+ }
75
+
59
76
  /**
60
77
  * Formats JavaScript/TypeScript code using a comprehensive set of formatting rules.
61
78
  *
@@ -77,6 +94,7 @@ export class AutoformatVisitor<P> extends JavaScriptVisitor<P> {
77
94
  }
78
95
 
79
96
  async visit<R extends J>(tree: Tree, p: P, cursor?: Cursor): Promise<R | undefined> {
97
+ formattingListener.getStore()?.onFormat(tree, this.stopAfter, cursor);
80
98
  const prettierStyle = getPrettierStyle(tree, cursor, this.styles);
81
99
  if (prettierStyle) {
82
100
  const formatted = await applyPrettierFormatting(tree as R, prettierStyle, p, cursor, this.stopAfter);
@@ -408,6 +408,10 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
408
408
  return literal;
409
409
  }
410
410
  return produce(literal, draft => {
411
+ // JSX text is its own value, as the parser makes it
412
+ if (draft.value === draft.valueSource) {
413
+ draft.value = normalizedValueSource;
414
+ }
411
415
  draft.valueSource = normalizedValueSource;
412
416
  });
413
417
  }
@@ -416,6 +416,10 @@ export class WhitespaceReconciler {
416
416
  original: J.Identifier | J.Literal,
417
417
  formatted: J.Identifier | J.Literal
418
418
  ): J.Identifier | J.Literal {
419
+ // A literal's type follows from how it is written, so an identifier's is not the literal's
420
+ if (isLiteral(formatted)) {
421
+ return formatted;
422
+ }
419
423
  const result: Record<string, unknown> = { ...formatted };
420
424
 
421
425
  // Preserve type attribution - both Identifier and Literal have `type`
@@ -19,6 +19,7 @@ import {asRef} from "../reference";
19
19
  import {RpcCodecs, RpcReceiveQueue, RpcSendQueue} from "../rpc";
20
20
  import {castDraft} from "mutative";
21
21
  import {updateIfChanged} from "../util";
22
+ import {registerHeavyweight} from "../heavyweight";
22
23
  import * as semver from "semver";
23
24
  import * as fsp from "fs/promises";
24
25
  import * as path from "path";
@@ -259,6 +260,8 @@ export interface NodeResolutionResult extends Marker {
259
260
  readonly npmrcConfigs?: Npmrc[];
260
261
  }
261
262
 
263
+ registerHeavyweight(NodeResolutionResultKind);
264
+
262
265
  /**
263
266
  * Creates a NodeResolutionResult marker from a package.json file.
264
267
  * Should be called during parsing to attach to JS.CompilationUnit.
@@ -17,7 +17,8 @@ import * as ts from "typescript";
17
17
  import {Expression, J, Statement} from "../java";
18
18
  import {JS} from "./tree";
19
19
 
20
- const is_statements = new Set<string>([
20
+ // Built on first use, since ./tree imports the visitor, which imports this module
21
+ const statementKinds = (): string[] => [
21
22
  J.Kind.Assert,
22
23
  J.Kind.Assignment,
23
24
  J.Kind.AssignmentOperation,
@@ -78,9 +79,9 @@ const is_statements = new Set<string>([
78
79
  JS.Kind.WithStatement,
79
80
  JS.Kind.ExpressionStatement,
80
81
  JS.Kind.StatementExpression
81
- ])
82
+ ];
82
83
 
83
- const is_expressions = new Set<string>([
84
+ const expressionKinds = (): string[] => [
84
85
  J.Kind.AnnotatedType,
85
86
  J.Kind.Annotation,
86
87
  J.Kind.ArrayAccess,
@@ -157,14 +158,17 @@ const is_expressions = new Set<string>([
157
158
  JS.Kind.Void,
158
159
  JS.Kind.ExpressionStatement,
159
160
  JS.Kind.StatementExpression
160
- ])
161
+ ];
162
+
163
+ let is_statements: Set<string> | undefined;
164
+ let is_expressions: Set<string> | undefined;
161
165
 
162
166
  export function isStatement(statement: J): statement is Statement {
163
- return is_statements.has(statement.kind);
167
+ return (is_statements ??= new Set(statementKinds())).has(statement.kind);
164
168
  }
165
169
 
166
170
  export function isExpression(expression: J): expression is Expression {
167
- return is_expressions.has(expression.kind);
171
+ return (is_expressions ??= new Set(expressionKinds())).has(expression.kind);
168
172
  }
169
173
 
170
174
  export function getNextSibling(node: ts.Node): ts.Node | undefined {
@@ -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';
@@ -756,17 +770,22 @@ class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
756
770
  }
757
771
 
758
772
  // FIXME: This is a heuristic to determine if the parent expects a statement child
759
- function expectsStatement(parentTree: J): boolean {
773
+ function expectsStatement(parentTree: J, childId: string): boolean {
774
+ if (parentTree.kind === J.Kind.Case) {
775
+ // the labels of a case are expressions
776
+ return !(parentTree as J.Case).caseLabels.elements.some(label => label.element.id === childId);
777
+ }
760
778
  return parentTree.kind === J.Kind.Block ||
761
- parentTree.kind === J.Kind.Case ||
762
779
  parentTree.kind === J.Kind.DoWhileLoop ||
763
780
  parentTree.kind === J.Kind.ForEachLoop ||
764
781
  parentTree.kind === J.Kind.ForLoop ||
765
782
  parentTree.kind === J.Kind.If ||
766
783
  parentTree.kind === J.Kind.IfElse ||
784
+ parentTree.kind === J.Kind.Label ||
767
785
  parentTree.kind === J.Kind.WhileLoop ||
768
786
  parentTree.kind === JS.Kind.CompilationUnit ||
769
- parentTree.kind === JS.Kind.ForInLoop;
787
+ parentTree.kind === JS.Kind.ForInLoop ||
788
+ parentTree.kind === JS.Kind.WithStatement;
770
789
  }
771
790
 
772
791
  /** Whether the code opens with `{`, which is a block where a statement goes and an object literal elsewhere. */
@@ -782,7 +801,7 @@ export function replacedByObjectLiteral(tree: J, cursor: Cursor): boolean {
782
801
  return true;
783
802
  }
784
803
  const parentTree = cursor.parentTree()?.value;
785
- return parentTree !== undefined && !expectsStatement(parentTree);
804
+ return parentTree !== undefined && !expectsStatement(parentTree, tree.id);
786
805
  }
787
806
 
788
807
  /**
@@ -913,9 +932,21 @@ export class TemplateApplier {
913
932
  private wrapTree(originalTree: J, resultToUse: J) {
914
933
  const parentTree = this.cursor?.parentTree()?.value;
915
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
+
916
947
  // Only apply wrapping logic if we have parent context
917
948
  if (parentTree) {
918
- const parentExpectsStatement = expectsStatement(parentTree);
949
+ const parentExpectsStatement = expectsStatement(parentTree, originalTree.id);
919
950
  const originalIsStatement = isStatement(originalTree);
920
951
 
921
952
  const resultIsStatement = isStatement(resultToUse);
@@ -935,8 +966,9 @@ export class TemplateApplier {
935
966
  expression: { ...expression, prefix: emptySpace }
936
967
  } as JS.ExpressionStatement;
937
968
  }
938
- } else if (!parentExpectsStatement) {
939
- // Expression context: wrap in StatementExpression if result is statement-only
969
+ } else if (!parentExpectsStatement && isExpression(originalTree)) {
970
+ // Expression context: wrap in StatementExpression if result is statement-only. A slot that held
971
+ // a statement-only node, such as an arrow function's block body, takes one unwrapped.
940
972
  if (resultIsStatement && !resultIsExpression) {
941
973
  const stmt = resultToUse as Statement;
942
974
  resultToUse = {
@@ -756,6 +756,16 @@ class Matcher {
756
756
  opensWithBrace(this.pattern.templateParts) && this.ast.kind !== J.Kind.Block);
757
757
  }
758
758
 
759
+ // A visit reaches what parentheses hold right after them, so seeing through them here would match it twice
760
+ if ((this.ast.kind === J.Kind.Parentheses || this.ast.kind === J.Kind.ControlParentheses) &&
761
+ this.patternAst.kind !== this.ast.kind) {
762
+ if (this.debugOptions.enabled) {
763
+ this.setExplanation('kind-mismatch', 'An expression without parentheses',
764
+ `Parentheses (${this.ast.kind.split('.').pop()})`,
765
+ 'The expression the parentheses hold is matched on its own');
766
+ }
767
+ return false;
768
+ }
759
769
  return this.matchNode(this.patternAst, this.ast);
760
770
  }
761
771
 
@@ -41,6 +41,25 @@ export class SubstitutedValue implements Marker {
41
41
  /** Chooses a substituted value's prefix, and says whether it is the source's own. */
42
42
  type MergePrefix = (sourcePrefix: J.Space, templatePrefix: J.Space) => [J.Space, boolean];
43
43
 
44
+ /** Whether `value` is a property, method or spread of an object literal. */
45
+ function isObjectMember(value: unknown): value is J {
46
+ switch (isTree(value) && value.kind) {
47
+ case JS.Kind.PropertyAssignment:
48
+ case JS.Kind.Spread:
49
+ case J.Kind.MethodDeclaration:
50
+ case JS.Kind.ComputedPropertyMethodDeclaration:
51
+ return true;
52
+ }
53
+ return false;
54
+ }
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
+
44
63
  /**
45
64
  * Visitor that replaces placeholder nodes with actual parameter values.
46
65
  */
@@ -319,6 +338,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
319
338
  const param = this.substitutions.get(placeholderText);
320
339
  if (param) {
321
340
  let arrayToExpand: J[] | J.RightPadded<J>[] | undefined = undefined;
341
+ const member = element.kind === JS.Kind.PropertyAssignment ? this.objectMember(param.value) : undefined;
322
342
 
323
343
  // Check if it's a J.Container
324
344
  const isContainer = param.value && typeof param.value === 'object' &&
@@ -331,6 +351,10 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
331
351
  else if (Array.isArray(param.value)) {
332
352
  arrayToExpand = param.value as J[];
333
353
  }
354
+ // One member of an object literal fills the slot of a member, not the name of a property
355
+ else if (member) {
356
+ arrayToExpand = [member] as J[] | J.RightPadded<J>[];
357
+ }
334
358
  // Check if it's a CaptureValue
335
359
  else if (param.value instanceof CaptureValue) {
336
360
  const resolved = param.value.resolve(this.values);
@@ -435,7 +459,20 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
435
459
  }
436
460
  }
437
461
 
438
- 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;
439
476
  }
440
477
 
441
478
  /** As `visit` on a placeholder, with `merge` choosing the replacement's prefix. */