@openrewrite/rewrite 8.91.0 → 8.91.2

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 (153) hide show
  1. package/dist/java/formatting-utils.d.ts +8 -5
  2. package/dist/java/formatting-utils.d.ts.map +1 -1
  3. package/dist/java/formatting-utils.js +14 -10
  4. package/dist/java/formatting-utils.js.map +1 -1
  5. package/dist/java/type.d.ts +2 -0
  6. package/dist/java/type.d.ts.map +1 -1
  7. package/dist/java/type.js +5 -0
  8. package/dist/java/type.js.map +1 -1
  9. package/dist/javascript/add-import.d.ts +96 -43
  10. package/dist/javascript/add-import.d.ts.map +1 -1
  11. package/dist/javascript/add-import.js +668 -236
  12. package/dist/javascript/add-import.js.map +1 -1
  13. package/dist/javascript/amd.d.ts +138 -0
  14. package/dist/javascript/amd.d.ts.map +1 -0
  15. package/dist/javascript/amd.js +768 -0
  16. package/dist/javascript/amd.js.map +1 -0
  17. package/dist/javascript/assertions.d.ts +7 -0
  18. package/dist/javascript/assertions.d.ts.map +1 -1
  19. package/dist/javascript/assertions.js +31 -5
  20. package/dist/javascript/assertions.js.map +1 -1
  21. package/dist/javascript/binding.d.ts +94 -0
  22. package/dist/javascript/binding.d.ts.map +1 -0
  23. package/dist/javascript/binding.js +335 -0
  24. package/dist/javascript/binding.js.map +1 -0
  25. package/dist/javascript/comparator.d.ts +5 -0
  26. package/dist/javascript/comparator.d.ts.map +1 -1
  27. package/dist/javascript/comparator.js +34 -9
  28. package/dist/javascript/comparator.js.map +1 -1
  29. package/dist/javascript/format/format.d.ts.map +1 -1
  30. package/dist/javascript/format/format.js +8 -9
  31. package/dist/javascript/format/format.js.map +1 -1
  32. package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
  33. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
  34. package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
  35. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  36. package/dist/javascript/index.d.ts +8 -1
  37. package/dist/javascript/index.d.ts.map +1 -1
  38. package/dist/javascript/index.js +26 -1
  39. package/dist/javascript/index.js.map +1 -1
  40. package/dist/javascript/node-resolution-result.d.ts +19 -1
  41. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  42. package/dist/javascript/node-resolution-result.js +37 -2
  43. package/dist/javascript/node-resolution-result.js.map +1 -1
  44. package/dist/javascript/package-exported-types.d.ts.map +1 -1
  45. package/dist/javascript/package-exported-types.js +56 -72
  46. package/dist/javascript/package-exported-types.js.map +1 -1
  47. package/dist/javascript/parser-utils.d.ts.map +1 -1
  48. package/dist/javascript/parser-utils.js +2 -0
  49. package/dist/javascript/parser-utils.js.map +1 -1
  50. package/dist/javascript/parser.d.ts +7 -1
  51. package/dist/javascript/parser.d.ts.map +1 -1
  52. package/dist/javascript/parser.js +17 -4
  53. package/dist/javascript/parser.js.map +1 -1
  54. package/dist/javascript/recipes/change-import.d.ts +2 -4
  55. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  56. package/dist/javascript/recipes/change-import.js +8 -244
  57. package/dist/javascript/recipes/change-import.js.map +1 -1
  58. package/dist/javascript/remove-import.d.ts +2 -28
  59. package/dist/javascript/remove-import.d.ts.map +1 -1
  60. package/dist/javascript/remove-import.js +19 -57
  61. package/dist/javascript/remove-import.js.map +1 -1
  62. package/dist/javascript/scope.d.ts +49 -0
  63. package/dist/javascript/scope.d.ts.map +1 -0
  64. package/dist/javascript/scope.js +324 -0
  65. package/dist/javascript/scope.js.map +1 -0
  66. package/dist/javascript/style.d.ts.map +1 -1
  67. package/dist/javascript/style.js +3 -10
  68. package/dist/javascript/style.js.map +1 -1
  69. package/dist/javascript/templating/bindings.d.ts +8 -0
  70. package/dist/javascript/templating/bindings.d.ts.map +1 -0
  71. package/dist/javascript/templating/bindings.js +92 -0
  72. package/dist/javascript/templating/bindings.js.map +1 -0
  73. package/dist/javascript/templating/comparator.d.ts +10 -0
  74. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  75. package/dist/javascript/templating/comparator.js +48 -20
  76. package/dist/javascript/templating/comparator.js.map +1 -1
  77. package/dist/javascript/templating/engine.d.ts +34 -10
  78. package/dist/javascript/templating/engine.d.ts.map +1 -1
  79. package/dist/javascript/templating/engine.js +109 -55
  80. package/dist/javascript/templating/engine.js.map +1 -1
  81. package/dist/javascript/templating/index.d.ts +1 -1
  82. package/dist/javascript/templating/index.d.ts.map +1 -1
  83. package/dist/javascript/templating/index.js.map +1 -1
  84. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  85. package/dist/javascript/templating/pattern.js +13 -10
  86. package/dist/javascript/templating/pattern.js.map +1 -1
  87. package/dist/javascript/templating/placeholder-replacement.d.ts +4 -0
  88. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  89. package/dist/javascript/templating/placeholder-replacement.js +24 -10
  90. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  91. package/dist/javascript/templating/precedence.d.ts +12 -4
  92. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  93. package/dist/javascript/templating/precedence.js +18 -11
  94. package/dist/javascript/templating/precedence.js.map +1 -1
  95. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  96. package/dist/javascript/templating/rewrite.js +19 -17
  97. package/dist/javascript/templating/rewrite.js.map +1 -1
  98. package/dist/javascript/templating/template.d.ts +14 -0
  99. package/dist/javascript/templating/template.d.ts.map +1 -1
  100. package/dist/javascript/templating/template.js +63 -3
  101. package/dist/javascript/templating/template.js.map +1 -1
  102. package/dist/javascript/templating/types.d.ts +39 -1
  103. package/dist/javascript/templating/types.d.ts.map +1 -1
  104. package/dist/javascript/templating/utils.d.ts +6 -1
  105. package/dist/javascript/templating/utils.d.ts.map +1 -1
  106. package/dist/javascript/templating/utils.js +20 -2
  107. package/dist/javascript/templating/utils.js.map +1 -1
  108. package/dist/javascript/type-mapping.d.ts +1 -0
  109. package/dist/javascript/type-mapping.d.ts.map +1 -1
  110. package/dist/javascript/type-mapping.js +52 -2
  111. package/dist/javascript/type-mapping.js.map +1 -1
  112. package/dist/rewrite-javascript-version.txt +1 -1
  113. package/dist/style.d.ts +2 -0
  114. package/dist/style.d.ts.map +1 -1
  115. package/dist/style.js +5 -0
  116. package/dist/style.js.map +1 -1
  117. package/dist/test/rewrite-test.d.ts +12 -0
  118. package/dist/test/rewrite-test.d.ts.map +1 -1
  119. package/dist/test/rewrite-test.js +40 -3
  120. package/dist/test/rewrite-test.js.map +1 -1
  121. package/package.json +2 -2
  122. package/src/java/formatting-utils.ts +14 -10
  123. package/src/java/type.ts +5 -0
  124. package/src/javascript/add-import.ts +751 -247
  125. package/src/javascript/amd.ts +915 -0
  126. package/src/javascript/assertions.ts +33 -5
  127. package/src/javascript/binding.ts +449 -0
  128. package/src/javascript/comparator.ts +39 -10
  129. package/src/javascript/format/format.ts +9 -10
  130. package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
  131. package/src/javascript/index.ts +12 -1
  132. package/src/javascript/node-resolution-result.ts +59 -4
  133. package/src/javascript/package-exported-types.ts +60 -74
  134. package/src/javascript/parser-utils.ts +2 -0
  135. package/src/javascript/parser.ts +23 -3
  136. package/src/javascript/recipes/change-import.ts +10 -263
  137. package/src/javascript/remove-import.ts +20 -56
  138. package/src/javascript/scope.ts +338 -0
  139. package/src/javascript/style.ts +3 -11
  140. package/src/javascript/templating/bindings.ts +99 -0
  141. package/src/javascript/templating/comparator.ts +57 -20
  142. package/src/javascript/templating/engine.ts +148 -68
  143. package/src/javascript/templating/index.ts +1 -0
  144. package/src/javascript/templating/pattern.ts +19 -16
  145. package/src/javascript/templating/placeholder-replacement.ts +32 -11
  146. package/src/javascript/templating/precedence.ts +21 -13
  147. package/src/javascript/templating/rewrite.ts +23 -17
  148. package/src/javascript/templating/template.ts +84 -9
  149. package/src/javascript/templating/types.ts +46 -1
  150. package/src/javascript/templating/utils.ts +21 -2
  151. package/src/javascript/type-mapping.ts +54 -2
  152. package/src/style.ts +5 -0
  153. package/src/test/rewrite-test.ts +61 -3
