@openrewrite/rewrite 8.91.0 → 8.91.1

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 (116) 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 +39 -20
  10. package/dist/javascript/add-import.d.ts.map +1 -1
  11. package/dist/javascript/add-import.js +513 -243
  12. package/dist/javascript/add-import.js.map +1 -1
  13. package/dist/javascript/assertions.d.ts +7 -0
  14. package/dist/javascript/assertions.d.ts.map +1 -1
  15. package/dist/javascript/assertions.js +31 -5
  16. package/dist/javascript/assertions.js.map +1 -1
  17. package/dist/javascript/comparator.d.ts +5 -0
  18. package/dist/javascript/comparator.d.ts.map +1 -1
  19. package/dist/javascript/comparator.js +34 -9
  20. package/dist/javascript/comparator.js.map +1 -1
  21. package/dist/javascript/format/format.d.ts.map +1 -1
  22. package/dist/javascript/format/format.js +8 -9
  23. package/dist/javascript/format/format.js.map +1 -1
  24. package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
  25. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
  26. package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
  27. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  28. package/dist/javascript/node-resolution-result.d.ts +19 -1
  29. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  30. package/dist/javascript/node-resolution-result.js +37 -2
  31. package/dist/javascript/node-resolution-result.js.map +1 -1
  32. package/dist/javascript/package-exported-types.d.ts.map +1 -1
  33. package/dist/javascript/package-exported-types.js +56 -72
  34. package/dist/javascript/package-exported-types.js.map +1 -1
  35. package/dist/javascript/parser.d.ts.map +1 -1
  36. package/dist/javascript/parser.js +10 -3
  37. package/dist/javascript/parser.js.map +1 -1
  38. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  39. package/dist/javascript/recipes/change-import.js +4 -8
  40. package/dist/javascript/recipes/change-import.js.map +1 -1
  41. package/dist/javascript/style.d.ts.map +1 -1
  42. package/dist/javascript/style.js +3 -10
  43. package/dist/javascript/style.js.map +1 -1
  44. package/dist/javascript/templating/bindings.d.ts +19 -0
  45. package/dist/javascript/templating/bindings.d.ts.map +1 -0
  46. package/dist/javascript/templating/bindings.js +121 -0
  47. package/dist/javascript/templating/bindings.js.map +1 -0
  48. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  49. package/dist/javascript/templating/comparator.js +5 -6
  50. package/dist/javascript/templating/comparator.js.map +1 -1
  51. package/dist/javascript/templating/engine.d.ts +18 -7
  52. package/dist/javascript/templating/engine.d.ts.map +1 -1
  53. package/dist/javascript/templating/engine.js +70 -42
  54. package/dist/javascript/templating/engine.js.map +1 -1
  55. package/dist/javascript/templating/index.d.ts +1 -1
  56. package/dist/javascript/templating/index.d.ts.map +1 -1
  57. package/dist/javascript/templating/index.js.map +1 -1
  58. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  59. package/dist/javascript/templating/pattern.js +13 -10
  60. package/dist/javascript/templating/pattern.js.map +1 -1
  61. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  62. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  63. package/dist/javascript/templating/placeholder-replacement.js +14 -5
  64. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  65. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  66. package/dist/javascript/templating/rewrite.js +14 -18
  67. package/dist/javascript/templating/rewrite.js.map +1 -1
  68. package/dist/javascript/templating/template.d.ts +8 -0
  69. package/dist/javascript/templating/template.d.ts.map +1 -1
  70. package/dist/javascript/templating/template.js +43 -3
  71. package/dist/javascript/templating/template.js.map +1 -1
  72. package/dist/javascript/templating/types.d.ts +47 -1
  73. package/dist/javascript/templating/types.d.ts.map +1 -1
  74. package/dist/javascript/templating/utils.d.ts +5 -0
  75. package/dist/javascript/templating/utils.d.ts.map +1 -1
  76. package/dist/javascript/templating/utils.js +17 -0
  77. package/dist/javascript/templating/utils.js.map +1 -1
  78. package/dist/javascript/type-mapping.d.ts +1 -0
  79. package/dist/javascript/type-mapping.d.ts.map +1 -1
  80. package/dist/javascript/type-mapping.js +52 -2
  81. package/dist/javascript/type-mapping.js.map +1 -1
  82. package/dist/rewrite-javascript-version.txt +1 -1
  83. package/dist/style.d.ts +2 -0
  84. package/dist/style.d.ts.map +1 -1
  85. package/dist/style.js +5 -0
  86. package/dist/style.js.map +1 -1
  87. package/dist/test/rewrite-test.d.ts +12 -0
  88. package/dist/test/rewrite-test.d.ts.map +1 -1
  89. package/dist/test/rewrite-test.js +40 -3
  90. package/dist/test/rewrite-test.js.map +1 -1
  91. package/package.json +1 -1
  92. package/src/java/formatting-utils.ts +14 -10
  93. package/src/java/type.ts +5 -0
  94. package/src/javascript/add-import.ts +590 -230
  95. package/src/javascript/assertions.ts +33 -5
  96. package/src/javascript/comparator.ts +39 -10
  97. package/src/javascript/format/format.ts +9 -10
  98. package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
  99. package/src/javascript/node-resolution-result.ts +59 -4
  100. package/src/javascript/package-exported-types.ts +60 -74
  101. package/src/javascript/parser.ts +10 -3
  102. package/src/javascript/recipes/change-import.ts +4 -7
  103. package/src/javascript/style.ts +3 -11
  104. package/src/javascript/templating/bindings.ts +129 -0
  105. package/src/javascript/templating/comparator.ts +5 -6
  106. package/src/javascript/templating/engine.ts +82 -44
  107. package/src/javascript/templating/index.ts +2 -0
  108. package/src/javascript/templating/pattern.ts +16 -15
  109. package/src/javascript/templating/placeholder-replacement.ts +16 -5
  110. package/src/javascript/templating/rewrite.ts +16 -18
  111. package/src/javascript/templating/template.ts +45 -3
  112. package/src/javascript/templating/types.ts +53 -1
  113. package/src/javascript/templating/utils.ts +17 -0
  114. package/src/javascript/type-mapping.ts +54 -2
  115. package/src/style.ts +5 -0
  116. package/src/test/rewrite-test.ts +61 -3
