@openrewrite/rewrite 8.92.17 → 8.92.18

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 (199) hide show
  1. package/README.md +48 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/java/formatting-utils.d.ts +2 -1
  6. package/dist/java/formatting-utils.d.ts.map +1 -1
  7. package/dist/java/formatting-utils.js +4 -3
  8. package/dist/java/formatting-utils.js.map +1 -1
  9. package/dist/java/rpc.d.ts.map +1 -1
  10. package/dist/java/rpc.js +2 -0
  11. package/dist/java/rpc.js.map +1 -1
  12. package/dist/java/tree.d.ts +8 -0
  13. package/dist/java/tree.d.ts.map +1 -1
  14. package/dist/java/tree.js +11 -1
  15. package/dist/java/tree.js.map +1 -1
  16. package/dist/java/type.d.ts +7 -1
  17. package/dist/java/type.d.ts.map +1 -1
  18. package/dist/java/type.js +16 -1
  19. package/dist/java/type.js.map +1 -1
  20. package/dist/javascript/add-import.d.ts +27 -21
  21. package/dist/javascript/add-import.d.ts.map +1 -1
  22. package/dist/javascript/add-import.js +303 -143
  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 +25 -10
  26. package/dist/javascript/amd.js.map +1 -1
  27. package/dist/javascript/autodetect.js +36 -45
  28. package/dist/javascript/autodetect.js.map +1 -1
  29. package/dist/javascript/binding.d.ts +6 -4
  30. package/dist/javascript/binding.d.ts.map +1 -1
  31. package/dist/javascript/binding.js +54 -18
  32. package/dist/javascript/binding.js.map +1 -1
  33. package/dist/javascript/comparator.d.ts +22 -5
  34. package/dist/javascript/comparator.d.ts.map +1 -1
  35. package/dist/javascript/comparator.js +116 -38
  36. package/dist/javascript/comparator.js.map +1 -1
  37. package/dist/javascript/format/format.d.ts +2 -0
  38. package/dist/javascript/format/format.d.ts.map +1 -1
  39. package/dist/javascript/format/format.js +11 -1
  40. package/dist/javascript/format/format.js.map +1 -1
  41. package/dist/javascript/index.d.ts +4 -2
  42. package/dist/javascript/index.d.ts.map +1 -1
  43. package/dist/javascript/index.js +10 -1
  44. package/dist/javascript/index.js.map +1 -1
  45. package/dist/javascript/markers.d.ts.map +1 -1
  46. package/dist/javascript/markers.js +64 -0
  47. package/dist/javascript/markers.js.map +1 -1
  48. package/dist/javascript/method-matcher.d.ts +13 -2
  49. package/dist/javascript/method-matcher.d.ts.map +1 -1
  50. package/dist/javascript/method-matcher.js +32 -19
  51. package/dist/javascript/method-matcher.js.map +1 -1
  52. package/dist/javascript/package-name.d.ts +6 -0
  53. package/dist/javascript/package-name.d.ts.map +1 -0
  54. package/dist/javascript/package-name.js +47 -0
  55. package/dist/javascript/package-name.js.map +1 -0
  56. package/dist/javascript/preconditions.d.ts +4 -1
  57. package/dist/javascript/preconditions.d.ts.map +1 -1
  58. package/dist/javascript/preconditions.js +7 -4
  59. package/dist/javascript/preconditions.js.map +1 -1
  60. package/dist/javascript/recipes/change-method-target-to-import.d.ts +29 -0
  61. package/dist/javascript/recipes/change-method-target-to-import.d.ts.map +1 -0
  62. package/dist/javascript/recipes/change-method-target-to-import.js +164 -0
  63. package/dist/javascript/recipes/change-method-target-to-import.js.map +1 -0
  64. package/dist/javascript/recipes/index.d.ts +1 -0
  65. package/dist/javascript/recipes/index.d.ts.map +1 -1
  66. package/dist/javascript/recipes/index.js +1 -0
  67. package/dist/javascript/recipes/index.js.map +1 -1
  68. package/dist/javascript/scope.d.ts +60 -16
  69. package/dist/javascript/scope.d.ts.map +1 -1
  70. package/dist/javascript/scope.js +596 -131
  71. package/dist/javascript/scope.js.map +1 -1
  72. package/dist/javascript/search/uses-method.d.ts +1 -1
  73. package/dist/javascript/search/uses-method.d.ts.map +1 -1
  74. package/dist/javascript/search/uses-method.js +2 -2
  75. package/dist/javascript/search/uses-method.js.map +1 -1
  76. package/dist/javascript/search/uses-type.d.ts +14 -1
  77. package/dist/javascript/search/uses-type.d.ts.map +1 -1
  78. package/dist/javascript/search/uses-type.js +24 -9
  79. package/dist/javascript/search/uses-type.js.map +1 -1
  80. package/dist/javascript/templating/bindings.d.ts +1 -1
  81. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  82. package/dist/javascript/templating/bindings.js +10 -50
  83. package/dist/javascript/templating/bindings.js.map +1 -1
  84. package/dist/javascript/templating/comparator.d.ts +8 -11
  85. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  86. package/dist/javascript/templating/comparator.js +12 -110
  87. package/dist/javascript/templating/comparator.js.map +1 -1
  88. package/dist/javascript/templating/engine.d.ts +34 -24
  89. package/dist/javascript/templating/engine.d.ts.map +1 -1
  90. package/dist/javascript/templating/engine.js +200 -112
  91. package/dist/javascript/templating/engine.js.map +1 -1
  92. package/dist/javascript/templating/pattern.d.ts +11 -5
  93. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  94. package/dist/javascript/templating/pattern.js +86 -20
  95. package/dist/javascript/templating/pattern.js.map +1 -1
  96. package/dist/javascript/templating/placeholder-replacement.d.ts +34 -3
  97. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  98. package/dist/javascript/templating/placeholder-replacement.js +126 -60
  99. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  100. package/dist/javascript/templating/precedence.d.ts +3 -1
  101. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  102. package/dist/javascript/templating/precedence.js +28 -8
  103. package/dist/javascript/templating/precedence.js.map +1 -1
  104. package/dist/javascript/templating/template.d.ts +7 -6
  105. package/dist/javascript/templating/template.d.ts.map +1 -1
  106. package/dist/javascript/templating/template.js +31 -28
  107. package/dist/javascript/templating/template.js.map +1 -1
  108. package/dist/javascript/templating/types.d.ts +6 -3
  109. package/dist/javascript/templating/types.d.ts.map +1 -1
  110. package/dist/javascript/templating/utils.d.ts +10 -1
  111. package/dist/javascript/templating/utils.d.ts.map +1 -1
  112. package/dist/javascript/templating/utils.js +22 -3
  113. package/dist/javascript/templating/utils.js.map +1 -1
  114. package/dist/javascript/tree-debug.d.ts +2 -0
  115. package/dist/javascript/tree-debug.d.ts.map +1 -1
  116. package/dist/javascript/tree-debug.js +6 -0
  117. package/dist/javascript/tree-debug.js.map +1 -1
  118. package/dist/javascript/tsconfig.d.ts.map +1 -1
  119. package/dist/javascript/tsconfig.js +7 -17
  120. package/dist/javascript/tsconfig.js.map +1 -1
  121. package/dist/javascript/type-mapping.d.ts +34 -16
  122. package/dist/javascript/type-mapping.d.ts.map +1 -1
  123. package/dist/javascript/type-mapping.js +253 -249
  124. package/dist/javascript/type-mapping.js.map +1 -1
  125. package/dist/javascript/type-report-cli.d.ts +7 -0
  126. package/dist/javascript/type-report-cli.d.ts.map +1 -0
  127. package/dist/javascript/type-report-cli.js +129 -0
  128. package/dist/javascript/type-report-cli.js.map +1 -0
  129. package/dist/javascript/type-report.d.ts +61 -0
  130. package/dist/javascript/type-report.d.ts.map +1 -0
  131. package/dist/javascript/type-report.js +488 -0
  132. package/dist/javascript/type-report.js.map +1 -0
  133. package/dist/markers.d.ts +4 -0
  134. package/dist/markers.d.ts.map +1 -1
  135. package/dist/markers.js +8 -0
  136. package/dist/markers.js.map +1 -1
  137. package/dist/recipe.d.ts +5 -0
  138. package/dist/recipe.d.ts.map +1 -1
  139. package/dist/recipe.js +5 -0
  140. package/dist/recipe.js.map +1 -1
  141. package/dist/rewrite-javascript-version.txt +1 -1
  142. package/dist/rpc/queue.d.ts +16 -0
  143. package/dist/rpc/queue.d.ts.map +1 -1
  144. package/dist/rpc/queue.js +27 -9
  145. package/dist/rpc/queue.js.map +1 -1
  146. package/dist/rpc/request/prepare-recipe.d.ts +3 -1
  147. package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
  148. package/dist/rpc/request/prepare-recipe.js +13 -6
  149. package/dist/rpc/request/prepare-recipe.js.map +1 -1
  150. package/dist/search/is-source-file.d.ts +1 -0
  151. package/dist/search/is-source-file.d.ts.map +1 -1
  152. package/dist/search/is-source-file.js +7 -2
  153. package/dist/search/is-source-file.js.map +1 -1
  154. package/dist/test/rewrite-test.d.ts +5 -0
  155. package/dist/test/rewrite-test.d.ts.map +1 -1
  156. package/dist/test/rewrite-test.js +14 -3
  157. package/dist/test/rewrite-test.js.map +1 -1
  158. package/package.json +4 -3
  159. package/src/index.ts +3 -1
  160. package/src/java/formatting-utils.ts +5 -4
  161. package/src/java/rpc.ts +2 -0
  162. package/src/java/tree.ts +15 -0
  163. package/src/java/type.ts +19 -2
  164. package/src/javascript/add-import.ts +338 -152
  165. package/src/javascript/amd.ts +28 -12
  166. package/src/javascript/autodetect.ts +32 -41
  167. package/src/javascript/binding.ts +64 -22
  168. package/src/javascript/comparator.ts +119 -43
  169. package/src/javascript/format/format.ts +9 -0
  170. package/src/javascript/index.ts +6 -3
  171. package/src/javascript/markers.ts +43 -1
  172. package/src/javascript/method-matcher.ts +34 -22
  173. package/src/javascript/package-name.ts +45 -0
  174. package/src/javascript/preconditions.ts +7 -4
  175. package/src/javascript/recipes/change-method-target-to-import.ts +178 -0
  176. package/src/javascript/recipes/index.ts +1 -0
  177. package/src/javascript/scope.ts +705 -169
  178. package/src/javascript/search/uses-method.ts +2 -2
  179. package/src/javascript/search/uses-type.ts +30 -9
  180. package/src/javascript/templating/bindings.ts +13 -38
  181. package/src/javascript/templating/comparator.ts +16 -127
  182. package/src/javascript/templating/engine.ts +229 -127
  183. package/src/javascript/templating/pattern.ts +107 -28
  184. package/src/javascript/templating/placeholder-replacement.ts +154 -62
  185. package/src/javascript/templating/precedence.ts +33 -11
  186. package/src/javascript/templating/template.ts +35 -30
  187. package/src/javascript/templating/types.ts +6 -3
  188. package/src/javascript/templating/utils.ts +24 -2
  189. package/src/javascript/tree-debug.ts +9 -0
  190. package/src/javascript/tsconfig.ts +7 -17
  191. package/src/javascript/type-mapping.ts +258 -252
  192. package/src/javascript/type-report-cli.ts +96 -0
  193. package/src/javascript/type-report.ts +575 -0
  194. package/src/markers.ts +8 -0
  195. package/src/recipe.ts +6 -0
  196. package/src/rpc/queue.ts +44 -5
  197. package/src/rpc/request/prepare-recipe.ts +16 -6
  198. package/src/search/is-source-file.ts +8 -3
  199. package/src/test/rewrite-test.ts +14 -3