@@ -17,16 +17,25 @@ import {Cursor, isTree, produceAsync, Tree, updateIfChanged} from '../..';
17
17
  import {emptySpace, J, Statement, Type} from '../../java';
18
18
  import {Any, Capture, JavaScriptParser, JavaScriptVisitor, JS} from '..';
19
19
  import {create as produce} from 'mutative';
20
- import {CaptureMarker, PlaceholderUtils, randomizeIds, retainIds, treeIds, WRAPPER_FUNCTION_NAME} from './utils';
20
+ import {CaptureMarker, dedentTemplate, PlaceholderUtils, randomizeIds, retainIds, treeIds, WRAPPER_FUNCTION_NAME} from './utils';
21
21
  import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, RAW_CODE_SYMBOL, RawCode} from './capture';
22
22
  import {PlaceholderReplacementVisitor} from './placeholder-replacement';
23
23
  import {maybeParenthesize, parenthesize, requiredPrecedence, startsWithDeclarationToken} from './precedence';
24
24
  import {JavaCoordinates} from './template';
25
25
  import {maybeAutoFormat} from '../format';
26
+ import {renameBindings} from './bindings';
26
27
  import {isExpression, isStatement} from '../parser-utils';
27
28
  import {randomId} from '../../uuid';
28
29
  import ts from "typescript";