@@ -0,0 +1,129 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {J, Type} from '../../java';
17
+ import {JavaScriptVisitor} from '../visitor';
18
+ import {Cursor} from '../../tree';
19
+ import {ModuleBinding} from './types';
20
+
21
+ /**
22
+ * Whether the template's dependencies bring a workspace that could resolve `module`.
23
+ */
24
+ export function isResolvable(module: string, dependencies: Record<string, string>): boolean {
25
+ const segments = module.split('/');
26
+ const pkg = module.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
27
+ return Object.prototype.hasOwnProperty.call(dependencies, pkg);
28
+ }
29
+
30
+ /**
31
+ * What a declared binding's name is parsed against, ahead of the template and so out of its output.
32
+ * An import is what carries attribution, and costs a module resolution to get it; a declaration
33
+ * names the binding for a module no workspace could have resolved anyway.
34
+ */
35
+ export function bindingContextStatement(name: string, binding: ModuleBinding, dependencies: Record<string, string>): string {
36
+ if (!isResolvable(binding.module, dependencies)) {
37
+ return binding.typeOnly ? `type ${name} = any;` : `declare const ${name}: any;`;
38
+ }
39
+ const type = binding.typeOnly ? 'type ' : '';
40
+ if (binding.member === '*') {
41
+ return `import ${type}* as ${name} from '${binding.module}';`;
42
+ }
43
+ if (binding.member === undefined || binding.member === 'default') {
44
+ return `import ${type}${name} from '${binding.module}';`;
45
+ }
46
+ const specifier = binding.member === name ? name : `${binding.member} as ${name}`;
47
+ return `import ${type}{${specifier}} from '${binding.module}';`;
48
+ }
49
+
50
+ /**
51
+ * Renames the identifiers a template uses for its declared bindings to the names the file
52
+ * actually binds. Runs before parameter substitution, so only the template's own code is in
53
+ * scope and a caller's captured code is never rewritten.
54
+ */
55
+ export async function renameBindings<T extends J>(tree: T, renames: Record<string, string>, modules: Record<string, string>): Promise<T> {
56
+ return new RenameBindingsVisitor(renames, modules).visit(tree, undefined) as Promise<T>;
57
+ }
58
+
59
+ class RenameBindingsVisitor extends JavaScriptVisitor<undefined> {
60
+ constructor(private readonly renames: Record<string, string>,
61
+ private readonly modules: Record<string, string>) {
62
+ super();
63
+ }
64
+
65
+ override async visitIdentifier(identifier: J.Identifier, p: undefined): Promise<J | undefined> {
66
+ const renamed = this.renames[identifier.simpleName];
67
+ if (renamed === undefined || renamed === identifier.simpleName) {
68
+ return identifier;
69
+ }
70
+
71
+ // Attribution settles it where the context import resolved. It is absent for a module the
72
+ // parse could not reach, and the identifier's position decides instead.
73
+ const resolved = resolvedModule(identifier);
74
+ const refersToBinding = resolved !== undefined
75
+ ? resolved === this.modules[identifier.simpleName]
76
+ : !namesItsParent(this.cursor, identifier);
77
+
78
+ return refersToBinding ? {...identifier, simpleName: renamed} as J.Identifier : identifier;
79
+ }
80
+ }
81
+
82
+ /** Whether the parent is naming this identifier — as a property, a method, a declaration — rather than referencing it. */
83
+ function namesItsParent(cursor: Cursor, identifier: J.Identifier): boolean {
84
+ let c: Cursor | undefined = cursor.parent;
85
+ while (c && isPadding(c.value)) {
86
+ c = c.parent;
87
+ }
88
+ const parent = c?.value as { kind?: string; name?: unknown; select?: unknown } | undefined;
89
+
90
+ // A call names a member of whatever it selects from. With nothing selected there is no member,
91
+ // and its `name` is a reference to the function being called.
92
+ if (parent?.kind === J.Kind.MethodInvocation && !parent.select) {
93
+ return false;
94
+ }
95
+
96
+ const name = parent?.name;
97
+ return name === identifier || (name as { element?: unknown } | undefined)?.element === identifier;
98
+ }
99
+
100
+ function isPadding(value: unknown): boolean {
101
+ const kind = (value as { kind?: string } | undefined)?.kind;
102
+ return kind === J.Kind.RightPadded || kind === J.Kind.LeftPadded || kind === J.Kind.Container;
103
+ }
104
+
105
+ /** The module an identifier's attribution traces back to, following the owning-class chain to its root. */
106
+ function resolvedModule(identifier: J.Identifier): string | undefined {
107
+ const fieldType = identifier.fieldType;
108
+ if (fieldType?.kind === Type.Kind.Variable) {
109
+ const owner = (fieldType as Type.Variable).owner;
110
+ return owner && Type.isClass(owner) ? rootName(owner as Type.Class) : undefined;
111
+ }
112
+ const type = identifier.type;
113
+ if (type && Type.isMethod(type)) {
114
+ const declaring = (type as Type.Method).declaringType;
115
+ return declaring ? rootName(declaring as Type.Class) : undefined;
116
+ }
117
+ if (type && Type.isClass(type)) {
118
+ return rootName(type as Type.Class);
119
+ }
120
+ return undefined;
121
+ }
122
+
123
+ function rootName(classType: Type.Class): string {
124
+ let current: Type.Class = classType;
125
+ while (current.owningClass && Type.isClass(current.owningClass)) {
126
+ current = current.owningClass as Type.Class;
127
+ }
128
+ return Type.FullyQualified.getFullyQualifiedName(current);
129
+ }
@@ -344,13 +344,12 @@ export class PatternMatchingComparator extends JavaScriptSemanticComparatorVisit
344
344
  if (!this.match) return methodInvocation;
