@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
@@ -24,7 +24,6 @@ import {
24
24
  J,
25
25
  rightPadded,
26
26
  space,
27
- spaceContainsNewline,
28
27
  Statement,
29
28
  TrailingComma
30
29
  } from "../java";
@@ -193,16 +192,20 @@ export function parameterNames(block: AmdBlock): (string | undefined)[] {
193
192
  }
194
193
 
195
194
  export function identifierOf(parameter: J): J.Identifier | undefined {
196
- if (parameter.kind === J.Kind.VariableDeclarations) {
197
- const name = (parameter as J.VariableDeclarations).variables[0]?.element.name;
198
- return name !== undefined && isIdentifier(name) ? name : undefined;
199
- }
200
- return isIdentifier(parameter) ? parameter : undefined;
195
+ const name = declaredNameOf(parameter);
196
+ return name !== undefined && isIdentifier(name) ? name : undefined;
197
+ }
198
+
199
+ /** The name a factory parameter binds, which may be a destructuring pattern. */
200
+ function declaredNameOf(parameter: J): J | undefined {
201
+ return parameter.kind === J.Kind.VariableDeclarations ?
202
+ (parameter as J.VariableDeclarations).variables[0]?.element.name :
203
+ parameter;
201
204
  }
202
205
 
203
206
  /**
204
207
  * Where an entry's leading whitespace sits. A dependency string carries it directly, while
205
- * a factory parameter carries it on the identifier inside the declaration that wraps it.
208
+ * a factory parameter carries it on the name its declaration wraps, even a destructuring one.
206
209
  */