29
30
  import {DependencyWorkspace} from "../dependency-workspace";
31
+ import {ModuleScopeBinding, moduleScopeBindings} from '../add-import';
32
+ import {walk} from '../scope';
33
+ import {isIdentifier} from '../../java';
34
+
35
+ /** A module a template's context binds, and whether the parse resolved it well enough to attribute. */
36
+ export interface ContextBinding extends ModuleScopeBinding {
37
+ attributed: boolean;
38
+ }
30
39
  import {Parameter} from "./types";
31
40
 
32
41
  /**
@@ -82,6 +91,23 @@ let templateSourceFileCache: Map<string, ts.SourceFile> | undefined;
82
91
  */
83
92
  export function setTemplateSourceFileCache(cache?: Map<string, ts.SourceFile>): void {
84
93
  templateSourceFileCache = cache;
94
+ templateParsers.clear();
95
+ }
96
+
97
+ // Every template parses under `template.tsx`, so one parser per workspace carries its program
98
+ // from one compile to the next; see `JavaScriptParser.parse`.
99
+ const templateParsers: Map<string, JavaScriptParser> = new Map();
100
+
101
+ function templateParser(workspaceDir?: string, types?: string[]): JavaScriptParser {
102
+ // `types` changes what the compiler loads, so parsers cannot be shared across differing sets.
103
+ // An empty list loads nothing where absence loads the defaults, so the two encode apart.
104
+ const key = `${workspaceDir ?? ""}::${JSON.stringify(types ?? null)}`;
105
+ let parser = templateParsers.get(key);
106
+ if (!parser) {
107
+ parser = new JavaScriptParser({relativeTo: workspaceDir, sourceFileCache: templateSourceFileCache, types});
108
+ templateParsers.set(key, parser);
109
+ }
110
+ return parser;
85
111
  }
86
112
 
87
113
  /**
@@ -99,7 +125,8 @@ class TemplateCache {
99
125
  templateString: string,
100
126
  captures: (Capture | Any)[],
101
127
  contextStatements: string[],
102
- dependencies: Record<string, string>
128
+ dependencies: Record<string, string>,
129
+ types: string[] | undefined
103
130
  ): string {
104
131
  // Use the actual template string (with placeholders) as the primary key
105
132
  const templateKey = templateString;
@@ -113,7 +140,7 @@ class TemplateCache {
113
140
  // Dependencies
114
141
  const depsKey = JSON.stringify(dependencies || {});
115
142
 
116
- return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}`;
143
+ return `${templateKey}::${capturesKey}::${contextKey}::${depsKey}::${JSON.stringify(types ?? null)}`;
117
144
  }
118
145
 
119
146
  /**
@@ -123,9 +150,10 @@ class TemplateCache {
123
150
  templateString: string,
124
151
  captures: (Capture | Any)[],
125
152
  contextStatements: string[],
126
- dependencies: Record<string, string>
153
+ dependencies: Record<string, string>,
154
+ types?: string[]
127
155
  ): Promise<JS.CompilationUnit> {
128
- const key = this.generateKey(templateString, captures, contextStatements, dependencies);
156
+ const key = this.generateKey(templateString, captures, contextStatements, dependencies, types);
129
157
 
130
158
  let cu = this.cache.get(key);
131
159
  if (cu) {
@@ -147,10 +175,7 @@ class TemplateCache {
147
175
 
148
176
  // Parse and cache (workspace only needed during parsing)
149
177
  // Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
150
- const parser = new JavaScriptParser({
151
- relativeTo: workspaceDir,
152
- sourceFileCache: templateSourceFileCache
153
- });
178
+ const parser = templateParser(workspaceDir, types);
154
179
  const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.tsx'});
155
180
  cu = (await parseGenerator.next()).value as JS.CompilationUnit;
156
181
 
@@ -195,34 +220,62 @@ export class TemplateEngine {
195
220
  * @param dependencies NPM dependencies for type attribution
196
221
  * @returns A Promise resolving to the extracted template AST
197
222
  */