@@ -13,30 +13,25 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {Cursor, isTree, produceAsync, Tree, updateIfChanged} from '../..';
17
- import {emptySpace, J, Statement, Type} from '../../java';
16
+ import {Cursor, isTree, Markers, markers, produceAsync, Tree, updateIfChanged} from '../..';
17
+ import {emptySpace, J, Statement, Type, TypedTree} from '../../java';
18
18
  import {Any, Capture, JavaScriptParser, JavaScriptVisitor, JS} from '..';
19
19
  import {create as produce} from 'mutative';
20
- import {CaptureMarker, dedentTemplate, PlaceholderUtils, randomizeIds, retainIds, treeIds, WRAPPER_FUNCTION_NAME} from './utils';
21
- import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, RAW_CODE_SYMBOL, RawCode} from './capture';
22
- import {PlaceholderReplacementVisitor} from './placeholder-replacement';
23
- import {maybeParenthesize, parenthesize, requiredPrecedence, startsWithDeclarationToken} from './precedence';
20
+ import {CaptureMarker, dedentTemplate, PlaceholderUtils, randomizeIds, retainIds, TEMPLATE_MODULE, treeIds, wrapCode} from './utils';
21
+ import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, RAW_CODE_SYMBOL, RawCode, TemplateParamImpl} from './capture';
22
+ import {PlaceholderReplacementVisitor, SubstitutedValue} from './placeholder-replacement';
23
+ import {isTrailingMarker, maybeParenthesize, parenthesize, requiredPrecedence, startsWithDeclarationToken} from './precedence';
24
24
  import {JavaCoordinates} from './template';
