@openrewrite/rewrite 8.92.16 → 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 (254) 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 +120 -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/format/tabs-and-indents-visitor.d.ts.map +1 -1
  42. package/dist/javascript/format/tabs-and-indents-visitor.js +31 -4
  43. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  44. package/dist/javascript/index.d.ts +4 -2
  45. package/dist/javascript/index.d.ts.map +1 -1
  46. package/dist/javascript/index.js +10 -1
  47. package/dist/javascript/index.js.map +1 -1
  48. package/dist/javascript/markers.d.ts +12 -0
  49. package/dist/javascript/markers.d.ts.map +1 -1
  50. package/dist/javascript/markers.js +81 -2
  51. package/dist/javascript/markers.js.map +1 -1
  52. package/dist/javascript/method-matcher.d.ts +13 -2
  53. package/dist/javascript/method-matcher.d.ts.map +1 -1
  54. package/dist/javascript/method-matcher.js +32 -19
  55. package/dist/javascript/method-matcher.js.map +1 -1
  56. package/dist/javascript/node-resolution-result.js +3 -2
  57. package/dist/javascript/node-resolution-result.js.map +1 -1
  58. package/dist/javascript/package-name.d.ts +6 -0
  59. package/dist/javascript/package-name.d.ts.map +1 -0
  60. package/dist/javascript/package-name.js +47 -0
  61. package/dist/javascript/package-name.js.map +1 -0
  62. package/dist/javascript/parser.d.ts +9 -0
  63. package/dist/javascript/parser.d.ts.map +1 -1
  64. package/dist/javascript/parser.js +185 -88
  65. package/dist/javascript/parser.js.map +1 -1
  66. package/dist/javascript/preconditions.d.ts +4 -1
  67. package/dist/javascript/preconditions.d.ts.map +1 -1
  68. package/dist/javascript/preconditions.js +7 -4
  69. package/dist/javascript/preconditions.js.map +1 -1
  70. package/dist/javascript/print.d.ts +10 -1
  71. package/dist/javascript/print.d.ts.map +1 -1
  72. package/dist/javascript/print.js +120 -77
  73. package/dist/javascript/print.js.map +1 -1
  74. package/dist/javascript/recipes/change-method-target-to-import.d.ts +29 -0
  75. package/dist/javascript/recipes/change-method-target-to-import.d.ts.map +1 -0
  76. package/dist/javascript/recipes/change-method-target-to-import.js +164 -0
  77. package/dist/javascript/recipes/change-method-target-to-import.js.map +1 -0
  78. package/dist/javascript/recipes/index.d.ts +1 -0
  79. package/dist/javascript/recipes/index.d.ts.map +1 -1
  80. package/dist/javascript/recipes/index.js +1 -0
  81. package/dist/javascript/recipes/index.js.map +1 -1
  82. package/dist/javascript/scope.d.ts +60 -16
  83. package/dist/javascript/scope.d.ts.map +1 -1
  84. package/dist/javascript/scope.js +596 -131
  85. package/dist/javascript/scope.js.map +1 -1
  86. package/dist/javascript/search/uses-method.d.ts +1 -1
  87. package/dist/javascript/search/uses-method.d.ts.map +1 -1
  88. package/dist/javascript/search/uses-method.js +2 -2
  89. package/dist/javascript/search/uses-method.js.map +1 -1
  90. package/dist/javascript/search/uses-type.d.ts +14 -1
  91. package/dist/javascript/search/uses-type.d.ts.map +1 -1
  92. package/dist/javascript/search/uses-type.js +24 -9
  93. package/dist/javascript/search/uses-type.js.map +1 -1
  94. package/dist/javascript/templating/bindings.d.ts +1 -1
  95. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  96. package/dist/javascript/templating/bindings.js +10 -50
  97. package/dist/javascript/templating/bindings.js.map +1 -1
  98. package/dist/javascript/templating/comparator.d.ts +8 -11
  99. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  100. package/dist/javascript/templating/comparator.js +12 -110
  101. package/dist/javascript/templating/comparator.js.map +1 -1
  102. package/dist/javascript/templating/engine.d.ts +34 -24
  103. package/dist/javascript/templating/engine.d.ts.map +1 -1
  104. package/dist/javascript/templating/engine.js +208 -115
  105. package/dist/javascript/templating/engine.js.map +1 -1
  106. package/dist/javascript/templating/pattern.d.ts +11 -5
  107. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  108. package/dist/javascript/templating/pattern.js +86 -20
  109. package/dist/javascript/templating/pattern.js.map +1 -1
  110. package/dist/javascript/templating/placeholder-replacement.d.ts +34 -3
  111. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  112. package/dist/javascript/templating/placeholder-replacement.js +126 -60
  113. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  114. package/dist/javascript/templating/precedence.d.ts +3 -1
  115. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  116. package/dist/javascript/templating/precedence.js +28 -8
  117. package/dist/javascript/templating/precedence.js.map +1 -1
  118. package/dist/javascript/templating/template.d.ts +7 -6
  119. package/dist/javascript/templating/template.d.ts.map +1 -1
  120. package/dist/javascript/templating/template.js +31 -28
  121. package/dist/javascript/templating/template.js.map +1 -1
  122. package/dist/javascript/templating/types.d.ts +6 -3
  123. package/dist/javascript/templating/types.d.ts.map +1 -1
  124. package/dist/javascript/templating/utils.d.ts +10 -1
  125. package/dist/javascript/templating/utils.d.ts.map +1 -1
  126. package/dist/javascript/templating/utils.js +22 -3
  127. package/dist/javascript/templating/utils.js.map +1 -1
  128. package/dist/javascript/tree-debug.d.ts +2 -0
  129. package/dist/javascript/tree-debug.d.ts.map +1 -1
  130. package/dist/javascript/tree-debug.js +6 -0
  131. package/dist/javascript/tree-debug.js.map +1 -1
  132. package/dist/javascript/tsconfig.d.ts.map +1 -1
  133. package/dist/javascript/tsconfig.js +7 -17
  134. package/dist/javascript/tsconfig.js.map +1 -1
  135. package/dist/javascript/type-mapping.d.ts +35 -16
  136. package/dist/javascript/type-mapping.d.ts.map +1 -1
  137. package/dist/javascript/type-mapping.js +271 -252
  138. package/dist/javascript/type-mapping.js.map +1 -1
  139. package/dist/javascript/type-report-cli.d.ts +7 -0
  140. package/dist/javascript/type-report-cli.d.ts.map +1 -0
  141. package/dist/javascript/type-report-cli.js +129 -0
  142. package/dist/javascript/type-report-cli.js.map +1 -0
  143. package/dist/javascript/type-report.d.ts +61 -0
  144. package/dist/javascript/type-report.d.ts.map +1 -0
  145. package/dist/javascript/type-report.js +488 -0
  146. package/dist/javascript/type-report.js.map +1 -0
  147. package/dist/markers.d.ts +4 -0
  148. package/dist/markers.d.ts.map +1 -1
  149. package/dist/markers.js +8 -0
  150. package/dist/markers.js.map +1 -1
  151. package/dist/print.d.ts +6 -1
  152. package/dist/print.d.ts.map +1 -1
  153. package/dist/print.js +21 -7
  154. package/dist/print.js.map +1 -1
  155. package/dist/recipe.d.ts +5 -0
  156. package/dist/recipe.d.ts.map +1 -1
  157. package/dist/recipe.js +5 -0
  158. package/dist/recipe.js.map +1 -1
  159. package/dist/rewrite-javascript-version.txt +1 -1
  160. package/dist/rpc/java-rpc-client.d.ts +2 -0
  161. package/dist/rpc/java-rpc-client.d.ts.map +1 -1
  162. package/dist/rpc/java-rpc-client.js +1 -0
  163. package/dist/rpc/java-rpc-client.js.map +1 -1
  164. package/dist/rpc/queue.d.ts +24 -0
  165. package/dist/rpc/queue.d.ts.map +1 -1
  166. package/dist/rpc/queue.js +71 -11
  167. package/dist/rpc/queue.js.map +1 -1
  168. package/dist/rpc/request/get-object.d.ts +4 -0
  169. package/dist/rpc/request/get-object.d.ts.map +1 -1
  170. package/dist/rpc/request/get-object.js +23 -1
  171. package/dist/rpc/request/get-object.js.map +1 -1
  172. package/dist/rpc/request/parse.d.ts.map +1 -1
  173. package/dist/rpc/request/parse.js +3 -1
  174. package/dist/rpc/request/parse.js.map +1 -1
  175. package/dist/rpc/request/prepare-recipe.d.ts +3 -1
  176. package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
  177. package/dist/rpc/request/prepare-recipe.js +13 -6
  178. package/dist/rpc/request/prepare-recipe.js.map +1 -1
  179. package/dist/rpc/request/print.d.ts +8 -2
  180. package/dist/rpc/request/print.d.ts.map +1 -1
  181. package/dist/rpc/request/print.js +12 -9
  182. package/dist/rpc/request/print.js.map +1 -1
  183. package/dist/rpc/rewrite-rpc.d.ts +3 -2
  184. package/dist/rpc/rewrite-rpc.d.ts.map +1 -1
  185. package/dist/rpc/rewrite-rpc.js +9 -3
  186. package/dist/rpc/rewrite-rpc.js.map +1 -1
  187. package/dist/rpc/server.d.ts.map +1 -1
  188. package/dist/rpc/server.js +19 -5
  189. package/dist/rpc/server.js.map +1 -1
  190. package/dist/search/is-source-file.d.ts +1 -0
  191. package/dist/search/is-source-file.d.ts.map +1 -1
  192. package/dist/search/is-source-file.js +7 -2
  193. package/dist/search/is-source-file.js.map +1 -1
  194. package/dist/test/java-rpc.d.ts.map +1 -1
  195. package/dist/test/java-rpc.js +3 -0
  196. package/dist/test/java-rpc.js.map +1 -1
  197. package/dist/test/rewrite-test.d.ts +5 -0
  198. package/dist/test/rewrite-test.d.ts.map +1 -1
  199. package/dist/test/rewrite-test.js +14 -3
  200. package/dist/test/rewrite-test.js.map +1 -1
  201. package/package.json +4 -3
  202. package/src/index.ts +3 -1
  203. package/src/java/formatting-utils.ts +5 -4
  204. package/src/java/rpc.ts +2 -0
  205. package/src/java/tree.ts +15 -0
  206. package/src/java/type.ts +19 -2
  207. package/src/javascript/add-import.ts +338 -152
  208. package/src/javascript/amd.ts +28 -12
  209. package/src/javascript/autodetect.ts +32 -41
  210. package/src/javascript/binding.ts +64 -22
  211. package/src/javascript/comparator.ts +129 -43
  212. package/src/javascript/format/format.ts +9 -0
  213. package/src/javascript/format/tabs-and-indents-visitor.ts +30 -3
  214. package/src/javascript/index.ts +6 -3
  215. package/src/javascript/markers.ts +77 -5
  216. package/src/javascript/method-matcher.ts +34 -22
  217. package/src/javascript/node-resolution-result.ts +3 -2
  218. package/src/javascript/package-name.ts +45 -0
  219. package/src/javascript/parser.ts +200 -91
  220. package/src/javascript/preconditions.ts +7 -4
  221. package/src/javascript/print.ts +123 -76
  222. package/src/javascript/recipes/change-method-target-to-import.ts +178 -0
  223. package/src/javascript/recipes/index.ts +1 -0
  224. package/src/javascript/scope.ts +705 -169
  225. package/src/javascript/search/uses-method.ts +2 -2
  226. package/src/javascript/search/uses-type.ts +30 -9
  227. package/src/javascript/templating/bindings.ts +13 -38
  228. package/src/javascript/templating/comparator.ts +16 -127
  229. package/src/javascript/templating/engine.ts +236 -129
  230. package/src/javascript/templating/pattern.ts +107 -28
  231. package/src/javascript/templating/placeholder-replacement.ts +154 -62
  232. package/src/javascript/templating/precedence.ts +33 -11
  233. package/src/javascript/templating/template.ts +35 -30
  234. package/src/javascript/templating/types.ts +6 -3
  235. package/src/javascript/templating/utils.ts +24 -2
  236. package/src/javascript/tree-debug.ts +9 -0
  237. package/src/javascript/tsconfig.ts +7 -17
  238. package/src/javascript/type-mapping.ts +277 -255
  239. package/src/javascript/type-report-cli.ts +96 -0
  240. package/src/javascript/type-report.ts +575 -0
  241. package/src/markers.ts +8 -0
  242. package/src/print.ts +23 -8
  243. package/src/recipe.ts +6 -0
  244. package/src/rpc/java-rpc-client.ts +3 -0
  245. package/src/rpc/queue.ts +80 -7
  246. package/src/rpc/request/get-object.ts +24 -0
  247. package/src/rpc/request/parse.ts +4 -1
  248. package/src/rpc/request/prepare-recipe.ts +16 -6
  249. package/src/rpc/request/print.ts +12 -8
  250. package/src/rpc/rewrite-rpc.ts +16 -5
  251. package/src/rpc/server.ts +18 -6
  252. package/src/search/is-source-file.ts +8 -3
  253. package/src/test/java-rpc.ts +3 -0
  254. package/src/test/rewrite-test.ts +14 -3
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import {Cursor} from '../..';
17
- import {J} from '../../java';
17
+ import {isIdentifier, J} from '../../java';
18
18
  import {
19
19
  Any,
20
20
  Capture,
@@ -28,10 +28,20 @@ import {
28
28
  } from './types';
29
29
  import {CAPTURE_CAPTURING_SYMBOL, CAPTURE_NAME_SYMBOL, CaptureImpl, RAW_CODE_SYMBOL, RawCode} from './capture';
30
30
  import {DebugPatternMatchingComparator, MatcherCallbacks, MatcherState, PatternMatchingComparator} from './comparator';
31
- import {CaptureMarker, CaptureStorageValue, generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
32
- import {TemplateEngine} from './engine';
31
+ import {
32
+ CaptureMarker,
33
+ CaptureStorageValue,
34
+ generateCacheKey,
35
+ globalAstCache,
36
+ PATTERN_PREFIXES_SYMBOL,
37
+ PlaceholderUtils,
38
+ WRAPPERS_MAP_SYMBOL
39
+ } from './utils';
40
+ import {walk} from '../scope';
41
+ import {opensWithBrace, TemplateEngine} from './engine';
33
42
  import {TreePrinters} from '../../print';
34
43
  import {JS} from '../index';
44
+ import {JavaScriptSemanticComparatorVisitor} from '../comparator';
35
45
 
36
46
 
37
47
  /**
@@ -130,7 +140,8 @@ export class PatternBuilder {
130
140
  */
131
141
  export class Pattern {
132
142
  private _options: PatternOptions = {};
133
- private _cachedAstPattern?: J;
143
+ /** The pattern tree, keyed by whether its code was parsed as an expression. */
144
+ private _cachedAstPatterns = new Map<boolean, J>();
134
145
  private static nextPatternId = 1;
135
146
  private readonly patternId: number;
136
147
  private readonly unnamedCaptureMapping = new Map<string, string>();
@@ -202,23 +213,25 @@ export class Pattern {
202
213
  configure(options: PatternOptions): Pattern {
203
214
  this._options = { ...this._options, ...options };
204
215
  // Invalidate cache when configuration changes
205
- this._cachedAstPattern = undefined;
216
+ this._cachedAstPatterns.clear();
206
217
  return this;
207
218
  }
208
219
 
209
220
  /**
210
- * Gets the AST pattern for this pattern, using two-level caching:
221
+ * Gets the AST pattern for this pattern, using three-level caching:
211
222
  * 1. Instance-level cache (fastest - this pattern instance)
212
223
  * 2. Global LRU cache (fast - shared across pattern instances with same code)
213
224
  * 3. Compute via TemplateProcessor (slow - parse and process)
214
225
  *
226
+ * @param expression Whether to parse the code as an expression, with statements as the fallback
215
227
  * @returns The cached or newly computed pattern AST
216
228
  * @internal
217
229
  */
218
- async getAstPattern(): Promise<J> {
230
+ async getAstPattern(expression: boolean = false): Promise<J> {
219
231
  // Level 1: Instance cache (fastest path)
220
- if (this._cachedAstPattern) {
221
- return this._cachedAstPattern;
232
+ const instanceCached = this._cachedAstPatterns.get(expression);
233
+ if (instanceCached) {
234
+ return instanceCached;
222
235
  }
223
236
 
224
237
  // Generate cache key for global lookup
@@ -229,7 +242,7 @@ export class Pattern {
229
242
  return `raw:${(c as RawCode).code}`;
230
243
  }
231
244
  return c.getName();
232
- }).join(',');
245
+ }).join(',') + (expression ? '::expression' : '');
233
246
  const cacheKey = generateCacheKey(
234
247
  this.templateParts,
235
248
  capturesKey,
@@ -244,20 +257,31 @@ export class Pattern {
244
257
  let tree = globalAstCache.get(cacheKey);
245
258
  if (!tree) {
246
259
  // 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
- );
260
+ try {
261
+ tree = await TemplateEngine.getPatternTree(
262
+ this.templateParts,
263
+ this.captures,
264
+ contextStatements,
265
+ this._options.dependencies || {},
266
+ this._options.types,
267
+ expression
268
+ );
269
+ } catch (e) {
270
+ // A block matches only a block, which the statement parse is for
271
+ if (!expression) {
272
+ throw e;
273
+ }
274
+ const statementPattern = await this.getAstPattern(false);
275
+ this._cachedAstPatterns.set(expression, statementPattern);
276
+ return statementPattern;
277
+ }
254
278
  globalAstCache.set(cacheKey, tree);
255
279
  }
256
280
 
257
281
  // The key names captures but says nothing about their constraints, so two patterns of the
258
282
  // same shape share an entry; markers are attached per instance to keep them apart.
259
283
  const result = await TemplateEngine.attachCaptureMarkers(tree, this.captures);
260
- this._cachedAstPattern = result;
284
+ this._cachedAstPatterns.set(expression, result);
261
285
 
262
286
  return result;
263
287
  }
@@ -310,7 +334,7 @@ export class Pattern {
310
334
  }
311
335
  // Create MatchResult with unified storage
312
336
  const storage = (matcher as any).storage;
313
- return new MatchResult(new Map(storage));
337
+ return new MatchResult(new Map(storage), () => matcher.patternPrefixes());
314
338
  }
315
339
 
316
340
  /**
@@ -556,7 +580,7 @@ export class Pattern {
556
580
  if (success) {
557
581
  // Match succeeded - return MatchResult with debug info
558
582
  const storage = (matcher as any).storage;
559
- const matchResult = new MatchResult(new Map(storage));
583
+ const matchResult = new MatchResult(new Map(storage), () => matcher.patternPrefixes());
560
584
  return {
561
585
  matched: true,
562
586
  result: matchResult,
@@ -600,10 +624,13 @@ export class Pattern {
600
624
  */
601
625
  export class MatchResult implements IMatchResult {
602
626
  constructor(
603
- private readonly storage: Map<string, CaptureStorageValue> = new Map()
627
+ private readonly storage: Map<string, CaptureStorageValue> = new Map(),
628
+ private readonly computePatternPrefixes: () => Map<string, J.Space> = () => new Map()
604
629
  ) {
605
630
  }
606
631
 
632
+ private patternPrefixes?: Map<string, J.Space>;
633
+
607
634
  // Overload: get with Capture returns value
608
635
  get<T>(capture: Capture<T>): T | undefined;
609
636
  // Overload: get with string returns value
@@ -655,6 +682,11 @@ export class MatchResult implements IMatchResult {
655
682
  return value as J;
656
683
  }
657
684
 
685
+ /** @internal */
686
+ [PATTERN_PREFIXES_SYMBOL](): Map<string, J.Space> {
687
+ return this.patternPrefixes ??= this.computePatternPrefixes();
688
+ }
689
+
658
690
  /**
659
691
  * Internal method to get wrappers (used by template expansion).
660
692
  * Returns both scalar and variadic wrappers.
@@ -705,9 +737,13 @@ class Matcher {
705
737
  ) {
706
738
  this.cursor = cursor;
707
739
  this.debugOptions = debugOptions ?? {};
740
+ this.lenientTypeMatching = pattern.options.lenientTypeMatching ?? true;
741
+ this.sameCodeComparator = new JavaScriptSemanticComparatorVisitor(this.lenientTypeMatching);
708
742
  }
709
743
 
710
744
  private readonly cursor: Cursor;
745
+ private readonly lenientTypeMatching: boolean;
746
+ private readonly sameCodeComparator: JavaScriptSemanticComparatorVisitor;
711
747
 
712
748
  /**
713
749
  * Checks if the pattern matches the AST node.
@@ -716,12 +752,34 @@ class Matcher {
716
752
  */
717
753
  async matches(): Promise<boolean> {
718
754
  if (!this.patternAst) {
719
- this.patternAst = await this.pattern.getAstPattern();
755
+ this.patternAst = await this.pattern.getAstPattern(
756
+ opensWithBrace(this.pattern.templateParts) && this.ast.kind !== J.Kind.Block);
720
757
  }
721
758
 
722
759
  return this.matchNode(this.patternAst, this.ast);
723
760
  }
724
761
 
762
+ /**
763
+ * The prefix the pattern writes before each capture, by capture name. A capture of the whole
764
+ * match has none, since the prefix of what it holds lies outside the match.
765
+ */
766
+ patternPrefixes(): Map<string, J.Space> {
767
+ const prefixes = new Map<string, J.Space>();
768
+ walk(this.patternAst, node => {
769
+ const capture = isIdentifier(node) ? PlaceholderUtils.parseCapture(node.simpleName) : null;
770
+ if (capture && !prefixes.has(capture.name) && !this.capturesWholeMatch(capture.name)) {
771
+ prefixes.set(capture.name, (node as J.Identifier).prefix);
772
+ }
773
+ return true;
774
+ });
775
+ return prefixes;
776
+ }
777
+
778
+ private capturesWholeMatch(name: string): boolean {
779
+ const bound = this.storage.get(name);
780
+ return bound !== undefined && !Array.isArray(bound) && this.extractElements(bound) === this.ast;
781
+ }
782
+
725
783
  /**
726
784
  * Gets all captured nodes (projected view: extracts elements from wrappers).
727
785
  *
@@ -850,8 +908,6 @@ class Matcher {
850
908
  // - Kind checking
851
909
  // - Deep structural comparison
852
910
  // This centralizes all matching logic in one place
853
- const lenientTypeMatching = this.pattern.options.lenientTypeMatching ?? true;
854
-
855
911
  // Factory pattern: instantiate debug or production comparator
856
912
  // Zero cost in production - DebugPatternMatchingComparator is never instantiated
857
913
  const matcherCallbacks: MatcherCallbacks = {
@@ -872,8 +928,8 @@ class Matcher {
872
928
  };
873
929
 
874
930
  const comparator = this.debugOptions.enabled
875
- ? new DebugPatternMatchingComparator(matcherCallbacks, lenientTypeMatching)
876
- : new PatternMatchingComparator(matcherCallbacks, lenientTypeMatching);
931
+ ? new DebugPatternMatchingComparator(matcherCallbacks, this.lenientTypeMatching)
932
+ : new PatternMatchingComparator(matcherCallbacks, this.lenientTypeMatching);
877
933
  // Pass cursors to allow constraints to navigate to root
878
934
  // Pattern cursor is undefined (pattern is the root), target cursor is provided by user
879
935
  const result = await comparator.compare(pattern, target, undefined, this.cursor);
@@ -942,7 +998,7 @@ class Matcher {
942
998
  * @param wrapper Optional wrapper containing the target (for preserving markers)
943
999
  * @returns true if the capture is successful, false otherwise
944
1000
  */
945
- private handleCapture(capture: CaptureMarker, target: J, wrapper?: J.RightPadded<J>): boolean {
1001
+ private async handleCapture(capture: CaptureMarker, target: J, wrapper?: J.RightPadded<J>): Promise<boolean> {
946
1002
  const captureName = capture.captureName;
947
1003
 
948
1004
  if (!captureName) {
@@ -960,6 +1016,10 @@ class Matcher {
960
1016
  // Only store the binding if this is a capturing placeholder
961
1017
  const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
962
1018
  if (capturing) {
1019
+ const bound = this.storage.get(captureName);
1020
+ if (bound !== undefined) {
1021
+ return this.bindsSameCode(bound, [target]);
1022
+ }
963
1023
  // Store wrapper if available (preserves markers), otherwise store element
964
1024
  this.storage.set(captureName, wrapper ?? target);
965
1025
  }
@@ -967,6 +1027,21 @@ class Matcher {
967
1027
  return true;
968
1028
  }
969
1029
 
1030
+ /** A capture named twice in a pattern binds the same code at both places. */
1031
+ private async bindsSameCode(bound: CaptureStorageValue, targets: J[]): Promise<boolean> {
1032
+ const boundElements = (Array.isArray(bound) ? bound : [bound]).map(b =>
1033
+ b.kind === J.Kind.RightPadded ? (b as J.RightPadded<J>).element : b as J);
1034
+ if (boundElements.length !== targets.length) {
1035
+ return false;
1036
+ }
1037
+ for (let i = 0; i < targets.length; i++) {
1038
+ if (!await this.sameCodeComparator.compare(boundElements[i], targets[i])) {
1039
+ return false;
1040
+ }
1041
+ }
1042
+ return true;
1043
+ }
1044
+
970
1045
  /**
971
1046
  * Handles a variadic capture placeholder.
972
1047
  *
@@ -975,7 +1050,7 @@ class Matcher {
975
1050
  * @param wrappers Optional wrappers to preserve markers
976
1051
  * @returns true if the capture is successful, false otherwise
977
1052
  */
978
- private handleVariadicCapture(capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded<J>[]): boolean {
1053
+ private async handleVariadicCapture(capture: CaptureMarker, targets: J[], wrappers?: J.RightPadded<J>[]): Promise<boolean> {
979
1054
  const captureName = capture.captureName;
980
1055
 
981
1056
  if (!captureName) {
@@ -993,6 +1068,10 @@ class Matcher {
993
1068
  // Only store the binding if this is a capturing placeholder
994
1069
  const capturing = (captureObj as any)?.[CAPTURE_CAPTURING_SYMBOL] ?? true;
995
1070
  if (capturing) {
1071
+ const bound = this.storage.get(captureName);
1072
+ if (bound !== undefined) {
1073
+ return this.bindsSameCode(bound, targets);
1074
+ }
996
1075
  // Store the richest representation: wrappers if available, otherwise elements
997
1076
  if (wrappers && wrappers.length > 0) {
998
1077
  this.storage.set(captureName, wrappers);
@@ -13,8 +13,8 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {Cursor, isTree, Markers} from '../..';
17
- import {J} from '../../java';
16
+ import {Cursor, isTree, Marker, Markers} from '../..';
17
+ import {Expression, J} from '../../java';
18
18
  import {JS} from '..';
19
19
  import {JavaScriptVisitor} from '../visitor';
20
20
  import {create as produce} from 'mutative';
@@ -22,15 +22,37 @@ import {PlaceholderUtils} from './utils';
22
22
  import {CaptureImpl, TemplateParamImpl, CaptureValue, CAPTURE_NAME_SYMBOL} from './capture';
23
23
  import {enclosingTree, maybeParenthesize} from './precedence';
24
24
  import {Parameter} from './types';
25
+ import {isExpression} from '../parser-utils';
26
+ import {randomId} from '../../uuid';
27
+
28
+ /**
29
+ * Marks a value the template substituted, whose layout is the source's rather than the template's.
30
+ * `ownPrefix` says whether the whitespace before it is the source's too.
31
+ */
32
+ export class SubstitutedValue implements Marker {
33
+ static readonly KIND = 'org.openrewrite.javascript.templating.SubstitutedValue';
34
+ readonly kind = SubstitutedValue.KIND;
35
+ readonly id = randomId();
36
+
37
+ constructor(readonly ownPrefix: boolean) {
38
+ }
39
+ }
40
+
41
+ /** Chooses a substituted value's prefix, and says whether it is the source's own. */
42
+ type MergePrefix = (sourcePrefix: J.Space, templatePrefix: J.Space) => [J.Space, boolean];
25
43
 
26
44
  /**
27
45
  * Visitor that replaces placeholder nodes with actual parameter values.
28
46
  */
29
47
  export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
48
+ /** Whether any value was substituted, and so marked as a {@link SubstitutedValue}. */
49
+ substituted = false;
50
+
30
51
  constructor(
31
52
  private readonly substitutions: Map<string, Parameter>,
32
53
  private readonly values: Pick<Map<string, J | J[]>, 'get'> = new Map(),
33
- private readonly wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map()
54
+ private readonly wrappersMap: Pick<Map<string, J.RightPadded<J> | J.RightPadded<J>[]>, 'get'> = new Map(),
55
+ private readonly patternPrefixes: Pick<Map<string, J.Space>, 'get'> = new Map()
34
56
  ) {
35
57
  super();
36
58
  }
@@ -70,6 +92,71 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
70
92
  return bindingElement;
71
93
  }
72
94
 
95
+ /** A statement substituted into an expression statement's placeholder is the statement itself. */
96
+ override async visitExpressionStatement(expressionStatement: JS.ExpressionStatement, p: any): Promise<J | undefined> {
97
+ const visited = await super.visitExpressionStatement(expressionStatement, p);
98
+ if (visited?.kind !== JS.Kind.ExpressionStatement) {
99
+ return visited;
100
+ }
101
+ const statement = visited as JS.ExpressionStatement;
102
+ const expression: J = statement.expression;
103
+ if (isExpression(expression)) {
104
+ return statement;
105
+ }
106
+ return {...expression, prefix: this.concatPrefix(statement.prefix, expression.prefix)};
107
+ }
108
+
109
+ /**
110
+ * A declaration substituted for the name of a declaration is the declaration itself: `(${p})` and
111
+ * `(...${p})` with `p` bound to the parameter `props: Props` give `(props: Props)` and `(...props: Props)`.
112
+ */
113
+ override async visitVariableDeclarations(variableDeclarations: J.VariableDeclarations, p: any): Promise<J | undefined> {
114
+ const visited = await super.visitVariableDeclarations(variableDeclarations, p);
115
+ if (visited?.kind !== J.Kind.VariableDeclarations) {
116
+ return visited;
117
+ }
118
+ const outer = visited as J.VariableDeclarations;
119
+ if (outer.variables.length !== 1) {
120
+ return outer;
121
+ }
122
+ const variable = outer.variables[0].element;
123
+ const spread = variable.name.kind === JS.Kind.Spread ? variable.name as JS.Spread : undefined;
124
+ const declared: J = spread ? spread.expression : variable.name;
125
+ if (declared.kind !== J.Kind.VariableDeclarations) {
126
+ return outer;
127
+ }
128
+ const inner = declared as J.VariableDeclarations;
129
+ const prefix = this.concatPrefix(this.concatPrefix(outer.prefix, variable.prefix), inner.prefix);
130
+ if (!spread) {
131
+ return {...inner, prefix};
132
+ } else if (inner.variables.length !== 1) {
133
+ return outer;
134
+ }
135
+ const innerVariable = inner.variables[0];
136
+ const spreadDeclaration: J.VariableDeclarations = {
137
+ ...inner,
138
+ prefix,
139
+ variables: [{
140
+ ...innerVariable,
141
+ element: {...innerVariable.element, name: {...spread, expression: innerVariable.element.name as Expression} as JS.Spread}
142
+ }]
143
+ };
144
+ return spreadDeclaration;
145
+ }
146
+
147
+ private concatPrefix(outer: J.Space, inner: J.Space): J.Space {
148
+ if (outer.whitespace === '' && outer.comments.length === 0) {
149
+ return inner;
150
+ } else if (outer.comments.length === 0) {
151
+ return {...inner, whitespace: outer.whitespace + inner.whitespace};
152
+ }
153
+ const last = outer.comments[outer.comments.length - 1];
154
+ return {
155
+ ...outer,
156
+ comments: [...outer.comments.slice(0, -1), {...last, suffix: last.suffix + inner.whitespace}, ...inner.comments]
157
+ };
158
+ }
159
+
73
160
  /**
74
161
  * Override visitContainer to handle variadic expansion for containers.
75
162
  * This handles J.Container instances anywhere in the AST (method arguments, etc.).
@@ -105,28 +192,18 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
105
192
  * array-level access for variadic expansion.
106
193
  */
107
194
  override async visitBlock(block: J.Block, p: any): Promise<J | undefined> {
108
- const hasPlaceholder = block.statements.some(stmt => {
109
- const stmtElement = stmt.element;
110
- // Check if it's an ExpressionStatement containing a placeholder
111
- if (stmtElement.kind === JS.Kind.ExpressionStatement) {
112
- const exprStmt = stmtElement as JS.ExpressionStatement;
113
- return this.isPlaceholder(exprStmt.expression);
114
- }
115
- return this.isPlaceholder(stmtElement);
116
- });
117
-
118
- if (!hasPlaceholder) {
119
- return super.visitBlock(block, p);
120
- }
121
-
122
- // Unwrap function to extract placeholder from ExpressionStatement
195
+ // An object literal's body is a block too, whose placeholders are shorthand properties
123
196
  const unwrapStatement = (element: J): J => {
124
197
  if (element.kind === JS.Kind.ExpressionStatement) {
125
198
  return (element as JS.ExpressionStatement).expression;
126
199
  }
127
- return element;
200
+ return PlaceholderUtils.shorthandPropertyName(element) ?? element;
128
201
  };
129
202
 
203
+ if (!block.statements.some(stmt => this.isPlaceholder(unwrapStatement(stmt.element)))) {
204
+ return super.visitBlock(block, p);
205
+ }
206
+
130
207
  const newStatements = await this.expandVariadicElements(block.statements, unwrapStatement, p);
131
208
 
132
209
  return produce(block, draft => {
@@ -175,10 +252,31 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
175
252
  };
176
253
  }
177
254
 
255
+ private readonly templatePrefix: MergePrefix = (source, template) => [this.mergePrefix(source, template), false];
256
+
178
257
  /** 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);
258
+ private readonly elementPrefix: MergePrefix = (source, template) =>
259
+ source.whitespace.includes('\n') && !template.whitespace.includes('\n') ?
260
+ [source, true] : this.templatePrefix(source, template);
261
+
262
+ private readonly sourcePrefix: MergePrefix = source => [source, true];
263
+
264
+ /** Whether the template writes the same prefix before capture `name` as the pattern it matched with. */
265
+ private spacedAsInPattern(name: string, placeholder: J): boolean {
266
+ const pattern = this.patternPrefixes.get(name);
267
+ return pattern !== undefined && pattern.comments.length === 0 && placeholder.prefix.comments.length === 0 &&
268
+ pattern.whitespace === placeholder.prefix.whitespace;
269
+ }
270
+
271
+ /** `value` substituted for `placeholder` under the prefix `merge` chooses, marked as a {@link SubstitutedValue}. */
272
+ private substitute(value: J, placeholder: J, merge: MergePrefix): J {
273
+ this.substituted = true;
274
+ const [prefix, ownPrefix] = merge(value.prefix, placeholder.prefix);
275
+ const markers = this.mergeMarkers(value.markers, placeholder.markers);
276
+ return produce(value, draft => {
277
+ draft.prefix = prefix;
278
+ draft.markers = {...markers, markers: [...markers.markers, new SubstitutedValue(ownPrefix)]};
279
+ });
182
280
  }
183
281
 
184
282
  /** As `mergePrefix`, for markers: everything the value was matched with, plus the kinds only the slot wrote. */
@@ -206,6 +304,9 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
206
304
  ownLayout: boolean = false
207
305
  ): Promise<J.RightPadded<J>[]> {
208
306
  const newElements: J.RightPadded<J>[] = [];
307
+ const merge = ownLayout ? this.elementPrefix : this.templatePrefix;
308
+ // Past the first item, an item's prefix is the source's separator from the one before it
309
+ const keep: MergePrefix = source => [source, ownLayout];
209
310
 
210
311
  for (const wrapped of elements) {
211
312
  const element = wrapped.element;
@@ -276,29 +377,18 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
276
377
 
277
378
  if (isWrapper) {
278
379
  // Item is a JRightPadded wrapper - use it directly to preserve markers
279
- newElements.push(produce(item, draft => {
280
- if (i === 0 && draft.element) {
281
- // Merge the placeholder's prefix with the first item's prefix
282
- // Modify prefix directly within the draft
283
- draft.element.prefix = ownLayout ?
284
- this.mergeElementPrefix(draft.element.prefix, element.prefix) :
285
- this.mergePrefix(draft.element.prefix, element.prefix);
286
- }
287
- // Keep all other wrapper properties (including markers with Semicolon)
288
- }));
380
+ const wrapper = item as J.RightPadded<J>;
381
+ // Keep all other wrapper properties (including markers with Semicolon)
382
+ newElements.push({
383
+ ...wrapper,
384
+ element: this.substitute(wrapper.element, element, i > 0 ? keep : merge)
385
+ });
289
386
  } else if (item) {
290
387
  // Item is just an element (not a wrapper) - wrap it (backward compatibility)
291
- const elem = item as J;
292
- newElements.push(produce(wrapped, draft => {
293
- draft.element = produce(elem, itemDraft => {
294
- if (i === 0) {
295
- itemDraft.prefix = ownLayout ?
296
- this.mergeElementPrefix(elem.prefix, element.prefix) :
297
- this.mergePrefix(elem.prefix, element.prefix);
298
- }
299
- // For i > 0, prefix is already correct, no changes needed
300
- });
301
- }));
388
+ newElements.push({
389
+ ...wrapped,
390
+ element: this.substitute(item as J, element, i > 0 ? keep : merge)
391
+ });
302
392
  }
303
393
  }
304
394
  continue; // Skip adding the placeholder itself
@@ -312,14 +402,18 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
312
402
  }
313
403
 
314
404
  // Not a placeholder (or expansion failed) - process normally
315
- const replacedElement = await this.visit(element, p);
405
+ const slot = ownLayout && element.kind !== JS.Kind.BindingElement && this.isPlaceholder(element);
406
+ const replacedElement = slot ?
407
+ this.replaceElement(element, merge) :
408
+ await this.visit(element, p);
316
409
  if (replacedElement) {
317
410
  // Check if the replacement came from a capture with a wrapper (to preserve markers)
318
411
  const placeholderNode = unwrapElement(element);
319
412
  const placeholderText = this.getPlaceholderText(placeholderNode);
320
413
  let wrapperToUse = wrapped;
321
414
 
322
- if (placeholderText && this.isPlaceholder(placeholderNode)) {
415
+ // A shorthand property keeps its own wrapper, since a captured one belongs to the name alone
416
+ if (placeholderText && this.isPlaceholder(placeholderNode) && element.kind !== JS.Kind.PropertyAssignment) {
323
417
  const param = this.substitutions.get(placeholderText);
324
418
  if (param) {
325
419
  const isCapture = param.value instanceof CaptureImpl ||
@@ -344,6 +438,13 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
344
438
  return newElements;
345
439
  }
346
440
 
441
+ /** As `visit` on a placeholder, with `merge` choosing the replacement's prefix. */
442
+ private replaceElement(placeholder: J, merge: MergePrefix): J {
443
+ const replacement = this.replacePlaceholder(placeholder, merge);
444
+ return replacement === placeholder ? placeholder :
445
+ maybeParenthesize(enclosingTree(this.cursor), placeholder.id, replacement, placeholder.markers);
446
+ }
447
+
347
448
  /**
348
449
  * Checks if a node is a placeholder.
349
450
  *
@@ -369,9 +470,10 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
369
470
  * Replaces a placeholder node with the actual parameter value.
370
471
  *
371
472
  * @param placeholder The placeholder node
473
+ * @param merge Chooses the replacement's prefix from its own and the placeholder's
372
474
  * @returns The replacement node or the original if not a placeholder
373
475
  */
374
- private replacePlaceholder(placeholder: J): J {
476
+ private replacePlaceholder(placeholder: J, merge: MergePrefix = this.templatePrefix): J {
375
477
  const placeholderText = this.getPlaceholderText(placeholder);
376
478
 
377
479
  if (!placeholderText || !placeholderText.startsWith(PlaceholderUtils.PLACEHOLDER_PREFIX)) {
@@ -395,10 +497,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
395
497
  // If the property value is already a J node, use it
396
498
  if (isTree(propertyValue)) {
397
499
  const propValueAsJ = propertyValue as J;
398
- return produce(propValueAsJ, draft => {
399
- draft.markers = this.mergeMarkers(propValueAsJ.markers, placeholder.markers);
400
- draft.prefix = this.mergePrefix(propValueAsJ.prefix, placeholder.prefix);
401
- });
500
+ return this.substitute(propValueAsJ, placeholder, merge);
402
501
  }
403
502
  // If it's a primitive value and placeholder is an identifier, update the simpleName
404
503
  if (typeof propertyValue === 'string' && placeholder.kind === J.Kind.Identifier) {
@@ -430,10 +529,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
430
529
  (param.value[CAPTURE_NAME_SYMBOL] || param.value.name);
431
530
  const matchedNode = this.values.get(name);
432
531
  if (matchedNode && !Array.isArray(matchedNode)) {
433
- return produce(matchedNode, draft => {
434
- draft.markers = this.mergeMarkers(matchedNode.markers, placeholder.markers);
435
- draft.prefix = this.mergePrefix(matchedNode.prefix, placeholder.prefix);
436
- });
532
+ return this.substitute(matchedNode, placeholder,
533
+ this.spacedAsInPattern(name, placeholder) ? this.sourcePrefix : merge);
437
534
  }
438
535
 
439
536
  // If no match found, return placeholder unchanged
@@ -447,10 +544,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
447
544
  if (isRightPadded) {
448
545
  // Extract the element from the J.RightPadded wrapper
449
546
  const element = param.value.element as J;
450
- return produce(element, draft => {
451
- draft.markers = this.mergeMarkers(element.markers, placeholder.markers);
452
- draft.prefix = this.mergePrefix(element.prefix, placeholder.prefix);
453
- });
547
+ return this.substitute(element, placeholder, merge);
454
548
  }
455
549
 
456
550
  // Check if the parameter value is a J.Container
@@ -467,10 +561,8 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
467
561
  // If the parameter value is an AST node, use it directly
468
562
  if (isTree(param.value)) {
469
563
  // Return the AST node, preserving comments from the source
470
- return produce(param.value as J, draft => {
471
- draft.markers = this.mergeMarkers(param.value.markers, placeholder.markers);
472
- draft.prefix = this.mergePrefix(param.value.prefix, placeholder.prefix);
473
- });
564
+ const value = param.value as J;
565
+ return this.substitute(value, placeholder, merge);
474
566
  }
475
567
 
476
568
  return placeholder;