198
- static async getTemplateTree(
223
+ /** The template parsed with its context, which is what gives its code types to attribute against. */
224
+ private static async parseWithContext(
199
225
  templateParts: TemplateStringsArray,
200
226
  parameters: Parameter[],
201
- contextStatements: string[] = [],
202
- dependencies: Record<string, string> = {}
203
- ): Promise<J> {
204
- // Generate type preamble for captures/parameters with types
205
- const preamble = TemplateEngine.generateTypePreamble(parameters);
206
-
207
- // Build the template string with parameter placeholders
227
+ contextStatements: string[],
228
+ dependencies: Record<string, string>,
229
+ types: string[] | undefined
230
+ ): Promise<JS.CompilationUnit> {
231
+ // A capture's declared type reaches the parse as a declaration, so it belongs with context.
232
+ const preamble = TemplateEngine.parameterPreamble(parameters);
208
233
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
209
-
210
- // Add preamble to context statements (so they're skipped during extraction)
211
234
  const contextWithPreamble = preamble.length > 0
212
235
  ? [...contextStatements, ...preamble]
213
236
  : contextStatements;
237
+ return templateCache.getOrParse(templateString, [], contextWithPreamble, dependencies, types);
238
+ }
214
239
 
215
- // Use cache to get or parse the compilation unit
216
- const cu = await templateCache.getOrParse(
217
- templateString,
218
- [],
219
- contextWithPreamble,
220
- dependencies
221
- );
240
+ /**
241
+ * The modules the template's context binds, for the caller to bind in the file being edited.
242
+ * An `import` or `require` states one; anything else — a `declare`, a helper signature — types
243
+ * the template without asking for a binding.
244
+ */
245
+ static async getContextBindings(
246
+ templateParts: TemplateStringsArray,
247
+ parameters: Parameter[],
248
+ contextStatements: string[] = [],
249
+ dependencies: Record<string, string> = {},
250
+ types?: string[]
251
+ ): Promise<ContextBinding[]> {
252
+ const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
253
+ // The template's own code is the last statement, so everything ahead of it is context.
254
+ const context = {...cu, statements: cu.statements.slice(0, -1)};
255
+ const attributed = new Set<string>();
256
+ walk(context.statements, node => {
257
+ if (isIdentifier(node) && (node.type !== undefined || node.fieldType !== undefined)) {
258
+ attributed.add(node.simpleName);
259
+ }
260
+ return true;
261
+ });
262
+ return moduleScopeBindings(context)
263
+ .filter(b => b.module !== undefined)
264
+ .map(b => ({...b, attributed: attributed.has(b.name)}));
265
+ }
266
+
267
+ static async getTemplateTree(
268
+ templateParts: TemplateStringsArray,
269
+ parameters: Parameter[],
270
+ contextStatements: string[] = [],
271
+ dependencies: Record<string, string> = {},
272
+ types?: string[]
273
+ ): Promise<J> {
274
+ const cu = await TemplateEngine.parseWithContext(templateParts, parameters, contextStatements, dependencies, types);
222
275
 
223
276
  // Check if there are any statements
224
277
  if (!cu.statements || cu.statements.length === 0) {
225
- throw new Error(`Failed to parse template code (no statements):\n${templateString}`);
278
+ throw new Error(`Failed to parse template code (no statements):\n${TemplateEngine.buildTemplateString(templateParts, parameters)}`);
226
279
  }
227
280
 
228
281
  // The template code is always the last statement (after context + preamble)
@@ -244,15 +297,21 @@ export class TemplateEngine {
244
297
  * @param coordinates The coordinates specifying where and how to insert the generated AST
245
298
  * @param values Map of capture names to values to replace the parameters with
246
299
  * @param wrappersMap Map of capture names to J.RightPadded wrappers (for preserving markers)
300
+ * @param format Whether to fit the result to where it lands
301
+ * @param renames Local names for the template's declared bindings, keyed as declared
302
+ * @param modules The module each declared binding names, keyed as declared
247
303
  * @returns A Promise resolving to the generated AST node
248
304
  */
249
305
  static async applyTemplateFromAst(
250
- ast: JS.CompilationUnit,
306
+ ast: J,
251
307
  parameters: Parameter[],
252
308
  cursor: Cursor,
253
309
  coordinates: JavaCoordinates,
254
310
  values: Pick<Map<string, J>, 'get'> = new Map(),
255
- wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map()
311
+ wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map(),
312
+ format: boolean = true,
313
+ renames: Record<string, string> = {},
314
+ modules: Record<string, string> = {}
256
315
  ): Promise<J | undefined> {
257
316
  // Create substitutions map for placeholders
258
317
  const substitutions = new Map<string, Parameter>();
@@ -264,9 +323,13 @@ export class TemplateEngine {
264
323
  // Before substitution, so that ids carried over from the source tree survive this pass
265
324
  const fresh = await randomizeIds(ast);
266
325
 
326
+ const bound = Object.keys(renames).length > 0
327
+ ? await renameBindings(fresh.tree as J, renames, modules)
328
+ : fresh.tree;
329
+
267
330
  // Unsubstitute placeholders with actual parameter values and match results
268
331
  const visitor = new PlaceholderReplacementVisitor(substitutions, values, wrappersMap);
269
- const unsubstitutedAst = (await visitor.visit(fresh.tree, null))!;
332
+ const unsubstitutedAst = (await visitor.visit(bound, null))!;
270
333
 
271
334
  // An id may only be kept where the node answering to it is leaving the tree, which is the
272
335
  // subtree this application replaces. A parameter named twice, or spliced in from somewhere
@@ -280,7 +343,7 @@ export class TemplateEngine {
280
343
  const uniqueAst = await retainIds(unsubstitutedAst, retainable);
281
344
 
282
345
  // Apply the template to the current AST
283
- return new TemplateApplier(cursor, coordinates, uniqueAst).apply();
346
+ return new TemplateApplier(cursor, coordinates, uniqueAst, format).apply();
284
347
  }
285
348
 
286
349
  /**
@@ -289,7 +352,33 @@ export class TemplateEngine {
289
352
  * @param parameters The parameters
290
353
  * @returns Array of preamble statements
291
354
  */
292
- private static generateTypePreamble(parameters: Parameter[]): string[] {
355
+ /** The declarations that give a capture's placeholder its type while the pattern is parsed. */
356
+ static capturePreamble(captures: (Capture | Any | RawCode)[]): string[] {
357
+ const preamble: string[] = [];
358
+ for (const capture of captures) {
359
+ // Raw code is spliced in as source, so it declares nothing
360
+ if (capture instanceof RawCode || (capture && typeof capture === 'object' && (capture as any)[RAW_CODE_SYMBOL])) {
361
+ continue;
362
+ }
363
+
364
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
365
+ const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
366
+ if (captureType) {
367
+ const typeString = typeof captureType === 'string'
368
+ ? captureType
369
+ : this.typeToString(captureType);
370
+ // `any` attributes nothing, so a declaration for it would only cost a parse
371
+ if (typeString !== 'any') {
372
+ const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
373
+ preamble.push(`let ${placeholder}: ${typeString};`);
374
+ }
375
+ }
376
+ }
377
+ return preamble;
378
+ }
379
+
380
+ /** The parameter counterpart of {@link capturePreamble}. */
381
+ static parameterPreamble(parameters: Parameter[]): string[] {
293
382
  const preamble: string[] = [];
294
383
 
295
384
  for (let i = 0; i < parameters.length; i++) {
@@ -377,7 +466,7 @@ export class TemplateEngine {
377
466
 
378
467
  // Always wrap in function body - let the parser decide what it is,
379
468
  // then we'll extract intelligently based on what was parsed
380
- return `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
469
+ return `function ${WRAPPER_FUNCTION_NAME}() { ${dedentTemplate(result)} }`;
381
470
  }
382
471
 
383
472
  /**
@@ -428,45 +517,23 @@ export class TemplateEngine {
428
517
  }
429
518
 
430
519
  /**
431
- * Gets the parsed and extracted pattern tree with capture markers attached.
432
- * This is the entry point for pattern processing, providing pattern-specific
433
- * functionality on top of the shared template tree generation.
520
+ * Gets the parsed and extracted pattern tree, with placeholder identifiers left bare;
521
+ * `attachCaptureMarkers` binds it to a particular set of captures.
434
522
  *
435
523
  * @param templateParts The string parts of the template
436
524
  * @param captures The captures between the string parts (can include RawCode)
437
525
  * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
438
526
  * @param dependencies NPM dependencies for type attribution
439
- * @returns A Promise resolving to the extracted pattern AST with capture markers
527
+ * @returns A Promise resolving to the extracted pattern AST
440
528
  */
441
529
  static async getPatternTree(
442
530
  templateParts: TemplateStringsArray,
443
531
  captures: (Capture | Any | RawCode)[],
444
532
  contextStatements: string[] = [],
445
- dependencies: Record<string, string> = {}
533
+ dependencies: Record<string, string> = {},
534
+ types?: string[]
446
535
  ): Promise<J> {
447
- // Generate type preamble for captures with types (skip RawCode)
448
- const preamble: string[] = [];
449
- for (const capture of captures) {
450
- // Skip raw code - it's not a capture
451
- if (capture instanceof RawCode || (capture && typeof capture === 'object' && (capture as any)[RAW_CODE_SYMBOL])) {
452
- continue;
453
- }
454
-
455
- const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
456
- const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
457
- if (captureType) {
458
- // Convert Type to string if needed
459
- const typeString = typeof captureType === 'string'
460
- ? captureType
461
- : this.typeToString(captureType);
462
- // Only add preamble if we have a concrete type (not 'any')
463
- if (typeString !== 'any') {
464
- const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
465
- preamble.push(`let ${placeholder}: ${typeString};`);
466
- }
467
- }
468
- // Don't add preamble declarations without types - they don't provide type attribution
469
- }
536
+ const preamble = TemplateEngine.capturePreamble(captures);
470
537
 
471
538
  // Build the template string with placeholders for captures and raw code
472
539
  let result = '';
@@ -505,7 +572,8 @@ export class TemplateEngine {
505
572
  templateString,
506
573
  actualCaptures,
507
574
  contextWithPreamble,
508
- dependencies
575
+ dependencies,
576
+ types
509
577
  );
510
578
 
511
579
  // Check if there are any statements
@@ -517,18 +585,25 @@ export class TemplateEngine {
517
585
  const lastStatement = cu.statements[cu.statements.length - 1].element;
518
586
 
519
587
  // Extract from wrapper using shared utility
520
- const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
588
+ return PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
589
+ }
521
590
 
522
- // Attach CaptureMarkers to capture identifiers (only for actual captures, not raw code)
523
- const visitor = new MarkerAttachmentVisitor(actualCaptures);
524
- return (await visitor.visit(extracted, undefined))!;
591
+ /**
592
+ * Binds a pattern tree to a set of captures by attaching a CaptureMarker, carrying that
593
+ * capture's constraint and variadic options, to each placeholder identifier.
594
+ */
595
+ static async attachCaptureMarkers(tree: J, captures: (Capture | Any | RawCode)[]): Promise<J> {
596
+ const actualCaptures = captures.filter(c =>
597
+ !(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL]))
598
+ ) as (Capture | Any)[];
599
+ return (await new MarkerAttachmentVisitor(actualCaptures).visit(tree, undefined))!;
525
600
  }
526
601
  }
527
602
 
528
603
  /**
529
604
  * Visitor that attaches CaptureMarkers to capture identifiers in pattern ASTs.
530
605
  * This allows efficient capture detection without string parsing during matching.
531
- * Used by TemplateEngine.getPatternTree() for pattern-specific processing.
606
+ * Reached through TemplateEngine.attachCaptureMarkers().
532
607
  */
533
608
  class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
534
609
  constructor(private readonly captures: (Capture | Any)[]) {
@@ -656,7 +731,8 @@ export class TemplateApplier {
656
731
  constructor(
657
732
  private readonly cursor: Cursor,
658
733
  private readonly coordinates: JavaCoordinates,
659
- private readonly ast: J
734
+ private readonly ast: J,
735
+ private readonly shouldFormat: boolean = true
660
736
  ) {
661
737
  }
662
738
 
@@ -738,6 +814,10 @@ export class TemplateApplier {
738
814
  prefix: originalTree.prefix
739
815
  };
740
816
 
817
+ if (!this.shouldFormat) {
818
+ return {...result, id: resultToUse.id};
819
+ }
820
+
741
821
  // Apply auto-formatting to the result
742
822
  const formatted =
743
823
  await maybeAutoFormat(originalTree, result, null, undefined, this.cursor?.parent);
@@ -26,6 +26,7 @@ export type {
26
26
  TemplateParameter,
27
27
  TemplateOptions,
28
28
  RewriteRule,
29
+ TryOnOptions,
29
30
  RewriteConfig,
30
31
  DebugOptions,
31
32
  DebugLogEntry,
@@ -233,27 +233,30 @@ export class Pattern {
233
233
  const cacheKey = generateCacheKey(
234
234
  this.templateParts,
235
235
  capturesKey,
236
- contextStatements,
237
- this._options.dependencies || {}
236
+ // A capture's type reaches the parse as a declaration, so it shapes the tree the same
237
+ // way an explicit context statement does and belongs in the key alongside one.
238
+ [...contextStatements, ...TemplateEngine.capturePreamble(this.captures)],
239
+ this._options.dependencies || {},
240
+ this._options.types
238
241
  );
239
242
 
240
243
  // Level 2: Global cache (fast path - shared with Template)
241
- const cached = globalAstCache.get(cacheKey);
242
- if (cached) {
243
- this._cachedAstPattern = cached;
244
- return cached;
244
+ let tree = globalAstCache.get(cacheKey);
245
+ if (!tree) {
246
+ // Level 3: Compute via TemplateEngine (slow path)
247
+ tree = await TemplateEngine.getPatternTree(
248
+ this.templateParts,
249
+ this.captures,
250
+ contextStatements,
251
+ this._options.dependencies || {},
252
+ this._options.types
253
+ );
254
+ globalAstCache.set(cacheKey, tree);
245
255
  }
246
256
 
247
- // Level 3: Compute via TemplateEngine (slow path)
248
- const result = await TemplateEngine.getPatternTree(
249
- this.templateParts,
250
- this.captures,
251
- contextStatements,
252
- this._options.dependencies || {}
253
- );
254
-
255
- // Cache in both levels
256
- globalAstCache.set(cacheKey, result);
257
+ // The key names captures but says nothing about their constraints, so two patterns of the
258
+ // same shape share an entry; markers are attached per instance to keep them apart.
259
+ const result = await TemplateEngine.attachCaptureMarkers(tree, this.captures);
257
260
  this._cachedAstPattern = result;
258
261
 
259
262
  return result;
@@ -13,7 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {Cursor, isTree} from '../..';
16
+ import {Cursor, isTree, Markers} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {JS} from '..';
19
19
  import {JavaScriptVisitor} from '../visitor';
@@ -42,7 +42,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
42
42
  const replacement = this.replacePlaceholder(tree);
43
43
  if (replacement !== tree) {
44
44
  // `this.cursor` is still the enclosing template node: `super.visit()` has not pushed this one
45
- return maybeParenthesize(enclosingTree(parent ?? this.cursor), tree.id, replacement, true) as R;
45
+ return maybeParenthesize(enclosingTree(parent ?? this.cursor), tree.id, replacement, tree.markers) as R;
46
46
  }
47
47
  }
48
48
 
@@ -82,8 +82,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
82
82
  return super.visitContainer(container, p);
83
83
  }
84
84
 
85
- // Expand variadic placeholders in the container's elements
86
- const newElements = await this.expandVariadicElements(container.elements, undefined, p);
85
+ // A container's element layout is the author's; a block lays its statements out by indentation
86
+ const newElements = await this.expandVariadicElements(container.elements, undefined, p, true);
87
87
 
88
88
  return produce(container, draft => {
89
89
  draft.elements = newElements as any;
@@ -175,6 +175,22 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
175
175
  };
176
176
  }
177
177
 
178
+ /** As `mergePrefix`, but a line break the source wrote and the template did not is the source's layout. */
179
+ private mergeElementPrefix(sourcePrefix: J.Space, templatePrefix: J.Space): J.Space {
180
+ return sourcePrefix.whitespace.includes('\n') && !templatePrefix.whitespace.includes('\n') ?
181
+ sourcePrefix : this.mergePrefix(sourcePrefix, templatePrefix);
182
+ }
183
+
184
+ /** As `mergePrefix`, for markers: everything the value was matched with, plus the kinds only the slot wrote. */
185
+ private mergeMarkers(sourceMarkers: Markers, templateMarkers: Markers): Markers {
186
+ const added = templateMarkers.markers.filter(
187
+ template => !sourceMarkers.markers.some(source => source.kind === template.kind));
188
+ return added.length === 0 ? sourceMarkers : {
189
+ ...sourceMarkers,
190
+ markers: [...sourceMarkers.markers, ...added]
191
+ };
192
+ }
193
+
178
194
  /**
179
195
  * Expands variadic placeholders in a list of elements.
180
196
  *
@@ -186,7 +202,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
186
202
  private async expandVariadicElements(
187
203
  elements: J.RightPadded<J>[],
188
204
  unwrapElement: (element: J) => J = (e) => e,
189
- p: any
205
+ p: any,
206
+ ownLayout: boolean = false
190
207
  ): Promise<J.RightPadded<J>[]> {
191
208
  const newElements: J.RightPadded<J>[] = [];
192
209
 
@@ -263,7 +280,9 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
263
280
  if (i === 0 && draft.element) {
264
281
  // Merge the placeholder's prefix with the first item's prefix
265
282
  // Modify prefix directly within the draft
266
- draft.element.prefix = this.mergePrefix(draft.element.prefix, element.prefix);
283
+ draft.element.prefix = ownLayout ?
284
+ this.mergeElementPrefix(draft.element.prefix, element.prefix) :
285
+ this.mergePrefix(draft.element.prefix, element.prefix);
267
286
  }
268
287
  // Keep all other wrapper properties (including markers with Semicolon)
269
288
  }));
@@ -273,7 +292,9 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
273
292
  newElements.push(produce(wrapped, draft => {
274
293
  draft.element = produce(elem, itemDraft => {
275
294
  if (i === 0) {
276
- itemDraft.prefix = this.mergePrefix(elem.prefix, element.prefix);
295
+ itemDraft.prefix = ownLayout ?
296
+ this.mergeElementPrefix(elem.prefix, element.prefix) :
297
+ this.mergePrefix(elem.prefix, element.prefix);
277
298
  }
278
299
  // For i > 0, prefix is already correct, no changes needed
279
300
  });
@@ -375,7 +396,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
375
396
  if (isTree(propertyValue)) {
376
397
  const propValueAsJ = propertyValue as J;
377
398
  return produce(propValueAsJ, draft => {
378
- draft.markers = placeholder.markers;
399
+ draft.markers = this.mergeMarkers(propValueAsJ.markers, placeholder.markers);
379
400
  draft.prefix = this.mergePrefix(propValueAsJ.prefix, placeholder.prefix);
380
401
  });
381
402
  }
@@ -410,7 +431,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
410
431
  const matchedNode = this.values.get(name);
411
432
  if (matchedNode && !Array.isArray(matchedNode)) {
412
433
  return produce(matchedNode, draft => {
413
- draft.markers = placeholder.markers;
434
+ draft.markers = this.mergeMarkers(matchedNode.markers, placeholder.markers);
414
435
  draft.prefix = this.mergePrefix(matchedNode.prefix, placeholder.prefix);
415
436
  });
416
437
  }
@@ -427,7 +448,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
427
448
  // Extract the element from the J.RightPadded wrapper
428
449
  const element = param.value.element as J;
429
450
  return produce(element, draft => {
430
- draft.markers = placeholder.markers;
451
+ draft.markers = this.mergeMarkers(element.markers, placeholder.markers);
431
452
  draft.prefix = this.mergePrefix(element.prefix, placeholder.prefix);
432
453
  });
433
454
  }
@@ -447,7 +468,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
447
468
  if (isTree(param.value)) {
448
469
  // Return the AST node, preserving comments from the source
449
470
  return produce(param.value as J, draft => {
450
- draft.markers = placeholder.markers;
471
+ draft.markers = this.mergeMarkers(param.value.markers, placeholder.markers);
451
472
  draft.prefix = this.mergePrefix(param.value.prefix, placeholder.prefix);
452
473
  });
453
474
  }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import {isTree} from '../..';
17
17
  import {emptySpace, J} from '../../java';
18
- import {emptyMarkers, Marker, markers} from '../../markers';
18
+ import {emptyMarkers, Marker, markers, Markers} from '../../markers';
19
19
  import {randomId} from '../../uuid';
20
20
  import {JS} from '..';
21
21
 
@@ -319,9 +319,12 @@ export function requiredPrecedence(parent: J, childId: string): number | undefin
319
319
  return slotConstraints(parent, childId)?.precedence;
320
320
  }
321
321
 
322
- /** Parenthesizes `expression` if the slot of `parent` holding `childId` would otherwise reparse it. */
322
+ /**
323
+ * Parenthesizes `expression` if the slot of `parent` holding `childId` would otherwise reparse it.
324
+ * `slotMarkers` are the markers the slot contributed; {@link parenthesize} says what they decide.
325
+ */
323
326
  export function maybeParenthesize(parent: J | undefined, childId: string, expression: J,
324
- slotOwnsTrailingMarkers: boolean = false): J {
327
+ slotMarkers?: Markers): J {
325
328
  if (!parent) {
326
329
  return expression;
327
330
  }
@@ -334,10 +337,10 @@ export function maybeParenthesize(parent: J | undefined, childId: string, expres
334
337
  // A statement wrapper is transparent here: the parentheses belong around the expression
335
338
  if (expression.kind === JS.Kind.ExpressionStatement) {
336
339
  const inner = (expression as JS.ExpressionStatement).expression;
337
- const wrapped = wrapIfNeeded(parent, childId, constraints, inner, slotOwnsTrailingMarkers);
340
+ const wrapped = wrapIfNeeded(parent, childId, constraints, inner, slotMarkers);
338
341
  return wrapped === inner ? expression : {...expression, expression: wrapped} as JS.ExpressionStatement;
339
342
  }
340
- return wrapIfNeeded(parent, childId, constraints, expression, slotOwnsTrailingMarkers);
343
+ return wrapIfNeeded(parent, childId, constraints, expression, slotMarkers);
341
344
  }
342
345
 
343
346
  /** The nearest enclosing LST node in a cursor path, skipping the padding wrappers visitors push. */
@@ -352,18 +355,23 @@ export function enclosingTree(cursor: { value: any, parent?: any } | undefined):
352
355
  return undefined;
353
356
  }
354
357
 
355
- /** Wraps in `J.Parentheses`, moving the prefix out so the surrounding whitespace survives. */
356
- export function parenthesize(expression: J, slotOwnsTrailingMarkers: boolean = false): J.Parentheses<J> {
357
- const trailing = slotOwnsTrailingMarkers ? expression.markers.markers.filter(isTrailingMarker) : [];
358
- const inner = trailing.length === 0 ? expression : {
358
+ /**
359
+ * Wraps in `J.Parentheses`, moving the prefix out so the surrounding whitespace survives. A trailing marker
360
+ * `slotMarkers` also carries belongs to the slot around the expression, so it moves out too: `${x}!` with `x`
361
+ * bound to `a + b` gives `(a + b)!`, while a bare `${x}` bound to `a!` gives `(a!)`.
362
+ */
363
+ export function parenthesize(expression: J, slotMarkers?: Markers): J.Parentheses<J> {
364
+ const hoisted = expression.markers.markers.filter(
365
+ m => isTrailingMarker(m) && slotMarkers?.markers.some(slot => slot.kind === m.kind));
366
+ const inner = hoisted.length === 0 ? expression : {
359
367
  ...expression,
360
- markers: markers(...expression.markers.markers.filter(m => !isTrailingMarker(m)))
368
+ markers: markers(...expression.markers.markers.filter(m => !hoisted.includes(m)))
361
369
  };
362
370
  return {
363
371
  kind: J.Kind.Parentheses,
364
372
  id: randomId(),
365
373
  prefix: expression.prefix,
366
- markers: trailing.length === 0 ? emptyMarkers : markers(...trailing),
374
+ markers: hoisted.length === 0 ? emptyMarkers : markers(...hoisted),
367
375
  tree: {
368
376
  kind: J.Kind.RightPadded,
369
377
  element: {...inner, prefix: emptySpace},
@@ -374,7 +382,7 @@ export function parenthesize(expression: J, slotOwnsTrailingMarkers: boolean = f
374
382
  }
375
383
 
376
384
  function wrapIfNeeded(parent: J, childId: string, constraints: SlotConstraints, expression: J,
377
- slotOwnsTrailingMarkers: boolean): J {
385
+ slotMarkers: Markers | undefined): J {
378
386
  if (precedenceOf(expression) < constraints.precedence ||
379
387
  (constraints.noCallShape && isCallShaped(expression)) ||
380
388
  (constraints.noOptionalChain && hasOptionalChain(expression)) ||
@@ -383,7 +391,7 @@ function wrapIfNeeded(parent: J, childId: string, constraints: SlotConstraints,
383
391
  (constraints.followedByDot && isDotAdjacentNumber(expression)) ||
384
392
  mixesNullishWithLogical(parent, expression) ||
385
393
  wouldFuseSigns(parent, childId, expression)) {
386
- return parenthesize(expression, slotOwnsTrailingMarkers);
394
+ return parenthesize(expression, slotMarkers);
387
395
  }
388
396
  return expression;
389
397
  }