25
- import {maybeAutoFormat} from '../format';
25
+ import {autoIndent, maybeAutoFormat} from '../format';
26
26
  import {renameBindings} from './bindings';
27
27
  import {isExpression, isStatement} from '../parser-utils';
28
28
  import {randomId} from '../../uuid';
29
29
  import ts from "typescript";
30
30
  import {DependencyWorkspace} from "../dependency-workspace";
31
31
  import {ModuleScopeBinding, moduleScopeBindings} from '../add-import';
32
- import {walk} from '../scope';
33
- import {isIdentifier} from '../../java';
32
+ import {namesReferencedWithin} from '../scope';
34
33
  import {findMarker, MarkersKind, ParseExceptionResult} from '../../markers';
35
34
 
36
- /** A module a template's context binds, and whether the parse resolved it well enough to attribute. */
37
- export interface ContextBinding extends ModuleScopeBinding {
38
- attributed: boolean;
39
- }
40
35
  import {Parameter} from "./types";
41
36
 
42
37
  /**
@@ -177,7 +172,7 @@ class TemplateCache {
177
172
  // Parse and cache (workspace only needed during parsing)
178
173
  // Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
179
174
  const parser = templateParser(workspaceDir, types);
180
- const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.tsx'});
175
+ const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: `${TEMPLATE_MODULE}.tsx`});
181
176
  cu = (await parseGenerator.next()).value as JS.CompilationUnit;
182
177
 
183
178
  this.cache.set(key, cu);
@@ -225,27 +220,18 @@ function parseFailureReason(cu: JS.CompilationUnit): string {
225
220
  * Not exported from index, so only visible within the templating module.
226
221
  */