345
345
  }
346
346
 
347
- // Compare typeParameters
348
- if ((methodInvocation.typeParameters === undefined) !== (otherMethodInvocation.typeParameters === undefined)) {
349
- return this.structuralMismatch('typeParameters');
350
- }
347
+ // A pattern that spells out no type arguments says nothing about them, as with parentheses
348
+ if (methodInvocation.typeParameters) {
349
+ if (!otherMethodInvocation.typeParameters) {
350
+ return this.structuralMismatch('typeParameters');
351
+ }
351
352
 
352
- // Visit typeParameters if present
353
- if (methodInvocation.typeParameters && otherMethodInvocation.typeParameters) {
354
353
  if (methodInvocation.typeParameters.elements.length !== otherMethodInvocation.typeParameters.elements.length) {
355
354
  return this.arrayLengthMismatch('typeParameters.elements');
356
355
  }
@@ -17,12 +17,13 @@ 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";
@@ -82,6 +83,21 @@ let templateSourceFileCache: Map<string, ts.SourceFile> | undefined;
82
83
  */
83
84
  export function setTemplateSourceFileCache(cache?: Map<string, ts.SourceFile>): void {
84
85
  templateSourceFileCache = cache;
86
+ templateParsers.clear();
87
+ }
88
+
89
+ // Every template parses under `template.tsx`, so one parser per workspace carries its program
90
+ // from one compile to the next; see `JavaScriptParser.parse`.
91
+ const templateParsers: Map<string, JavaScriptParser> = new Map();
92
+
93
+ function templateParser(workspaceDir?: string): JavaScriptParser {
94
+ const key = workspaceDir ?? "";
95
+ let parser = templateParsers.get(key);
96
+ if (!parser) {
97
+ parser = new JavaScriptParser({relativeTo: workspaceDir, sourceFileCache: templateSourceFileCache});
98
+ templateParsers.set(key, parser);
99
+ }
100
+ return parser;
85
101
  }
86
102
 
87
103
  /**
@@ -147,10 +163,7 @@ class TemplateCache {
147
163
 
148
164
  // Parse and cache (workspace only needed during parsing)
149
165
  // Use templateSourceFileCache if configured for ~3.2x speedup on dependency file parsing
150
- const parser = new JavaScriptParser({
151
- relativeTo: workspaceDir,
152
- sourceFileCache: templateSourceFileCache
153
- });
166
+ const parser = templateParser(workspaceDir);
154
167
  const parseGenerator = parser.parse({text: fullTemplateString, sourcePath: 'template.tsx'});
155
168
  cu = (await parseGenerator.next()).value as JS.CompilationUnit;
156
169
 
@@ -202,7 +215,7 @@ export class TemplateEngine {
202
215
  dependencies: Record<string, string> = {}
203
216
  ): Promise<J> {
204
217
  // Generate type preamble for captures/parameters with types
205
- const preamble = TemplateEngine.generateTypePreamble(parameters);
218
+ const preamble = TemplateEngine.parameterPreamble(parameters);
206
219
 
207
220
  // Build the template string with parameter placeholders
208
221
  const templateString = TemplateEngine.buildTemplateString(templateParts, parameters);
@@ -244,6 +257,9 @@ export class TemplateEngine {
244
257
  * @param coordinates The coordinates specifying where and how to insert the generated AST
245
258
  * @param values Map of capture names to values to replace the parameters with
246
259
  * @param wrappersMap Map of capture names to J.RightPadded wrappers (for preserving markers)
260
+ * @param format Whether to fit the result to where it lands
261
+ * @param renames Local names for the template's declared bindings, keyed as declared
262
+ * @param modules The module each declared binding names, keyed as declared
247
263
  * @returns A Promise resolving to the generated AST node
248
264
  */
249
265
  static async applyTemplateFromAst(
@@ -252,7 +268,10 @@ export class TemplateEngine {
252
268
  cursor: Cursor,
253
269
  coordinates: JavaCoordinates,
254
270
  values: Pick<Map<string, J>, 'get'> = new Map(),
255
- wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map()
271
+ wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map(),
272
+ format: boolean = true,
273
+ renames: Record<string, string> = {},
274
+ modules: Record<string, string> = {}
256
275
  ): Promise<J | undefined> {
257
276
  // Create substitutions map for placeholders
258
277
  const substitutions = new Map<string, Parameter>();
@@ -264,9 +283,13 @@ export class TemplateEngine {
264
283
  // Before substitution, so that ids carried over from the source tree survive this pass
265
284
  const fresh = await randomizeIds(ast);
266
285
 
286
+ const bound = Object.keys(renames).length > 0
287
+ ? await renameBindings(fresh.tree as J, renames, modules)
288
+ : fresh.tree;
289
+
267
290
  // Unsubstitute placeholders with actual parameter values and match results
268
291
  const visitor = new PlaceholderReplacementVisitor(substitutions, values, wrappersMap);
269
- const unsubstitutedAst = (await visitor.visit(fresh.tree, null))!;
292
+ const unsubstitutedAst = (await visitor.visit(bound, null))!;
270
293
 
271
294
  // An id may only be kept where the node answering to it is leaving the tree, which is the
272
295
  // subtree this application replaces. A parameter named twice, or spliced in from somewhere
@@ -280,7 +303,7 @@ export class TemplateEngine {
280
303
  const uniqueAst = await retainIds(unsubstitutedAst, retainable);
281
304
 
282
305
  // Apply the template to the current AST
283
- return new TemplateApplier(cursor, coordinates, uniqueAst).apply();
306
+ return new TemplateApplier(cursor, coordinates, uniqueAst, format).apply();
284
307
  }
285
308
 
286
309
  /**
@@ -289,7 +312,33 @@ export class TemplateEngine {
289
312
  * @param parameters The parameters
290
313
  * @returns Array of preamble statements
291
314
  */
292
- private static generateTypePreamble(parameters: Parameter[]): string[] {
315
+ /** The declarations that give a capture's placeholder its type while the pattern is parsed. */
316
+ static capturePreamble(captures: (Capture | Any | RawCode)[]): string[] {
317
+ const preamble: string[] = [];
318
+ for (const capture of captures) {
319
+ // Raw code is spliced in as source, so it declares nothing
320
+ if (capture instanceof RawCode || (capture && typeof capture === 'object' && (capture as any)[RAW_CODE_SYMBOL])) {
321
+ continue;
322
+ }
323
+
324
+ const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName();
325
+ const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL];
326
+ if (captureType) {
327
+ const typeString = typeof captureType === 'string'
328
+ ? captureType
329
+ : this.typeToString(captureType);
330
+ // `any` attributes nothing, so a declaration for it would only cost a parse
331
+ if (typeString !== 'any') {
332
+ const placeholder = PlaceholderUtils.createCapture(captureName, undefined);
333
+ preamble.push(`let ${placeholder}: ${typeString};`);
334
+ }
335
+ }
336
+ }
337
+ return preamble;
338
+ }
339
+
340
+ /** The parameter counterpart of {@link capturePreamble}. */
341
+ static parameterPreamble(parameters: Parameter[]): string[] {
293
342
  const preamble: string[] = [];
294
343
 
295
344
  for (let i = 0; i < parameters.length; i++) {
@@ -377,7 +426,7 @@ export class TemplateEngine {
377
426
 
378
427
  // Always wrap in function body - let the parser decide what it is,
379
428
  // then we'll extract intelligently based on what was parsed
380
- return `function ${WRAPPER_FUNCTION_NAME}() { ${result} }`;
429
+ return `function ${WRAPPER_FUNCTION_NAME}() { ${dedentTemplate(result)} }`;
381
430
  }
382
431
 
383
432
  /**
@@ -428,15 +477,14 @@ export class TemplateEngine {
428
477
  }
429
478
 
430
479
  /**
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.
480
+ * Gets the parsed and extracted pattern tree, with placeholder identifiers left bare;
481
+ * `attachCaptureMarkers` binds it to a particular set of captures.
434
482
  *
435
483
  * @param templateParts The string parts of the template
436
484
  * @param captures The captures between the string parts (can include RawCode)
437
485
  * @param contextStatements Context declarations (imports, types, etc.) to prepend for type attribution
438
486
  * @param dependencies NPM dependencies for type attribution
439
- * @returns A Promise resolving to the extracted pattern AST with capture markers
487
+ * @returns A Promise resolving to the extracted pattern AST
440
488
  */
441
489
  static async getPatternTree(
442
490
  templateParts: TemplateStringsArray,
@@ -444,29 +492,7 @@ export class TemplateEngine {
444
492
  contextStatements: string[] = [],
445
493
  dependencies: Record<string, string> = {}
446
494
  ): 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
- }
495
+ const preamble = TemplateEngine.capturePreamble(captures);
470
496
 
471
497
  // Build the template string with placeholders for captures and raw code
472
498
  let result = '';
@@ -517,18 +543,25 @@ export class TemplateEngine {
517
543
  const lastStatement = cu.statements[cu.statements.length - 1].element;
518
544
 
519
545
  // Extract from wrapper using shared utility
520
- const extracted = PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
546
+ return PlaceholderUtils.extractFromWrapper(lastStatement, 'Pattern');
547
+ }
521
548
 
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))!;
549
+ /**
550
+ * Binds a pattern tree to a set of captures by attaching a CaptureMarker, carrying that
551
+ * capture's constraint and variadic options, to each placeholder identifier.
552
+ */
553
+ static async attachCaptureMarkers(tree: J, captures: (Capture | Any | RawCode)[]): Promise<J> {
554
+ const actualCaptures = captures.filter(c =>
555
+ !(c instanceof RawCode || (c && typeof c === 'object' && (c as any)[RAW_CODE_SYMBOL]))
556
+ ) as (Capture | Any)[];
557
+ return (await new MarkerAttachmentVisitor(actualCaptures).visit(tree, undefined))!;
525
558
  }
526
559
  }
527
560
 
528
561
  /**
529
562
  * Visitor that attaches CaptureMarkers to capture identifiers in pattern ASTs.
530
563
  * This allows efficient capture detection without string parsing during matching.
531
- * Used by TemplateEngine.getPatternTree() for pattern-specific processing.
564
+ * Reached through TemplateEngine.attachCaptureMarkers().
532
565
  */
533
566
  class MarkerAttachmentVisitor extends JavaScriptVisitor<undefined> {
534
567
  constructor(private readonly captures: (Capture | Any)[]) {
@@ -656,7 +689,8 @@ export class TemplateApplier {
656
689
  constructor(
657
690
  private readonly cursor: Cursor,
658
691
  private readonly coordinates: JavaCoordinates,
659
- private readonly ast: J
692
+ private readonly ast: J,
693
+ private readonly shouldFormat: boolean = true
660
694
  ) {
661
695
  }
662
696
 
@@ -738,6 +772,10 @@ export class TemplateApplier {
738
772
  prefix: originalTree.prefix
739
773
  };
740
774
 
775
+ if (!this.shouldFormat) {
776
+ return {...result, id: resultToUse.id};
777
+ }
778
+
741
779
  // Apply auto-formatting to the result
742
780
  const formatted =
743
781
  await maybeAutoFormat(originalTree, result, null, undefined, this.cursor?.parent);
@@ -25,7 +25,9 @@ export type {
25
25
  MatchOptions,
26
26
  TemplateParameter,
27
27
  TemplateOptions,
28
+ ModuleBinding,
28
29
  RewriteRule,
30
+ TryOnOptions,
29
31
  RewriteConfig,
30
32
  DebugOptions,
31
33
  DebugLogEntry,
@@ -233,27 +233,28 @@ export class Pattern {
233
233
  const cacheKey = generateCacheKey(
234
234
  this.templateParts,
235
235
  capturesKey,
236
- contextStatements,
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)],
237
239
  this._options.dependencies || {}
238
240
  );
239
241
 
240
242
  // 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;
243
+ let tree = globalAstCache.get(cacheKey);
244
+ if (!tree) {
245
+ // Level 3: Compute via TemplateEngine (slow path)
246
+ tree = await TemplateEngine.getPatternTree(
247
+ this.templateParts,
248
+ this.captures,
249
+ contextStatements,
250
+ this._options.dependencies || {}
251
+ );
252
+ globalAstCache.set(cacheKey, tree);
245
253
  }
246
254
 
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);
255
+ // The key names captures but says nothing about their constraints, so two patterns of the
256
+ // same shape share an entry; markers are attached per instance to keep them apart.
257
+ const result = await TemplateEngine.attachCaptureMarkers(tree, this.captures);
257
258
  this._cachedAstPattern = result;
258
259
 
259
260
  return result;
@@ -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,12 @@ 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
+
178
184
  /**
179
185
  * Expands variadic placeholders in a list of elements.
180
186
  *
@@ -186,7 +192,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
186
192
  private async expandVariadicElements(
187
193
  elements: J.RightPadded<J>[],
188
194
  unwrapElement: (element: J) => J = (e) => e,
189
- p: any
195
+ p: any,
196
+ ownLayout: boolean = false
190
197
  ): Promise<J.RightPadded<J>[]> {
191
198
  const newElements: J.RightPadded<J>[] = [];
192
199
 
@@ -263,7 +270,9 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
263
270
  if (i === 0 && draft.element) {
264
271
  // Merge the placeholder's prefix with the first item's prefix
265
272
  // Modify prefix directly within the draft
266
- draft.element.prefix = this.mergePrefix(draft.element.prefix, element.prefix);
273
+ draft.element.prefix = ownLayout ?
274
+ this.mergeElementPrefix(draft.element.prefix, element.prefix) :
275
+ this.mergePrefix(draft.element.prefix, element.prefix);
267
276
  }
268
277
  // Keep all other wrapper properties (including markers with Semicolon)
269
278
  }));
@@ -273,7 +282,9 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
273
282
  newElements.push(produce(wrapped, draft => {
274
283
  draft.element = produce(elem, itemDraft => {
275
284
  if (i === 0) {
276
- itemDraft.prefix = this.mergePrefix(elem.prefix, element.prefix);
285
+ itemDraft.prefix = ownLayout ?
286
+ this.mergeElementPrefix(elem.prefix, element.prefix) :
287
+ this.mergePrefix(elem.prefix, element.prefix);
277
288
  }
278
289
  // For i > 0, prefix is already correct, no changes needed
279
290
  });
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import {Cursor, ExecutionContext, Recipe, TreeVisitor} from '../..';
17
17
  import {J, Statement} from '../../java';
18
- import {PostMatchContext, PreMatchContext, RewriteConfig, RewriteRule} from './types';
18
+ import {PostMatchContext, PreMatchContext, RewriteConfig, RewriteRule, TryOnOptions} from './types';
19
19
  import {MatchResult, Pattern} from './pattern';
20
20
  import {Template} from './template';
21
21
  import {JavaScriptVisitor} from '../visitor';
@@ -28,11 +28,12 @@ class RewriteRuleImpl implements RewriteRule {
28
28
  private readonly before: Pattern[],
29
29
  private readonly after: Template | ((match: MatchResult) => Template),
30
30
  private readonly preMatch?: (node: J, context: PreMatchContext) => boolean | Promise<boolean>,
31
- private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>
31
+ private readonly postMatch?: (node: J, context: PostMatchContext) => boolean | Promise<boolean>,
32
+ private readonly format?: boolean
32
33
  ) {
33
34
  }
34
35
 
35
- async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
36
+ async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
36
37
  // Evaluate preMatch before attempting any pattern matching
37
38
  if (this.preMatch) {
38
39
  const preMatchResult = await this.preMatch(node, { cursor });
@@ -56,14 +57,10 @@ class RewriteRuleImpl implements RewriteRule {
56
57
  // Apply transformation
57
58
  let result: J | undefined;
58
59
 
59
- if (typeof this.after === 'function') {
60
- // Call the function to get a template, then apply it
61
- const template = this.after(match);
62
- result = await template.apply(node, cursor, { values: match });
63
- } else {
64
- // Use template.apply() as before
65
- result = await this.after.apply(node, cursor, { values: match });
66
- }
60
+ const template = typeof this.after === 'function' ? this.after(match) : this.after;
61
+ const bindings = options?.bindings ?? (options?.visitor && template.resolveBindings(options.visitor));
62
+ result = await template.apply(node, cursor,
63
+ { values: match, format: this.format, bindings: bindings || undefined });
67
64
 
68
65
  if (result) {
69
66
  return result;
@@ -84,10 +81,10 @@ class RewriteRuleImpl implements RewriteRule {
84
81
  super([], () => undefined as unknown as Template);
85
82
  }
86
83
 
87
- async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
88
- const firstResult = await first.tryOn(cursor, node);
84
+ async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
85
+ const firstResult = await first.tryOn(cursor, node, options);
89
86
  if (firstResult !== undefined) {
90
- const secondResult = await next.tryOn(cursor, firstResult);
87
+ const secondResult = await next.tryOn(cursor, firstResult, options);
91
88
  return secondResult ?? firstResult;
92
89
  }
93
90
  return undefined;
@@ -104,12 +101,12 @@ class RewriteRuleImpl implements RewriteRule {
104
101
  super([], () => undefined as unknown as Template);
105
102
  }
106
103
 
107
- async tryOn(cursor: Cursor, node: J): Promise<J | undefined> {
108
- const firstResult = await first.tryOn(cursor, node);
104
+ async tryOn(cursor: Cursor, node: J, options?: TryOnOptions): Promise<J | undefined> {
105
+ const firstResult = await first.tryOn(cursor, node, options);
109
106
  if (firstResult !== undefined) {
110
107
  return firstResult;
111
108
  }
112
- return await alternative.tryOn(cursor, node);
109
+ return await alternative.tryOn(cursor, node, options);
113
110
  }
114
111
  })();
115
112
  }
@@ -171,7 +168,8 @@ export function rewrite(
171
168
  Array.isArray(config.before) ? config.before : [config.before],
172
169
  config.after,
173
170
  config.preMatch,
174
- config.postMatch
171
+ config.postMatch,
172
+ config.format
175
173
  );
176
174
  }
177
175