207
210
  interface Slot<T extends J> {
208
211
  prefixOf(element: T): J.Space;
@@ -216,7 +219,7 @@ const dependencySlot: Slot<Expression> = {
216
219
  };
217
220
 
218
221
  const parameterSlot: Slot<J> = {
219
- prefixOf: element => identifierOf(element)?.prefix ?? element.prefix,
222
+ prefixOf: element => declaredNameOf(element)?.prefix ?? element.prefix,
220
223
  withPrefix: (element, prefix) => {
221
224
  if (element.kind !== J.Kind.VariableDeclarations) {
222
225
  return {...element, prefix};
@@ -257,14 +260,27 @@ function moveTrailingComma<T extends J>(
257
260
 
258
261
  /**
259
262
  * The whitespace that separates one entry from the next, taken from the entries already
260
- * there so that a block listing its dependencies one per line keeps doing so.
263
+ * there so that a block listing its dependencies one per line keeps doing so. Comments in an
264
+ * entry's prefix document that entry, so they stay behind.
261
265
  */
262
266
  function separator<T extends J>(entries: readonly J.RightPadded<T>[], slot: Slot<T>): J.Space {
263
267
  if (entries.length >= 2) {
264
- return slot.prefixOf(entries[1].element);
268
+ return space(layoutOf(slot.prefixOf(entries[1].element)));
269
+ }
270
+ const first = layoutOf(slot.prefixOf(entries[0].element));
271
+ return space(first.includes("\n") ? first : " ");
272
+ }
273
+
274
+ /**
275
+ * The prefix without its comments. That is the segment holding the line break, preferring the
276
+ * last comment's suffix, else the whitespace before the entry.
277
+ */
278
+ function layoutOf(prefix: J.Space): string {
279
+ const last = prefix.comments[prefix.comments.length - 1];
280
+ if (last === undefined || (!last.suffix.includes("\n") && prefix.whitespace.includes("\n"))) {
281
+ return prefix.whitespace;
265
282
  }
266
- const first = slot.prefixOf(entries[0].element);
267
- return spaceContainsNewline(first) ? first : space(" ");
283
+ return last.suffix;
268
284
  }
269
285
 
270
286
  /**
@@ -123,13 +123,13 @@ class TabsAndIndentsStatistics {
123
123
  private totalSpaceIndents = 0;
124
124
  private totalTabIndents = 0;
125
125
 
126
- // Track all observed indent sizes to compute GCD
127
- private observedIndents: number[] = [];
126
+ // How often a block was seen indenting its statements by each width past the enclosing block's
127
+ private readonly levelWidths = new Map<number, number>();
128
128
 
129
- recordSpaceIndent(spaceCount: number): void {
129
+ recordSpaceIndent(width: number): void {
130
130
  this.totalSpaceIndents++;
131
- if (spaceCount > 0) {
132
- this.observedIndents.push(spaceCount);
131
+ if (width > 0) {
132
+ this.levelWidths.set(width, (this.levelWidths.get(width) ?? 0) + 1);
133
133
  }
134
134
  }
135
135
 
@@ -141,23 +141,14 @@ class TabsAndIndentsStatistics {
141
141
  // Determine if using tabs or spaces
142
142
  const useTabs = this.totalTabIndents > this.totalSpaceIndents;
143
143
 
144
- // Find indent size by computing GCD of all observed indents
145
- // This correctly handles 2-space files where we see 2, 4, 6, 8... (all multiples of 2)
144
+ // The width most statements agree on, so a stray misindented line is outvoted
146
145
  let detectedIndentSize = 4; // Default
147
- if (this.observedIndents.length > 0) {
148
- // Compute GCD of all observed indents
149
- let gcd = this.observedIndents[0];
150
- for (let i = 1; i < this.observedIndents.length; i++) {
151
- gcd = this.computeGcd(gcd, this.observedIndents[i]);
152
- if (gcd === 1) break; // Can't get smaller than 1
153
- }
154
- // Only use common indent sizes (2, 4, 8)
155
- if (gcd === 2 || gcd === 4 || gcd === 8) {
156
- detectedIndentSize = gcd;
157
- } else if (gcd > 0 && gcd % 4 === 0) {
158
- detectedIndentSize = 4;
159
- } else if (gcd > 0 && gcd % 2 === 0) {
160
- detectedIndentSize = 2;
146
+ let votes = 0;
147
+ for (const size of [2, 4, 8]) {
148
+ const count = this.levelWidths.get(size) ?? 0;
149
+ if (count > votes) {
150
+ detectedIndentSize = size;
151
+ votes = count;
161
152
  }
162
153
  }
163
154
 
@@ -172,15 +163,6 @@ class TabsAndIndentsStatistics {
172
163
  indentAllChainedCallsInAGroup: false
173
164
  };
174
165
  }
175
-
176
- private computeGcd(a: number, b: number): number {
177
- while (b !== 0) {
178
- const temp = b;
179
- b = a % b;
180
- a = temp;
181
- }
182
- return a;
183
- }
184
166
  }
185
167
 
186
168
  /**
@@ -272,23 +254,32 @@ class FindIndentVisitor extends JavaScriptVisitor<any> {
272
254
  super();
273
255
  }
274
256
 
257
+ /** The indent of the statements in each enclosing block, innermost last. */
258
+ private readonly enclosing: number[] = [];
259
+
275
260
  protected async visitBlock(block: J.Block, p: any): Promise<J | undefined> {
276
- // Check indentation of statements in the block
261
+ const outer = this.enclosing[this.enclosing.length - 1] ?? 0;
262
+ let own: number | undefined;
277
263
  for (const stmt of block.statements) {
278
264
  const whitespace = stmt.element.prefix?.whitespace;
279
- if (whitespace) {
280
- this.analyzeIndent(whitespace);
281
- }
265
+ const indent = whitespace ? this.analyzeIndent(whitespace, outer) : undefined;
266
+ own ??= indent;
267
+ }
268
+ this.enclosing.push(own ?? outer);
269
+ try {
270
+ return await super.visitBlock(block, p);
271
+ } finally {
272
+ this.enclosing.pop();
282
273
  }
283
- return super.visitBlock(block, p);
284
274
  }
285
275
 
286
- private analyzeIndent(whitespace: string): void {
276
+ /** Records the indent `whitespace` ends in, relative to `outer`, and returns it if it is spaces. */
277
+ private analyzeIndent(whitespace: string, outer: number): number | undefined {
287
278
  const newlineIndex = whitespace.lastIndexOf('\n');
288
- if (newlineIndex < 0) return;
279
+ if (newlineIndex < 0) return undefined;
289
280
 
290
281
  const indent = whitespace.substring(newlineIndex + 1);
291
- if (indent.length === 0) return;
282
+ if (indent.length === 0) return 0;
292
283
 
293
284
  // Check first character to determine type
294
285
  if (indent[0] === '\t') {
@@ -300,10 +291,10 @@ class FindIndentVisitor extends JavaScriptVisitor<any> {
300
291
  if (char === ' ') spaceCount++;
301
292
  else break;
302
293
  }
303
- if (spaceCount > 0) {
304
- this.stats.recordSpaceIndent(spaceCount);
305
- }
294
+ this.stats.recordSpaceIndent(spaceCount - outer);
295
+ return spaceCount;
306
296
  }
297
+ return undefined;
307
298
  }
308
299
  }
309
300
 
@@ -16,10 +16,11 @@
16
16
  import {J} from "../java";
17
17
  import {JS} from "./tree";
18
18
  import {JavaScriptVisitor} from "./visitor";
19
- import {compilationUnitOf, cursorOf, declarationsOf, namesUsedIn, scopeOf} from "./scope";
19
+ import {compilationUnitOf, cursorOf, declarationsOf, isReference, namesUsedIn, scopeOf} from "./scope";
20
+ import {Cursor, isTree} from "../tree";
20
21
  import {
21
- AddImportOptions, bindImport, existingImportBinding, ExistingImportBinding, hasEsmSyntax, isCommonJs, memberName,
22
- moduleNameOf, nameTaken, RebindImport, requiredModuleOfDeclaration
22
+ AddImportOptions, bindImport, bindingShape, existingImportBinding, ExistingImportBinding, hasEsmSyntax, isCommonJs,
23
+ memberName, moduleNameOf, nameTaken, RebindImport, requiredModuleOfDeclaration
23
24
  } from "./add-import";
24
25
  import {RemoveImport} from "./remove-import";
25
26
  import {
@@ -229,7 +230,7 @@ function moduleObjectBindings(cu: JS.CompilationUnit): ModuleObjectBinding[] {
229
230
  * never a type-only import for a value, which erases and would leave the reference unbound.
230
231
  */
231
232
  function answersWholeModuleRequest(binding: ModuleObjectBinding, wantsNamespace: boolean, typeOnly: boolean): boolean {
232
- return binding.typeOnly === typeOnly &&
233
+ return (typeOnly || !binding.typeOnly) &&
233
234
  (binding.shape === "require" || binding.shape === (wantsNamespace ? "namespace" : "default"));
234
235
  }
235
236
 
@@ -274,9 +275,10 @@ export function maybeBind(
274
275
  }
275
276
  }
276
277
 
277
- if (isWholeModule && options.preferredName === undefined && derivedBindingName(module) === undefined) {
278
- // The module's last path segment is not a legal identifier, and the caller named no
279
- // preference of its own — there is no name left to bind it to.
278
+ if (isWholeModule && options.alias === undefined && options.preferredName === undefined &&
279
+ derivedBindingName(module) === undefined) {
280
+ // The module's last path segment is not a legal identifier, and the caller named none
281
+ // of its own — there is no name left to bind it to.
280
282
  return undefined;
281
283
  }
282
284
 
@@ -318,17 +320,13 @@ function sameCallees(a: readonly string[], b: readonly string[]): boolean {
318
320
  return a.length === b.length && a.every((callee, i) => callee === b[i]);
319
321
  }
320
322
 
321
- /** Which of an import clause's three slots `member` binds — `import`, `import *`, or `import {}`. */
322
- function bindingShape(member: string | undefined): "default" | "namespace" | "named" {
323
- const key = memberName(member);
324
- return key === undefined ? "default" : key === "*" ? "namespace" : "named";
325
- }
326
-
327
323
  /**
328
- * Moves the binding for `from` to `to` and answers with the name it now carries — the primitive
329
- * behind a member rename or a module move. Returns `undefined`, changing nothing, where the move
330
- * is not safely expressible; the refusals and the choice of name are listed in CLAUDE.md:
331
- * JavaScript module bindings.
324
+ * Moves the binding for `from` to `to` and answers with the name it now carries, or `undefined`,
325
+ * changing nothing, where the move cannot be expressed safely. One call moves one binding.
326
+ * Named members of one import moved to one module, a call each, end up in one import of it,
327
+ * whether the calls share a visit or come from separate recipes.
328
+ * On an ES import, every type in the file naming what moved is renamed the way a parse of the result names it.
329
+ * It is renamed, not re-resolved, so a moved member keeps its old declaration's signature.
332
330
  */
333
331
  export function maybeRebind(visitor: JavaScriptVisitor<any>, options: MaybeRebindOptions): string | undefined {
334
332
  const amd = enclosingAmdBlock(visitor, options);
@@ -361,10 +359,9 @@ export function maybeRebind(visitor: JavaScriptVisitor<any>, options: MaybeRebin
361
359
  if (existing === undefined) {
362
360
  return undefined;
363
361
  }
364
- if (existing.onlyMemberOfStatement && bindingShape(options.from.member) !== bindingShape(options.to.member)) {
365
- return undefined;
366
- }
367
- if (!existing.onlyMemberOfStatement && isCommonJs(cu)) {
362
+ // `RebindImport` replaces a statement it cannot rewrite in place, and a CommonJS file can gain no import.
363
+ if (isCommonJs(cu) &&
364
+ (!existing.onlyMemberOfStatement || bindingShape(options.from.member) !== bindingShape(options.to.member))) {
368
365
  return undefined;
369
366
  }
370
367
  const boundName = rebindingName(visitor, cu, options, existing);
@@ -390,7 +387,8 @@ function rebindingName(
390
387
  options: MaybeRebindOptions,
391
388
  existing: ExistingImportBinding
392
389
  ): string | undefined {
393
- const taken = (name: string) => nameTaken(name, namesUsedIn(cu), visitor);
390
+ const taken = (name: string) => nameTaken(name, noNames, visitor) ||
391
+ (namesUsedIn(cu).has(name) && !reusesTargetImport(cu, name, options.to, existing));
394
392
  const alias = options.to.alias;
395
393
  if (alias !== undefined) {
396
394
  return isBindableName(alias) && (alias === existing.localName || !taken(alias)) ? alias : undefined;
@@ -402,6 +400,50 @@ function rebindingName(
402
400
  return taken(member) ? existing.localName : member;
403
401
  }
404
402
 
403
+ const noNames: ReadonlySet<string> = new Set();
404
+
405
+ /** Whether the moved binding can merge into a value import of `to`'s named member that `name` binds. */
406
+ function reusesTargetImport(
407
+ cu: JS.CompilationUnit,
408
+ name: string,
409
+ to: MaybeRebindOptions["to"],
410
+ moved: ExistingImportBinding
411
+ ): boolean {
412
+ const target = existingImportBinding(cu, to.module, to.member);
413
+ return target?.localName === name && bindingShape(to.member) === "named" && !target.typeOnly &&
414
+ !moved.typeOnly && onlyReferences(cu, name);
415
+ }
416
+
417
+ /**
418
+ * Whether every spelling of `name` outside the imports is a reference. Any binder spells its name
419
+ * in a declaring position, so each reference then reads the import, whatever kind of scope it is in.
420
+ */
421
+ function onlyReferences(cu: JS.CompilationUnit, name: string): boolean {
422
+ let references = true;
423
+ const visit = (node: any, parent: Cursor): void => {
424
+ if (!references) {
425
+ return;
426
+ }
427
+ if (Array.isArray(node)) {
428
+ node.forEach(child => visit(child, parent));
429
+ return;
430
+ }
431
+ const cursor = new Cursor(node, parent);
432
+ if (node?.kind === J.Kind.Identifier && node.simpleName === name) {
433
+ references = isReference(cursor, node);
434
+ } else if (isTree(node) || node?.kind === J.Kind.RightPadded || node?.kind === J.Kind.LeftPadded ||
435
+ node?.kind === J.Kind.Container) {
436
+ Object.entries(node).forEach(([key, value]) => key !== 'markers' && visit(value, cursor));
437
+ }
438
+ };
439
+ const root = new Cursor(cu);
440
+ // An import or an `export {…}` binds nothing the name could collide with.
441
+ cu.statements
442
+ .filter(s => s.element?.kind !== JS.Kind.Import && s.element?.kind !== JS.Kind.ExportDeclaration)
443
+ .forEach(s => visit(s, root));
444
+ return references;
445
+ }
446
+
405
447
  /**
406
448
  * @deprecated Use {@link maybeUnbind} instead — this is a call-shape change, not a behaviour
407
449
  * change: `maybeRemoveImport(v, module, member)` is `maybeUnbind(v, {module, member})`.
@@ -2347,15 +2347,15 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2347
2347
  return true;
2348
2348
  }
2349
2349
 
2350
- /** Whether the receiver has to be walked even where attribution alone settles the call. */
2351
- protected selectMustBeVisited(_method: J.MethodInvocation): boolean {
2350
+ /** Whether a call declared on `declaringType` matches a call of the same name declared on any type. */
2351
+ protected declaresAnyCallee(_declaringType: Type.FullyQualified): boolean {
2352
2352
  return false;
2353
2353
  }
2354
2354
 
2355
2355
  /**
2356
2356
  * Override method invocation comparison to include type attribution checking.
2357
- * When types match semantically, we allow matching even if one has a receiver
2358
- * and the other doesn't (e.g., `isDate(x)` vs `util.isDate(x)`).
2357
+ * When types match semantically, a call without a receiver matches one with
2358
+ * (e.g., `isDate(x)` vs `util.isDate(x)`).
2359
2359
  */
2360
2360
  override async visitMethodInvocation(method: J.MethodInvocation, other: J): Promise<J | undefined> {
2361
2361
  if (other.kind !== J.Kind.MethodInvocation) {
@@ -2377,28 +2377,10 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2377
2377
  // Check if we can skip name checking based on type attribution
2378
2378
  // We can only skip the name check if both have method types AND they represent the SAME method
2379
2379
  // (not just type-compatible methods, but the actual same function with same FQN)
2380
- let canSkipNameCheck = false;
2381
- if (method.methodType && otherMethod.methodType) {
2382
- // Check if both method types have fully qualified declaring types with the same FQN
2383
- // This indicates they're the same method from the same module (possibly aliased)
2384
- const methodDeclaringType = method.methodType.declaringType;
2385
- const otherDeclaringType = otherMethod.methodType.declaringType;
2386
-
2387
- if (methodDeclaringType && otherDeclaringType &&
2388
- Type.isFullyQualified(methodDeclaringType) && Type.isFullyQualified(otherDeclaringType)) {
2389
-
2390
- const methodFQN = Type.FullyQualified.getFullyQualifiedName(methodDeclaringType as Type.FullyQualified);
2391
- const otherFQN = Type.FullyQualified.getFullyQualifiedName(otherDeclaringType as Type.FullyQualified);
2392
-
2393
- // Same module/class AND same method name in the type = same method (can be aliased)
2394
- if (methodFQN === otherFQN && method.methodType.name === otherMethod.methodType.name) {
2395
- canSkipNameCheck = true;
2396
- }
2397
- // If FQNs or method names don't match, we can't skip name check - fall through to name checking
2398
- }
2399
- // If one or both don't have fully qualified types, we can't safely skip name checking
2400
- // Fall through to normal name comparison below
2401
- }
2380
+ // Same module/class AND same method name in the type = same method (can be aliased)
2381
+ const canSkipNameCheck = !!method.methodType && !!otherMethod.methodType &&
2382
+ method.methodType.name === otherMethod.methodType.name &&
2383
+ this.sameFullyQualifiedName(method.methodType.declaringType, otherMethod.methodType.declaringType);
2402
2384
 
2403
2385
  // Check names unless we determined we can skip based on type FQN matching
2404
2386
  if (!canSkipNameCheck) {
@@ -2421,24 +2403,18 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2421
2403
  const methodDeclaringType = method.methodType.declaringType;
2422
2404
  const otherDeclaringType = otherMethod.methodType.declaringType;
2423
2405
 
2424
- if (methodDeclaringType && otherDeclaringType &&
2425
- Type.isFullyQualified(methodDeclaringType) && Type.isFullyQualified(otherDeclaringType)) {
2426
-
2427
- const methodFQN = Type.FullyQualified.getFullyQualifiedName(methodDeclaringType as Type.FullyQualified);
2428
- const otherFQN = Type.FullyQualified.getFullyQualifiedName(otherDeclaringType as Type.FullyQualified);
2429
-
2430
- // Different declaring types = different methods, even with same name
2431
- if (methodFQN !== otherFQN) {
2432
- return this.valueMismatch('methodType.declaringType');
2433
- }
2406
+ // Different declaring types = different methods, even with same name
2407
+ if (Type.isFullyQualified(methodDeclaringType) && Type.isFullyQualified(otherDeclaringType) &&
2408
+ !this.sameFullyQualifiedName(methodDeclaringType, otherDeclaringType) &&
2409
+ !this.declaresAnyCallee(methodDeclaringType)) {
2410
+ return this.valueMismatch('methodType.declaringType');
2434
2411
  }
2435
2412
  }
2436
2413
  }
2437
2414
 
2438
- // When types match (canSkipNameCheck = true), we can skip select comparison entirely.
2439
- // This allows matching forwardRef() vs React.forwardRef() where types indicate same method.
2440
- if (!canSkipNameCheck || this.selectMustBeVisited(method)) {
2441
- // Types didn't provide a match - must compare receivers structurally
2415
+ // A written-out receiver still has to match. The method type proves which function
2416
+ // is called, not that the code reads the same.
2417
+ if (!canSkipNameCheck || method.select && !this.functionReceivers(method, otherMethod)) {
2442
2418
  if ((method.select === undefined) !== (otherMethod.select === undefined)) {
2443
2419
  return this.structuralMismatch('select');
2444
2420
  }
@@ -2448,7 +2424,6 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2448
2424
  if (!this.match) return method;
2449
2425
  }
2450
2426
  }
2451
- // else: types matched, skip select comparison (allows namespace vs named imports)
2452
2427
 
2453
2428
  // A pattern that spells out no type arguments says nothing about them, as with parentheses
2454
2429
  if (method.typeParameters) {
@@ -2505,6 +2480,7 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2505
2480
  * Override identifier comparison to include:
2506
2481
  * 1. Type checking for field access
2507
2482
  * 2. Semantic equivalence between `undefined` identifier and void expressions
2483
+ * 3. Names that differ but denote one declaration, as an import alias and its export do
2508
2484
  */
2509
2485
  override async visitIdentifier(identifier: J.Identifier, other: J): Promise<J | undefined> {
2510
2486
  // Check if this identifier is "undefined" and the other is a void expression
@@ -2519,9 +2495,8 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2519
2495
 
2520
2496
  const otherIdentifier = other as J.Identifier;
2521
2497
 
2522
- // Check name matches
2523
2498
  if (identifier.simpleName !== otherIdentifier.simpleName) {
2524
- return this.valueMismatch('simpleName');
2499
+ return this.denoteOneDeclaration(identifier, otherIdentifier) ? identifier : this.valueMismatch('simpleName');
2525
2500
  }
2526
2501
 
2527
2502
  // For identifiers with field types, check type attribution
@@ -2540,6 +2515,39 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2540
2515
  return super.visitIdentifier(identifier, other);
2541
2516
  }
2542
2517
 
2518
+ /**
2519
+ * Whether both calls have a bare function-valued receiver, such as a default import of a
2520
+ * callable module. The function type shell names no declaration, so the receivers are told
2521
+ * apart by the call alone.
2522
+ */
2523
+ private functionReceivers(method: J.MethodInvocation, other: J.MethodInvocation): boolean {
2524
+ const isFunctionName = (select?: J.RightPadded<Expression>) =>
2525
+ select?.element.kind === J.Kind.Identifier &&
2526
+ !(select.element as J.Identifier).fieldType &&
2527
+ Type.isFunctionType((select.element as J.Identifier).type);
2528
+ return isFunctionName(method.select) && isFunctionName(other.select);
2529
+ }
2530
+
2531
+ /** An import alias and its export denote one declaration, where a variable holding the same value does not. */
2532
+ private denoteOneDeclaration(identifier: J.Identifier, other: J.Identifier): boolean {
2533
+ if (identifier.fieldType || other.fieldType) {
2534
+ // An unresolved import has the placeholder name `unknown`, which identifies nothing
2535
+ return !!identifier.fieldType && !!other.fieldType &&
2536
+ identifier.fieldType.name === other.fieldType.name && identifier.fieldType.name !== 'unknown' &&
2537
+ this.sameFullyQualifiedName(identifier.fieldType.owner, other.fieldType.owner);
2538
+ }
2539
+ // Only a nominal type names a declaration. The function and object type shells are shared,
2540
+ // and a parameterized type is named after its base alone.
2541
+ return identifier.type?.kind === Type.Kind.Class && other.type?.kind === Type.Kind.Class &&
2542
+ !Type.isFunctionType(identifier.type) && !Type.isObjectType(identifier.type) &&
2543
+ this.sameFullyQualifiedName(identifier.type, other.type);
2544
+ }
2545
+
2546
+ private sameFullyQualifiedName(type?: Type, other?: Type): boolean {
2547
+ return Type.isFullyQualified(type) && Type.isFullyQualified(other) &&
2548
+ Type.FullyQualified.getFullyQualifiedName(type) === Type.FullyQualified.getFullyQualifiedName(other);
2549
+ }
2550
+
2543
2551
  /**
2544
2552
  * Override variable declarations comparison to handle lenient type matching.
2545
2553
  * When lenientTypeMatching is true, patterns without typeExpression can match
@@ -2634,6 +2642,16 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2634
2642
  );
2635
2643
  if (!this.match) return methodDeclaration;
2636
2644
 
2645
+ // Visit name annotations
2646
+ await this.visitArrayProperty(
2647
+ methodDeclaration,
2648
+ 'nameAnnotations',
2649
+ methodDeclaration.nameAnnotations,
2650
+ otherMethodDeclaration.nameAnnotations,
2651
+ async (ann1, ann2) => { await this.visit(ann1, ann2); }
2652
+ );
2653
+ if (!this.match) return methodDeclaration;
2654
+
2637
2655
  // Visit type parameters if present
2638
2656
  if (!!methodDeclaration.typeParameters !== !!otherMethodDeclaration.typeParameters) {
2639
2657
  return this.structuralMismatch('typeParameters');
@@ -2742,3 +2760,71 @@ export class JavaScriptSemanticComparatorVisitor extends JavaScriptComparatorVis
2742
2760
  return literal;
2743
2761
  }
2744
2762
  }
2763
+
2764
+ /**
2765
+ * Whether two trees are the same code. Whitespace, comments, optional semicolons and trailing
2766
+ * commas, quote style (`'a'` and `"a"`), number spelling (`255` and `0xFF`) and type attribution
2767
+ * do not count. Markers count only where they are syntax that changes meaning, such as `?.`, a non-null
2768
+ * `!`, `function*` and `yield*`, so a marker a recipe attaches does not make trees differ.
2769
+ */
2770
+ export async function isEqual(a: J, b: J): Promise<boolean> {
2771
+ return new CodeComparator().compare(a, b);
2772
+ }
2773
+
2774
+ class CodeComparator extends JavaScriptComparatorVisitor {
2775
+ private readonly meaningfulMarkers: ReadonlySet<string> = new Set([
2776
+ JS.Markers.Optional, JS.Markers.NonNullAssertion, JS.Markers.Generator, JS.Markers.DelegatedYield
2777
+ ]);
2778
+
2779
+ override async visit<R extends J>(j: Tree, p: J, parent?: Cursor): Promise<R | undefined> {
2780
+ if (this.match && this.meaningOf(j as J) !== this.meaningOf(p)) {
2781
+ return this.structuralMismatch('markers') as R;
2782
+ }
2783
+ return super.visit(j, p, parent);
2784
+ }
2785
+
2786
+ protected override async visitProperty(j: any, other: any, propertyName?: string): Promise<any> {
2787
+ if (Type.isType(j) || Type.isType(other)) {
2788
+ return j;
2789
+ }
2790
+ return super.visitProperty(j, other, propertyName);
2791
+ }
2792
+
2793
+ override async visitLiteral(literal: J.Literal, other: J): Promise<J | undefined> {
2794
+ if (!this.match) return literal;
2795
+ const otherLiteral = other as J.Literal;
2796
+ if (denotation(literal) !== denotation(otherLiteral)) {
2797
+ return this.valueMismatch('value', literal.value, otherLiteral.value);
2798
+ }
2799
+ return literal;
2800
+ }
2801
+
2802
+ private meaningOf(tree: J): string {
2803
+ if (!tree.markers?.markers.length) {
2804
+ return '';
2805
+ }
2806
+ return tree.markers.markers
2807
+ .map(marker => marker.kind)
2808
+ .filter(kind => this.meaningfulMarkers.has(kind))
2809
+ .sort()
2810
+ .join(',');
2811
+ }
2812
+ }
2813
+
2814
+ /**
2815
+ * What a literal stands for, as a comparable key. A regex holds its source text as its value, so it
2816
+ * is told apart from a string by its slashes. A string with a `\uXXXX` surrogate escape holds no
2817
+ * decoded value, so its text between the quotes and its escapes stand in for one.
2818
+ */
2819
+ function denotation(literal: J.Literal): string {
2820
+ const source = literal.valueSource ?? '';
2821
+ const primitive = literal.type?.keyword;
2822
+ if (source.startsWith('/')) {
2823
+ return JSON.stringify(['regex', source]);
2824
+ }
2825
+ if (literal.unicodeEscapes) {
2826
+ const quoted = source.startsWith("'") || source.startsWith('"');
2827
+ return JSON.stringify([primitive, quoted ? source.slice(1, -1) : source, literal.unicodeEscapes]);
2828
+ }
2829
+ return JSON.stringify([primitive, literal.value]);
2830
+ }
@@ -47,6 +47,15 @@ export const autoFormat = async <J2 extends J, P>(
47
47
  ): Promise<J2> =>
48
48
  (await new AutoformatVisitor(stopAfter, styles).visit(j, p, parent) as J2);
49
49
 
50
+ /** `j` re-indented to the style of the file `parent` leads to, with its other whitespace as it is. */
51
+ export const autoIndent = async <J2 extends J, P>(j: J2, p: P, parent?: Cursor): Promise<J2> => {
52
+ const styleSource = (isSourceFile(j) ? j : parent?.firstEnclosing(isSourceFile)) ?? j;
53
+ const tabsAndIndents = getStyle(StyleKind.TabsAndIndentsStyle, styleSource) as TabsAndIndentsStyle;
54
+ const prettierStyle = getPrettierStyle(j, parent);
55
+ const style = prettierStyle ? tabsAndIndentsFrom(prettierStyle, tabsAndIndents) : tabsAndIndents;
56
+ return await new TabsAndIndentsVisitor(style).visit(j, p, parent) as J2;
57
+ }
58
+
50
59
  /**
51
60
  * Formats JavaScript/TypeScript code using a comprehensive set of formatting rules.
52
61
  *
@@ -179,15 +179,42 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
179
179
  // TemplateExpressionSpan: reset indent context - template literal content determines its own indentation
180
180
  // The expression inside ${...} should be indented based on where it appears in the template, not outer code
181
181
  if (tree.kind === JS.Kind.TemplateExpressionSpan) {
182
- // Extract base indent from the expression's prefix whitespace (after the last newline)
182
+ // Whitespace after ${ belongs to the span, not its child expression.
183
183
  const span = tree as JS.TemplateExpression.Span;
184
- const prefix = span.expression?.prefix?.whitespace ?? "";
184
+ const prefix = lastWhitespace(span.prefix);
185
185
  const lastNewline = prefix.lastIndexOf("\n");
186
186
  if (lastNewline >= 0) {
187
- return prefix.length - lastNewline - 1;
187
+ return this.indentWidth(prefix.substring(lastNewline + 1));
188
+ }
189
+ const template = this.cursor.parentTree()?.value as JS.TemplateExpression | undefined;
190
+ if (template?.kind === JS.Kind.TemplateExpression) {
191
+ const index = template.spans.findIndex(s => s.element.id === span.id);
192
+ const preceding = index > 0 ? template.spans[index - 1].element.tail : template.head;
193
+ const text = preceding.valueSource ?? "";
194
+ const newline = text.lastIndexOf("\n");
195
+ if (newline >= 0) {
196
+ // A span on a literal's own line is anchored to that line's indentation.
197
+ const indent = text.substring(newline + 1).match(/^[ \t]*/)?.[0] ?? "";
198
+ return this.indentWidth(indent);
199
+ }
200
+ }
201
+ // An inline interpolation continues the surrounding code line, not column zero.
202
+ for (let c = this.cursor.parent; c; c = c.parent) {
203
+ const parent = c.value as J;
204
+ if (parent?.prefix && spaceContainsNewline(parent.prefix)) {
205
+ return (c.messages.get("indentContext") as IndentContext | undefined)?.[0] ?? parentMyIndent;
206
+ }
188
207
  }
189
208
  return 0;
190
209
  }
210
+ // The tail prefix is the whitespace before }, not another continuation of the expression.
211
+ const parentTree = this.cursor.parentTree()?.value;
212
+ if (parentTree?.kind === JS.Kind.TemplateExpressionSpan &&
213
+ (parentTree as JS.TemplateExpression.Span).tail === tree) {
214
+ const prefix = lastWhitespace(tree.prefix);
215
+ const newline = prefix.lastIndexOf("\n");
216
+ return newline < 0 ? parentMyIndent : this.indentWidth(prefix.substring(newline + 1));
217
+ }
191
218
  if (tree.kind === J.Kind.IfElse || parentIndentKind === 'align') {
192
219
  return parentMyIndent;
193
220
  }