227
222
  export class TemplateEngine {
228
- /**
229
- * Gets the parsed and extracted template tree (before value substitution).
230
- * This is the cacheable part of template processing.
231
- *
232
- * @param templateParts The string parts of the template
233
- * @param parameters The parameters between the string parts
234
- * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
235
- * @param dependencies NPM dependencies for type attribution
236
- * @returns A Promise resolving to the extracted template AST
237
- */
238
223
  /** The template parsed with its context, which is what gives its code types to attribute against. */
239
224
  private static async parseWithContext(
240
225
  templateParts: TemplateStringsArray,
241
226
  parameters: Parameter[],
242
227
  contextStatements: string[],
243
228
  dependencies: Record<string, string>,
244
- types: string[] | undefined
229
+ types: string[] | undefined,
230
+ expression: boolean = false,
231
+ preamble: string[] = TemplateEngine.parameterPreamble(parameters)
245
232
  ): Promise<JS.CompilationUnit> {
246
- // A capture's declared type reaches the parse as a declaration, so it belongs with context.
247
- const preamble = TemplateEngine.parameterPreamble(parameters);
248
- const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
233
+ // A capture's type reaches the parse as a declaration, so it belongs with context.
234
+ const templateString = TemplateEngine.buildTemplateString(templateParts, parameters, expression);
249
235
  const contextWithPreamble = preamble.length > 0
250
236
  ? [...contextStatements, ...preamble]
251
237
  : contextStatements;
@@ -272,9 +258,9 @@ export class TemplateEngine {
272
258
  }
273
259
 
274
260
  /**
275
- * The modules the template's context binds, for the caller to bind in the file being edited.
276
- * An `import` or `require` states one; anything else — a `declare`, a helper signature — types
277
- * the template without asking for a binding.
261
+ * The modules the template's context binds and its code reads, for the caller to bind in the
262
+ * file being edited. An `import` or `require` states one; anything else — a `declare`, a helper
263
+ * signature — types the template without asking for a binding.
278
264
  */
279
265
  static async getContextBindings(
280
266
  templateParts: TemplateStringsArray,
@@ -282,36 +268,47 @@ export class TemplateEngine {
282
268
  contextStatements: string[] = [],
283
269
  dependencies: Record<string, string> = {},
284
270
  types?: string[]
285
- ): Promise<ContextBinding[]> {
286
- const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
287
- // The template's own code is the last statement, so everything ahead of it is context.
271
+ ): Promise<ModuleScopeBinding[]> {
272
+ // The bindings are the context's alone, so either reading of code opening with `{` serves
273
+ const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types)
274
+ .catch(e => opensWithBrace(templateParts) ?
275
+ TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types, true) :
276
+ Promise.reject(e));
277
+ // The template's own code is the last statement, so everything ahead of it is context, and a
278
+ // name the code reads without binding it itself resolves to the context's module-scope binding.
288
279
  const context = {...cu, statements: cu.statements.slice(0, -1)};
289
- const attributed = new Set<string>();
290
- walk(context.statements, node => {
291
- if (isIdentifier(node) && (node.type !== undefined || node.fieldType !== undefined)) {
292
- attributed.add(node.simpleName);
293
- }
294
- return true;
295
- });
296
- return moduleScopeBindings(context)
297
- .filter(b => b.module !== undefined)
298
- .map(b => ({...b, attributed: attributed.has(b.name)}));
280
+ const referenced = namesReferencedWithin(cu.statements[cu.statements.length - 1]);
281
+ return moduleScopeBindings(context).filter(b => b.module !== undefined && referenced.has(b.name));
299
282
  }
300
283
 
284
+ /**
285
+ * Gets the parsed and extracted template tree (before value substitution).
286
+ * This is the cacheable part of template processing.
287
+ *
288
+ * @param templateParts The string parts of the template
289
+ * @param parameters The parameters between the string parts
290
+ * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
291
+ * @param dependencies NPM dependencies for type attribution
292
+ * @param expression Whether to parse the code as an expression
293
+ * @param preamble The parameters' declarations, from {@link parameterPreamble}
294
+ * @returns A Promise resolving to the extracted template AST
295
+ */
301
296
  static async getTemplateTree(
302
297
  templateParts: TemplateStringsArray,
303
298
  parameters: Parameter[],
304
299
  contextStatements: string[] = [],
305
300
  dependencies: Record<string, string> = {},
306
- types?: string[]
301
+ types?: string[],
302
+ expression: boolean = false,
303
+ preamble?: string[]
307
304
  ): Promise<J> {
308
- const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
305
+ const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types, expression, preamble);
309
306
 
310
307
  // The template code is always the last statement (after context + preamble)
311
308
  const lastStatement = cu.statements[cu.statements.length - 1].element;
312
309
 
313
310
  // Extract from wrapper using shared utility
314
- const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Template');
311
+ const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Template', expression);
315
312
 
316
313
  return produce(extracted, _ => {});
317
314
  }
@@ -328,7 +325,7 @@ export class TemplateEngine {
328
325
  * @param wrappersMap Map of capture names to J.RightPadded wrappers (for preserving markers)
329
326
  * @param format Whether to fit the result to where it lands
330
327
  * @param renames Local names for the template's declared bindings, keyed as declared
331
- * @param modules The module each declared binding names, keyed as declared
328
+ * @param patternPrefixes The matched pattern's capture prefixes, given only under `format: false`
332
329
  * @returns A Promise resolving to the generated AST node
333
330
  */
334
331
  static async applyTemplateFromAst(
@@ -340,7 +337,7 @@ export class TemplateEngine {
340
337
  wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map(),
341
338
  format: boolean = true,
342
339
  renames: Record<string, string> = {},
343
- modules: Record<string, string> = {}
340
+ patternPrefixes?: Pick<Map<string, J.Space>, 'get'>
344
341
  ): Promise<J | undefined> {
345
342
  // Create substitutions map for placeholders
346
343
  const substitutions = new Map<string, Parameter>();
@@ -353,11 +350,11 @@ export class TemplateEngine {
353
350
  const fresh = await randomizeIds(ast);
354
351
 
355
352
  const bound = Object.keys(renames).length > 0
356
- ? await renameBindings(fresh.tree as J, renames, modules)
353
+ ? await renameBindings(fresh.tree as J, renames)
357
354
  : fresh.tree;
358
355
 
359
356
  // Unsubstitute placeholders with actual parameter values and match results
360
- const visitor = new PlaceholderReplacementVisitor(substitutions, values, wrappersMap);
357
+ const visitor = new PlaceholderReplacementVisitor(substitutions, values, wrappersMap, patternPrefixes);
361
358
  const unsubstitutedAst = (await visitor.visit(bound, null))!;
362
359
 
363
360
  // An id may only be kept where the node answering to it is leaving the tree, which is the
@@ -372,7 +369,7 @@ export class TemplateEngine {
372
369
  const uniqueAst = await retainIds(unsubstitutedAst, retainable);
373
370
 
374
371
  // Apply the template to the current AST
375
- return new TemplateApplier(cursor, coordinates, uniqueAst, format).apply();
372
+ return new TemplateApplier(cursor, coordinates, uniqueAst, format, visitor.substituted).apply();
376
373
  }
377
374
 
378
375
  /**
@@ -406,62 +403,42 @@ export class TemplateEngine {
406
403
  return preamble;
407
404
  }
408
405
 
409
- /** The parameter counterpart of {@link capturePreamble}. */
410
- static parameterPreamble(parameters: Parameter[]): string[] {
406
+ /**
407
+ * The parameter counterpart of {@link capturePreamble}. A capture without a declared type takes
408
+ * the type of the expression bound to it, as an expression parameter takes its own. A variadic
409
+ * capture binds a list, which has no one type to declare.
410
+ */
411
+ static parameterPreamble(parameters: Parameter[], values?: Pick<Map<string, J | J[]>, 'get'>): string[] {
411
412
  const preamble: string[] = [];
412
-
413
413
  for (let i = 0; i < parameters.length; i++) {
414
- const param = parameters[i].value;
415
- const placeholder = `${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__`;
416
-
417
- // Check for Capture (could be a Proxy, so check for symbol property)
418
- const isCapture = param instanceof CaptureImpl ||
419
- (param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL]);
420
- const isCaptureValue = param instanceof CaptureValue;
421
- const isTreeArray = Array.isArray(param) && param.length > 0 && isTree(param[0]);
422
-
423
- if (isCapture) {
424
- const captureType = param[CAPTURE_TYPE_SYMBOL];
425
- if (captureType) {
426
- const typeString = typeof captureType === 'string'
427
- ? captureType
428
- : this.typeToString(captureType);
429
- // Only add preamble if we have a concrete type (not 'any')
430
- if (typeString !== 'any') {
431
- preamble.push(`let ${placeholder}: ${typeString};`);
432
- }
433
- }
434
- } else if (isCaptureValue) {
435
- // For CaptureValue, check if the root capture has a type
436
- const rootCapture = param.rootCapture;
437
- if (rootCapture) {
438
- const captureType = (rootCapture as any)[CAPTURE_TYPE_SYMBOL];
439
- if (captureType) {
440
- const typeString = typeof captureType === 'string'
441
- ? captureType
442
- : this.typeToString(captureType);
443
- // Only add preamble if we have a concrete type (not 'any')
444
- if (typeString !== 'any') {
445
- preamble.push(`let ${placeholder}: ${typeString};`);
446
- }
447
- }
448
- }
449
- } else if (isTree(param) && !isTreeArray) {
450
- // For J elements, derive type from the element's type property if it exists
451
- const jElement = param as J;
452
- if ((jElement as any).type) {
453
- const typeString = this.typeToString((jElement as any).type);
454
- // Only add preamble if we have a concrete type (not 'any')
455
- if (typeString !== 'any') {
456
- preamble.push(`let ${placeholder}: ${typeString};`);
457
- }
458
- }
414
+ const type = this.parameterType(parameters[i].value, values);
415
+ const typeString = typeof type === 'string' ? type : type && this.typeToString(type);
416
+ if (typeString && typeString !== 'any') {
417
+ preamble.push(`let ${PlaceholderUtils.PLACEHOLDER_PREFIX}${i}__: ${typeString};`);
459
418
  }
460
419
  }
461
-
462
420
  return preamble;
463
421
  }
464
422
 
423
+ private static parameterType(param: any, values?: Pick<Map<string, J | J[]>, 'get'>): Type | string | undefined {
424
+ // A Capture may be a Proxy, so it is recognised by its symbol
425
+ if (param instanceof CaptureImpl || (param && typeof param === 'object' && param[CAPTURE_NAME_SYMBOL])) {
426
+ return param[CAPTURE_TYPE_SYMBOL] ?? this.treeType(values?.get(param[CAPTURE_NAME_SYMBOL] || param.getName()));
427
+ } else if (param instanceof CaptureValue) {
428
+ // The root's declared type is the root's alone, not that of a property or element of it
429
+ return values && this.treeType(param.resolve(values));
430
+ } else if (param instanceof TemplateParamImpl) {
431
+ return this.treeType(values?.get(param.name));
432
+ }
433
+ return this.treeType(param);
434
+ }
435
+
436
+ private static treeType(value: unknown): Type | undefined {
437
+ // A declaration's type is that of what it declares, which is no value the placeholder holds
438
+ return isTree(value) && (isExpression(value as J) || !isStatement(value as J)) ?
439
+ TypedTree.getType(value as TypedTree) : undefined;
440
+ }
441
+
465
442
  /**
466
443
  * Builds a template string with parameter placeholders.
467
444
  * RawCode parameters are spliced directly into the template at construction time.
@@ -473,7 +450,8 @@ export class TemplateEngine {
473
450
  */
474
451
  private static buildTemplateString(
475
452
  templateParts: TemplateStringsArray,
476
- parameters: Parameter[]
453
+ parameters: Parameter[],
454
+ expression: boolean
477
455
  ): string {
478
456
  let result = '';
479
457
  for (let i = 0; i < templateParts.length; i++) {
@@ -495,7 +473,7 @@ export class TemplateEngine {
495
473
 
496
474
  // Always wrap in function body - let the parser decide what it is,
497
475
  // then we'll extract intelligently based on what was parsed
498
- return `function ${WRAPPER_FUNCTION_NAME}() { ${dedentTemplate(result)} }`;
476
+ return wrapCode(dedentTemplate(result), expression);
499
477
  }
500
478
 
501
479
  /**
@@ -556,6 +534,7 @@ export class TemplateEngine {
556
534
  * @param captures The captures between the string parts (can include RawCode)
557
535
  * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
558
536
  * @param dependencies NPM dependencies for type attribution
537
+ * @param expression Whether to parse the code as an expression
559
538
  * @returns A Promise resolving to the extracted pattern AST
560
539
  */
561
540
  static async getPatternTree(
@@ -563,7 +542,8 @@ export class TemplateEngine {
563
542
  captures: (Capture | Any | RawCode)[],
564
543
  contextStatements: string[] = [],
565
544
  dependencies: Record<string, string> = {},
566
- types?: string[]
545
+ types?: string[],
546
+ expression: boolean = false
567
547
  ): Promise<J> {
568
548
  const preamble = TemplateEngine.capturePreamble(captures);
569
549
 
@@ -587,7 +567,7 @@ export class TemplateEngine {
587
567
 
588
568
  // Always wrap in function body - let the parser decide what it is,
589
569
  // then we'll extract intelligently based on what was parsed
590
- const templateString = `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
570
+ const templateString = wrapCode(result, expression);
591
571
 
592
572
  // Add preamble to context statements (so they're skipped during extraction)
593
573
  const contextWithPreamble = preamble.length > 0
@@ -612,7 +592,7 @@ export class TemplateEngine {
612
592
  const lastStatement = cu.statements[cu.statements.length - 1].element;
613
593
 
614
594
  // Extract from wrapper using shared utility
615
- return PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
595
+ return PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern', expression);
616
596
  }
617
597
 
618
598
  /**
@@ -749,6 +729,59 @@ class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
749
729
  name: visitedName
750
730
  });
751
731
  }
732
+
733
+ /**
734
+ * Promotes a variadic capture's marker from a shorthand property's name to the property, so
735
+ * that `{${props}}` matches the object literal's properties as a sequence. A scalar capture
736
+ * stays on the name, binding the identifier a shorthand property consists of.
737
+ */
738
+ protected override async visitPropertyAssignment(propertyAssignment: JS.PropertyAssignment, p: undefined): Promise<J | undefined> {
739
+ const visited = await super.visitPropertyAssignment(propertyAssignment, p) as JS.PropertyAssignment;
740
+ const nameMarker = visited.initializer === undefined ? PlaceholderUtils.getCaptureMarker(visited.name) : undefined;
741
+ if (!nameMarker?.variadicOptions) {
742
+ return visited;
743
+ }
744
+ return updateIfChanged(visited, {
745
+ name: {
746
+ ...visited.name,
747
+ markers: {...visited.name.markers, markers: visited.name.markers.markers.filter(m => m !== nameMarker)}
748
+ },
749
+ markers: {
750
+ ...visited.markers,
751
+ markers: [...visited.markers.markers, nameMarker]
752
+ },
753
+ });
754
+ }
755
+ }
756
+
757
+ // FIXME: This is a heuristic to determine if the parent expects a statement child
758
+ function expectsStatement(parentTree: J): boolean {
759
+ return parentTree.kind === J.Kind.Block ||
760
+ parentTree.kind === J.Kind.Case ||
761
+ parentTree.kind === J.Kind.DoWhileLoop ||
762
+ parentTree.kind === J.Kind.ForEachLoop ||
763
+ parentTree.kind === J.Kind.ForLoop ||
764
+ parentTree.kind === J.Kind.If ||
765
+ parentTree.kind === J.Kind.IfElse ||
766
+ parentTree.kind === J.Kind.WhileLoop ||
767
+ parentTree.kind === JS.Kind.CompilationUnit ||
768
+ parentTree.kind === JS.Kind.ForInLoop;
769
+ }
770
+
771
+ /** Whether the code opens with `{`, which is a block where a statement goes and an object literal elsewhere. */
772
+ export function opensWithBrace(templateParts: TemplateStringsArray): boolean {
773
+ return templateParts[0].trimStart().startsWith('{');
774
+ }
775
+
776
+ /** Whether code opening with `{` that replaces `tree`, which `cursor` points at, is an object literal. */
777
+ export function replacedByObjectLiteral(tree: J, cursor: Cursor): boolean {
778
+ if (tree.kind === J.Kind.Block) {
779
+ return false;
780
+ } else if (!isStatement(tree)) {
781
+ return true;
782
+ }
783
+ const parentTree = cursor.parentTree()?.value;
784
+ return parentTree !== undefined && !expectsStatement(parentTree);
752
785
  }
753
786
 
754
787
  /**
@@ -759,7 +792,8 @@ export class TemplateApplier {
759
792
  private readonly cursor: Cursor,
760
793
  private readonly coordinates: JavaCoordinates,
761
794
  private readonly ast: J,
762
- private readonly shouldFormat: boolean = true
795
+ private readonly shouldFormat: boolean = true,
796
+ private readonly substituted: boolean = false
763
797
  ) {
764
798
  }
765
799
 
@@ -791,16 +825,21 @@ export class TemplateApplier {
791
825
  const {tree} = this.coordinates;
792
826
 
793
827
  if (!tree) {
794
- return this.ast;
828
+ return this.substituted ? new SubstitutedLayoutVisitor().visit(this.ast, undefined) : this.ast;
795
829
  }
796
830
 
797
831
  const originalTree = tree as J;
798
832
  let resultToUse = this.wrapTree(originalTree, this.ast);
799
- const slot = this.replacedSlot(originalTree);
800
- if (slot) {
801
- // `format` substitutes the target's prefix, so decide against the prefix that will print
802
- resultToUse = maybeParenthesize(slot[0], slot[1], {...resultToUse, prefix: originalTree.prefix});
833
+ // A replaced node spliced back in as a capture carries its own trailing markers
834
+ const trailing = trailingMarkers(originalTree);
835
+ const slotMarkers = trailing && !(await treeIds(this.ast)).has(originalTree.id) ? trailing : undefined;
836
+ if (slotMarkers) {
837
+ resultToUse = {...resultToUse, markers: withMarkers(resultToUse.markers, slotMarkers)};
803
838
  }
839
+ const slot = this.replacedSlot(originalTree);
840
+ // `format` substitutes the target's prefix, so decide against the prefix that will print
841
+ resultToUse = maybeParenthesize(slot?.[0], slot?.[1] ?? originalTree.id,
842
+ {...resultToUse, prefix: originalTree.prefix}, slotMarkers);
804
843
  return this.format(resultToUse, originalTree);
805
844
  }
806
845
 
@@ -842,7 +881,8 @@ export class TemplateApplier {
842
881
  };
843
882
 
844
883
  if (!this.shouldFormat) {
845
- return {...result, id: resultToUse.id};
884
+ const unmarked = this.substituted ? await new SubstitutedLayoutVisitor().visit<J>(result, undefined) : result;
885
+ return {...unmarked!, id: resultToUse.id};
846
886
  }
847
887
 
848
888
  // A recipe can apply a template to a child while its cursor still points at the
@@ -853,9 +893,18 @@ export class TemplateApplier {
853
893
  (await treeIds(owner as J)).has(originalTree.id) ? this.cursor : this.cursor?.parent;
854
894
  const formatted =
855
895
  await maybeAutoFormat(originalTree, result, null, undefined, parent);
896
+ // A value Prettier lays out arrives unmarked, since reconciling takes the markers of Prettier's parse
897
+ let laidOut = formatted;
898
+ if (this.substituted) {
899
+ const restore = new SubstitutedLayoutVisitor(await substitutedValues(result));
900
+ laidOut = (await restore.visit<J>(formatted, undefined))!;
901
+ if (restore.restored) {
902
+ laidOut = await autoIndent(laidOut, null, parent);
903
+ }
904
+ }
856
905
 
857
906
  // Restore the original ID
858
- return {...formatted, id: resultToUse.id};
907
+ return {...laidOut!, id: resultToUse.id};
859
908
  }
860
909
 
861
910
  private wrapTree(originalTree: J, resultToUse: J) {
@@ -863,17 +912,7 @@ export class TemplateApplier {
863
912
 
864
913
  // Only apply wrapping logic if we have parent context
865
914
  if (parentTree) {
866
- // FIXME: This is a heuristic to determine if the parent expects a statement child
867
- const parentExpectsStatement = parentTree.kind === J.Kind.Block ||
868
- parentTree.kind === J.Kind.Case ||
869
- parentTree.kind === J.Kind.DoWhileLoop ||
870
- parentTree.kind === J.Kind.ForEachLoop ||
871
- parentTree.kind === J.Kind.ForLoop ||
872
- parentTree.kind === J.Kind.If ||
873
- parentTree.kind === J.Kind.IfElse ||
874
- parentTree.kind === J.Kind.WhileLoop ||
875
- parentTree.kind === JS.Kind.CompilationUnit ||
876
- parentTree.kind === JS.Kind.ForInLoop;
915
+ const parentExpectsStatement = expectsStatement(parentTree);
877
916
  const originalIsStatement = isStatement(originalTree);
878
917
 
879
918
  const resultIsStatement = isStatement(resultToUse);
@@ -889,7 +928,7 @@ export class TemplateApplier {
889
928
  kind: JS.Kind.ExpressionStatement,
890
929
  id: randomId(),
891
930
  prefix: expression.prefix,
892
- markers: expression.markers,
931
+ markers: wrapperMarkers(expression),
893
932
  expression: { ...expression, prefix: emptySpace }
894
933
  } as JS.ExpressionStatement;
895
934
  }
@@ -901,7 +940,7 @@ export class TemplateApplier {
901
940
  kind: JS.Kind.StatementExpression,
902
941
  id: randomId(),
903
942
  prefix: stmt.prefix,
904
- markers: stmt.markers,
943
+ markers: wrapperMarkers(stmt),
905
944
  statement: { ...stmt, prefix: emptySpace }
906
945
  } as JS.StatementExpression;
907
946
  }
@@ -910,3 +949,66 @@ export class TemplateApplier {
910
949
  return resultToUse;
911
950
  }
912
951
  }
952
+
953
+ /**
954
+ * The trailing markers of the node a template replaces. Each prints after whatever node fills the slot,
955
+ * so a pattern does not match on it and the replacement keeps it.
956
+ */
957
+ function trailingMarkers(replaced: J): Markers | undefined {
958
+ const trailing = replaced.markers.markers.filter(isTrailingMarker);
959
+ return trailing.length === 0 ? undefined : markers(...trailing);
960
+ }
961
+
962
+ /** `own`, plus each marker of `added` whose kind `own` does not already hold. */
963
+ function withMarkers(own: Markers, added: Markers): Markers {
964
+ const missing = added.markers.filter(m => !own.markers.some(o => o.kind === m.kind));
965
+ return missing.length === 0 ? own : {...own, markers: [...own.markers, ...missing]};
966
+ }
967
+
968
+ /** The markers of a node wrapping `wrapped`, less the {@link SubstitutedValue} that belongs to `wrapped` alone. */
969
+ function wrapperMarkers(wrapped: J): J['markers'] {
970
+ return {...wrapped.markers, markers: wrapped.markers.markers.filter(m => m.kind !== SubstitutedValue.KIND)};
971
+ }
972
+
973
+ /** The substituted values in `tree`, by the id of the {@link SubstitutedValue} marking each. */
974
+ async function substitutedValues(tree: J): Promise<Map<string, J>> {
975
+ const values = new Map<string, J>();
976
+ await new class extends JavaScriptVisitor<undefined> {
977
+ protected override async preVisit(t: J): Promise<J | undefined> {
978
+ const marker = findMarker<SubstitutedValue>(t, SubstitutedValue.KIND);
979
+ if (marker) {
980
+ values.set(marker.id, t);
981
+ }
982
+ return t;
983
+ }
984
+ }().visit(tree, undefined);
985
+ return values;
986
+ }
987
+
988
+ /**
989
+ * Puts each substituted value back as it was in `unformatted`, under the prefix its marker assigns it,
990
+ * and drops its {@link SubstitutedValue} marker. Re-indenting them is left to the indent pass.
991
+ */
992
+ class SubstitutedLayoutVisitor extends JavaScriptVisitor<undefined> {
993
+ /** Whether any value was put back. */
994
+ restored = false;
995
+
996
+ constructor(private readonly unformatted: Map<string, J> = new Map()) {
997
+ super();
998
+ }
999
+
1000
+ override async visit<R extends J>(tree: Tree, p: undefined, parent?: Cursor): Promise<R | undefined> {
1001
+ const marker = isTree(tree) ? findMarker<SubstitutedValue>(tree as J, SubstitutedValue.KIND) : undefined;
1002
+ if (!marker) {
1003
+ return super.visit(tree, p, parent);
1004
+ }
1005
+ const formatted = tree as J;
1006
+ const before = this.unformatted.get(marker.id);
1007
+ this.restored ||= before !== undefined;
1008
+ const value = before ? {...before, prefix: marker.ownPrefix ? before.prefix : formatted.prefix} : formatted;
1009
+ return {
1010
+ ...value,
1011
+ markers: {...value.markers, markers: value.markers.markers.filter(m => m !== marker)}
1012
+ } as J as R;
1013
+ }
1014
+ }