@openrewrite/rewrite 8.69.0-20251205-160300 → 8.69.0-20251205-203346

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.
@@ -13,9 +13,9 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {isJavaScript, JS, JSX} from "./tree";
16
+ import {JS} from "./tree";
17
17
  import {JavaScriptVisitor} from "./visitor";
18
- import {Comment, isJava, J, Statement} from "../java";
18
+ import {Comment, J, lastWhitespace, replaceLastWhitespace, Statement} from "../java";
19
19
  import {Draft, produce} from "immer";
20
20
  import {Cursor, isScope, Tree} from "../tree";
21
21
  import {
@@ -27,6 +27,11 @@ import {
27
27
  WrappingAndBracesStyle
28
28
  } from "./style";
29
29
  import {produceAsync} from "../visitor";
30
+ import {findMarker} from "../markers";
31
+ import {Generator} from "./markers";
32
+ import {TabsAndIndentsVisitor} from "./tabs-and-indents-visitor";
33
+
34
+ export {TabsAndIndentsVisitor} from "./tabs-and-indents-visitor";
30
35
 
31
36
  export const maybeAutoFormat = async <J2 extends J, P>(before: J2, after: J2, p: P, stopAfter?: J, parent?: Cursor): Promise<J2> => {
32
37
  if (before !== after) {
@@ -67,6 +72,7 @@ export class AutoformatVisitor<P> extends JavaScriptVisitor<P> {
67
72
 
68
73
  export class NormalizeWhitespaceVisitor<P> extends JavaScriptVisitor<P> {
69
74
  // called NormalizeFormat in Java
75
+ // Ensures that whitespace is on the outermost AST element possible
70
76
 
71
77
  constructor(private stopAfter?: Tree) {
72
78
  super();
@@ -170,8 +176,13 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
170
176
  throw new Error("Unsupported operator type " + ret.operator.element.valueOf());
171
177
  }
172
178
  return produce(ret, draft => {
173
- draft.operator.before.whitespace = property ? " " : "";
174
- draft.right.prefix.whitespace = property ? " " : "";
179
+ // Preserve newlines - only modify if no newlines present
180
+ if (!draft.operator.before.whitespace.includes("\n")) {
181
+ draft.operator.before.whitespace = property ? " " : "";
182
+ }
183
+ if (!draft.right.prefix.whitespace.includes("\n")) {
184
+ draft.right.prefix.whitespace = property ? " " : "";
185
+ }
175
186
  }) as J.Binary;
176
187
  }
177
188
 
@@ -199,7 +210,11 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
199
210
  return produce(ret, draft => {
200
211
  if (draft.elements.length > 1) {
201
212
  for (let i = 1; i < draft.elements.length; i++) {
202
- draft.elements[i].element.prefix.whitespace = this.style.other.afterComma ? " " : "";
213
+ const currentWs = draft.elements[i].element.prefix.whitespace;
214
+ // Preserve original newlines - only adjust spacing when elements are on same line
215
+ if (!currentWs.includes("\n")) {
216
+ draft.elements[i].element.prefix.whitespace = this.style.other.afterComma ? " " : "";
217
+ }
203
218
  }
204
219
  }
205
220
  });
@@ -313,6 +328,16 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
313
328
  }
314
329
  draft.parameters = await this.spaceBeforeContainer(draft.parameters, this.style.beforeParentheses.functionDeclarationParentheses);
315
330
 
331
+ // Handle generator asterisk spacing
332
+ // - space before * is in the Generator marker's prefix
333
+ // - space after * is in the method name's prefix
334
+ const generatorIndex = ret.markers.markers.findIndex(m => m.kind === JS.Markers.Generator);
335
+ if (generatorIndex >= 0) {
336
+ const generator = draft.markers.markers[generatorIndex] as Draft<Generator>;
337
+ generator.prefix.whitespace = this.style.other.beforeAsteriskInGenerator ? " " : "";
338
+ draft.name.prefix.whitespace = this.style.other.afterAsteriskInGenerator ? " " : "";
339
+ }
340
+
316
341
  // TODO typeParameters handling - see visitClassDeclaration
317
342
  });
318
343
  }
@@ -473,7 +498,10 @@ export class SpacesVisitor<P> extends JavaScriptVisitor<P> {
473
498
  private async spaceBeforeLeftPaddedElement<T extends J>(left: J.LeftPadded<T>, spaceBeforePadding: boolean, spaceBeforeElement: boolean): Promise<J.LeftPadded<T>> {
474
499
  return (await produceAsync(left, async draft => {
475
500
  if (draft.before.comments.length == 0) {
476
- draft.before.whitespace = spaceBeforePadding ? " " : "";
501
+ // Preserve newlines - only modify if no newlines present
502
+ if (!draft.before.whitespace.includes("\n")) {
503
+ draft.before.whitespace = spaceBeforePadding ? " " : "";
504
+ }
477
505
  }
478
506
  draft.element = await this.spaceBefore(left.element, spaceBeforeElement) as Draft<T>;
479
507
  }))!;
@@ -648,7 +676,10 @@ export class WrappingAndBracesVisitor<P> extends JavaScriptVisitor<P> {
648
676
  const b = await super.visitBlock(block, p) as J.Block;
649
677
  return produce(b, draft => {
650
678
  if (!draft.end.whitespace.includes("\n") && (draft.statements.length == 0 || !draft.statements[draft.statements.length - 1].after.whitespace.includes("\n"))) {
651
- if (this.cursor.parent?.value.kind !== J.Kind.NewClass) {
679
+ // Skip newline for object literals and empty lambda/function bodies
680
+ const parentKind = this.cursor.parent?.value.kind;
681
+ if (parentKind !== J.Kind.NewClass &&
682
+ !(draft.statements.length === 0 && (parentKind === J.Kind.Lambda || parentKind === J.Kind.MethodDeclaration))) {
652
683
  draft.end = this.withNewlineSpace(draft.end);
653
684
  }
654
685
  }
@@ -753,9 +784,12 @@ export class MinimumViableSpacingVisitor<P> extends JavaScriptVisitor<P> {
753
784
  first = false;
754
785
  }
755
786
 
756
- c = produce(c, draft => {
757
- this.ensureSpace(draft.name.prefix);
758
- });
787
+ // anonymous classes have an empty name
788
+ if (c.name.simpleName !== "") {
789
+ c = produce(c, draft => {
790
+ this.ensureSpace(draft.name.prefix);
791
+ });
792
+ }
759
793
 
760
794
  if (c.typeParameters && c.typeParameters.elements.length > 0 && c.typeParameters.before.whitespace === "" && !first) {
761
795
  c = produce(c, draft => {
@@ -789,10 +823,6 @@ export class MinimumViableSpacingVisitor<P> extends JavaScriptVisitor<P> {
789
823
  let m = await super.visitMethodDeclaration(method, p) as J.MethodDeclaration;
790
824
  let first = m.leadingAnnotations.length === 0;
791
825
 
792
- if (method.markers.markers.find(x => x.kind == JS.Markers.FunctionDeclaration)) {
793
- first = false;
794
- }
795
-
796
826
  if (m.modifiers.length > 0) {
797
827
  if (!first && m.modifiers[0].prefix.whitespace === "") {
798
828
  m = produce(m, draft => {
@@ -807,6 +837,12 @@ export class MinimumViableSpacingVisitor<P> extends JavaScriptVisitor<P> {
807
837
  first = false;
808
838
  }
809
839
 
840
+ // FunctionDeclaration marker check must come AFTER modifiers processing
841
+ // to avoid adding unwanted space before the first modifier (e.g., 'async')
842
+ if (findMarker(method, JS.Markers.FunctionDeclaration)) {
843
+ first = false;
844
+ }
845
+
810
846
  if (!first && m.name.prefix.whitespace === "") {
811
847
  m = produce(m, draft => {
812
848
  this.ensureSpace(draft.name.prefix);
@@ -1044,7 +1080,9 @@ export class BlankLinesVisitor<P> extends JavaScriptVisitor<P> {
1044
1080
  protected async visitBlock(block: J.Block, p: P): Promise<J.Block> {
1045
1081
  const b = await super.visitBlock(block, p) as J.Block;
1046
1082
  return produce(b, draft => {
1047
- if (this.cursor.parent?.value.kind != J.Kind.NewClass) {
1083
+ const parentKind = this.cursor.parent?.value.kind;
1084
+ // Skip newline only for object literals (NewClass) - they should preserve single-line formatting
1085
+ if (parentKind != J.Kind.NewClass) {
1048
1086
  if (!draft.end.whitespace.includes("\n")) {
1049
1087
  draft.end.whitespace = draft.end.whitespace.replace(/[ \t]+$/, '') + "\n";
1050
1088
  }
@@ -1083,197 +1121,28 @@ export class BlankLinesVisitor<P> extends JavaScriptVisitor<P> {
1083
1121
  }
1084
1122
 
1085
1123
  private ensurePrefixHasNewLine<T extends J>(node: Draft<J>) {
1086
- if (node.prefix && !node.prefix.whitespace.includes("\n")) {
1087
- if (node.kind === JS.Kind.ExpressionStatement) {
1088
- this.ensurePrefixHasNewLine((node as JS.ExpressionStatement).expression);
1089
- } else {
1090
- node.prefix.whitespace = "\n";
1091
- }
1092
- }
1093
- }
1124
+ if (!node.prefix) return;
1094
1125
 
1095
- private static countNewlines(s: string): number {
1096
- return [...s].filter(c => c === "\n").length;
1097
- }
1098
-
1099
- override async postVisit(tree: J, p: P): Promise<J | undefined> {
1100
- if (this.stopAfter != null && isScope(this.stopAfter, tree)) {
1101
- this.cursor?.root.messages.set("stop", true);
1126
+ // Check if newline already exists in the effective last whitespace
1127
+ if (lastWhitespace(node.prefix).includes("\n")) {
1128
+ return; // Already has a newline
1102
1129
  }
1103
- return super.postVisit(tree, p);
1104
- }
1105
- }
1106
-
1107
- export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
1108
- private readonly singleIndent: string;
1109
1130
 
1110
- constructor(private readonly tabsAndIndentsStyle: TabsAndIndentsStyle, private stopAfter?: Tree) {
1111
- super();
1112
-
1113
- if (this.tabsAndIndentsStyle.useTabCharacter) {
1114
- this.singleIndent = "\t";
1131
+ if (node.kind === JS.Kind.ExpressionStatement) {
1132
+ this.ensurePrefixHasNewLine((node as JS.ExpressionStatement).expression);
1115
1133
  } else {
1116
- this.singleIndent = " ".repeat(this.tabsAndIndentsStyle.indentSize);
1117
- }
1118
- }
1119
-
1120
- protected async preVisit(tree: J, p: P): Promise<J | undefined> {
1121
- let ret = await super.preVisit(tree, p)! as J;
1122
-
1123
- let indentShouldIncrease =
1124
- tree.kind === J.Kind.Block
1125
- || this.cursor.parent?.parent?.parent?.value.kind == J.Kind.Case
1126
- || (tree.kind === JS.Kind.StatementExpression && (tree as JS.StatementExpression).statement.kind == J.Kind.MethodDeclaration && tree.prefix.whitespace.includes("\n"))
1127
- || tree.kind === JS.Kind.JsxTag;
1128
-
1129
-
1130
- const previousIndent = this.currentIndent;
1131
-
1132
- if (tree.kind === J.Kind.IfElse && this.cursor.getNearestMessage("else-indent") !== undefined) {
1133
- this.cursor.messages.set("indentToUse", this.cursor.getNearestMessage("else-indent"));
1134
- } else if (indentShouldIncrease) {
1135
- this.cursor.messages.set("indentToUse", this.currentIndent + this.singleIndent);
1136
- }
1137
-
1138
- if (tree.kind === JS.Kind.JsxTag) {
1139
- this.cursor.messages.set("jsxTagIndent", this.currentIndent);
1140
- }
1141
-
1142
- if (tree.kind === J.Kind.IfElse && this.cursor.messages.get("else-indent") !== undefined) {
1143
- this.cursor.messages.set("indentToUse", this.cursor.messages.get("else-indent"));
1144
- this.cursor.messages.delete("else-indent");
1134
+ node.prefix = replaceLastWhitespace(node.prefix, () => "\n");
1145
1135
  }
1146
- const relativeIndent: string = this.currentIndent;
1147
-
1148
- ret = produce(ret, draft => {
1149
- if (draft.prefix == undefined) {
1150
- draft.prefix = {kind: J.Kind.Space, comments: [], whitespace: ""};
1151
- }
1152
- if (draft.prefix.whitespace.includes("\n")) {
1153
- draft.prefix.whitespace = this.combineIndent(draft.prefix.whitespace, relativeIndent);
1154
- }
1155
- if (draft.kind === J.Kind.Block) {
1156
- const block = draft as Draft<J> as Draft<J.Block>;
1157
- const indentToUseInClosing = indentShouldIncrease ? previousIndent : relativeIndent;
1158
- block.end.whitespace = this.combineIndent(block.end.whitespace, indentToUseInClosing);
1159
- }
1160
- });
1161
-
1162
- indentShouldIncrease = false;
1163
- // Increase indent for control structures with non-block bodies
1164
- if (tree.kind === J.Kind.If) {
1165
- const ifStmt = tree as J.If;
1166
- if (ifStmt.thenPart.element.kind !== J.Kind.Block) {
1167
- indentShouldIncrease = true;
1168
- this.cursor.messages.set("else-indent", this.currentIndent);
1169
- }
1170
- } else if (tree.kind === J.Kind.WhileLoop) {
1171
- const whileLoop = tree as J.WhileLoop;
1172
- if (whileLoop.body.element.kind !== J.Kind.Block) {
1173
- indentShouldIncrease = true;
1174
- }
1175
- } else if (tree.kind === J.Kind.ForLoop) {
1176
- const forLoop = tree as J.ForLoop;
1177
- if (forLoop.body.element.kind !== J.Kind.Block) {
1178
- indentShouldIncrease = true;
1179
- }
1180
- } else if (tree.kind === JS.Kind.JsxTag) {
1181
- indentShouldIncrease = true;
1182
- }
1183
- if (indentShouldIncrease) {
1184
- this.cursor.messages.set("indentToUse", this.currentIndent + this.singleIndent);
1185
- }
1186
-
1187
- return ret;
1188
- }
1189
-
1190
- override async visitSpace(space: J.Space, p: P): Promise<J.Space> {
1191
- const ret = await super.visitSpace(space, p);
1192
- if (space.whitespace.includes("\n")) {
1193
- let parentCursor = this.cursor.parent;
1194
- while (parentCursor != null && parentCursor.value.kind !== JS.Kind.JsxTag) {
1195
- parentCursor = parentCursor.parent;
1196
- }
1197
- if (parentCursor && parentCursor.value.kind === JS.Kind.JsxTag) {
1198
- parentCursor.messages.set("jsxTagWithNewline", true)
1199
- }
1200
- }
1201
- return ret;
1202
1136
  }
1203
1137
 
1204
- async visit<R extends J>(tree: Tree, p: P, parent?: Cursor): Promise<R | undefined> {
1205
- if (this.cursor?.getNearestMessage("stop") != null) {
1206
- return tree as R;
1207
- }
1208
-
1209
- if (parent) {
1210
- this.cursor = new Cursor(tree, parent);
1211
- for (let c: Cursor | undefined = this.cursor; c != null; c = c.parent) {
1212
- let space: J.Space;
1213
- const v = c.value;
1214
- if (v.kind == J.Kind.RightPadded) {
1215
- space = v.after;
1216
- } else if (v.kind == J.Kind.LeftPadded || v.kind == J.Kind.Container) {
1217
- space = v.before;
1218
- } else if (isJava(v) || isJavaScript(v)) {
1219
- space = v.prefix;
1220
- } else {
1221
- continue;
1222
- }
1223
-
1224
- const lastWhitespace = space.comments.length > 0 ? space.comments[space.comments.length - 1].suffix : space.whitespace;
1225
- const idx = lastWhitespace.lastIndexOf('\n');
1226
- if (idx !== -1) {
1227
- c.messages.set("indentToUse", lastWhitespace.substring(idx + 1));
1228
- break;
1229
- }
1230
- }
1231
- }
1232
- return await super.visit(tree, p) as R;
1233
- }
1234
-
1235
- public async visitLeftPadded<T extends J | J.Space | number | string | boolean>(left: J.LeftPadded<T>, p: P): Promise<J.LeftPadded<T> | undefined> {
1236
- const ret = await super.visitLeftPadded(left, p);
1237
- if (ret == undefined) {
1238
- return ret;
1239
- }
1240
- return produce(ret, draft => {
1241
- if (draft.before.whitespace.includes("\n")) {
1242
- draft.before.whitespace = this.combineIndent(draft.before.whitespace, this.currentIndent);
1243
- }
1244
- });
1245
- }
1246
-
1247
- private get currentIndent(): string {
1248
- return this.cursor.getNearestMessage("indentToUse") ?? "";
1249
- }
1250
-
1251
- private combineIndent(oldWs: string, relativeIndent: string): string {
1252
- return oldWs.substring(0, oldWs.lastIndexOf("\n") + 1) + relativeIndent;
1138
+ private static countNewlines(s: string): number {
1139
+ return [...s].filter(c => c === "\n").length;
1253
1140
  }
1254
1141
 
1255
1142
  override async postVisit(tree: J, p: P): Promise<J | undefined> {
1256
1143
  if (this.stopAfter != null && isScope(this.stopAfter, tree)) {
1257
1144
  this.cursor?.root.messages.set("stop", true);
1258
1145
  }
1259
- let treeChanged = tree;
1260
- if (tree.kind == JS.Kind.JsxTag) {
1261
- const tag = tree as JSX.Tag;
1262
- if (this.cursor.messages.get("jsxTagWithNewline")) {
1263
- const jsxTagIndent = this.cursor.messages.get("jsxTagIndent");
1264
- if (jsxTagIndent) {
1265
- this.cursor.messages.delete("jsxTagWithNewline");
1266
- treeChanged = produce(tag, draft => {
1267
- if (draft.children) {
1268
- const lastChild = draft.children[draft.children.length - 1];
1269
- if (lastChild.kind === J.Kind.Literal) {
1270
- lastChild.prefix.whitespace = this.combineIndent(lastChild.prefix.whitespace, jsxTagIndent);
1271
- }
1272
- }
1273
- });
1274
- }
1275
- }
1276
- }
1277
- return super.postVisit(treeChanged, p);
1146
+ return super.postVisit(tree, p);
1278
1147
  }
1279
1148
  }
@@ -3431,6 +3431,8 @@ export class JavaScriptParserVisitor {
3431
3431
  }
3432
3432
 
3433
3433
  visitCaseBlock(node: ts.CaseBlock): J.Block {
3434
+ // consume end space so it gets assigned to the block's `end`
3435
+ const end = this.prefix(node.getLastToken()!);
3434
3436
  return {
3435
3437
  kind: J.Kind.Block,
3436
3438
  id: randomId(),
@@ -3442,7 +3444,7 @@ export class JavaScriptParserVisitor {
3442
3444
  this.visit(clause),
3443
3445
  this.suffix(clause)
3444
3446
  )),
3445
- end: this.prefix(node.getLastToken()!)
3447
+ end: end
3446
3448
  }
3447
3449
  }
3448
3450
 
@@ -0,0 +1,250 @@
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 {isJavaScript, JS, JSX} from "./tree";
17
+ import {JavaScriptVisitor} from "./visitor";
18
+ import {isJava, J, lastWhitespace, replaceLastWhitespace} from "../java";
19
+ import {produce} from "immer";
20
+ import {Cursor, isScope, Tree} from "../tree";
21
+ import {TabsAndIndentsStyle} from "./style";
22
+
23
+ type IndentKind = 'block' | 'continuation' | 'align';
24
+ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
25
+ private readonly singleIndent: string;
26
+
27
+ constructor(private readonly tabsAndIndentsStyle: TabsAndIndentsStyle, private stopAfter?: Tree) {
28
+ super();
29
+
30
+ if (this.tabsAndIndentsStyle.useTabCharacter) {
31
+ this.singleIndent = "\t";
32
+ } else {
33
+ this.singleIndent = " ".repeat(this.tabsAndIndentsStyle.indentSize);
34
+ }
35
+ }
36
+
37
+ protected async preVisit(tree: J, _p: P): Promise<J | undefined> {
38
+ this.setupCursorMessagesForTree(this.cursor, tree);
39
+ return tree;
40
+ }
41
+
42
+ private setupCursorMessagesForTree(cursor: Cursor, tree: J): void {
43
+ const [parentMyIndent, parentIndentKind] = this.getParentIndentContext(cursor);
44
+ const myIndent = this.computeMyIndent(tree, parentMyIndent, parentIndentKind);
45
+ cursor.messages.set("myIndent", myIndent);
46
+ cursor.messages.set("indentKind", this.computeIndentKind(tree));
47
+ }
48
+
49
+ private getParentIndentContext(cursor: Cursor): [string, IndentKind] {
50
+ for (let c = cursor.parent; c != null; c = c.parent) {
51
+ const indent = c.messages.get("myIndent") as string | undefined;
52
+ if (indent !== undefined) {
53
+ const kind = c.messages.get("indentKind") as IndentKind ?? 'continuation';
54
+ return [indent, kind];
55
+ }
56
+ }
57
+ return ["", 'continuation'];
58
+ }
59
+
60
+ private computeMyIndent(tree: J, parentMyIndent: string, parentIndentKind: IndentKind): string {
61
+ if (tree.kind === J.Kind.IfElse || parentIndentKind === 'align') {
62
+ return parentMyIndent;
63
+ }
64
+ if (parentIndentKind === 'block') {
65
+ return parentMyIndent + this.singleIndent;
66
+ }
67
+ const hasNewline = tree.prefix?.whitespace?.includes("\n") ||
68
+ tree.prefix?.comments?.some(c => c.suffix.includes("\n"));
69
+ return hasNewline ? parentMyIndent + this.singleIndent : parentMyIndent;
70
+ }
71
+
72
+ private computeIndentKind(tree: J): IndentKind {
73
+ switch (tree.kind) {
74
+ case J.Kind.Block:
75
+ case J.Kind.Case:
76
+ return 'block';
77
+ case JS.Kind.CompilationUnit:
78
+ return 'align';
79
+ default:
80
+ return 'continuation';
81
+ }
82
+ }
83
+
84
+ override async postVisit(tree: J, _p: P): Promise<J | undefined> {
85
+ if (this.stopAfter != null && isScope(this.stopAfter, tree)) {
86
+ this.cursor?.root.messages.set("stop", true);
87
+ }
88
+
89
+ const myIndent = this.cursor.messages.get("myIndent") as string | undefined;
90
+ if (myIndent === undefined) {
91
+ return tree;
92
+ }
93
+
94
+ let result = tree;
95
+ if (result.prefix?.whitespace?.includes("\n")) {
96
+ result = produce(result, draft => {
97
+ draft.prefix!.whitespace = this.combineIndent(draft.prefix!.whitespace, myIndent);
98
+ });
99
+ }
100
+
101
+ if (result.kind === J.Kind.Block) {
102
+ result = this.normalizeBlockEnd(result as J.Block, myIndent);
103
+ } else if (result.kind === JS.Kind.JsxTag) {
104
+ result = this.normalizeJsxTagEnd(result as JSX.Tag, myIndent);
105
+ }
106
+
107
+ return result;
108
+ }
109
+
110
+ private normalizeBlockEnd(block: J.Block, myIndent: string): J.Block {
111
+ const effectiveLastWs = lastWhitespace(block.end);
112
+ if (!effectiveLastWs.includes("\n")) {
113
+ return block;
114
+ }
115
+ return produce(block, draft => {
116
+ draft.end = replaceLastWhitespace(draft.end, ws => this.combineIndent(ws, myIndent));
117
+ });
118
+ }
119
+
120
+ private normalizeJsxTagEnd(tag: JSX.Tag, myIndent: string): JSX.Tag {
121
+ if (!tag.children || tag.children.length === 0) {
122
+ return tag;
123
+ }
124
+ const lastChild = tag.children[tag.children.length - 1];
125
+ if (lastChild.kind !== J.Kind.Literal || !lastChild.prefix.whitespace.includes("\n")) {
126
+ return tag;
127
+ }
128
+ return produce(tag, draft => {
129
+ const lastChildDraft = draft.children![draft.children!.length - 1];
130
+ lastChildDraft.prefix.whitespace = this.combineIndent(lastChildDraft.prefix.whitespace, myIndent);
131
+ });
132
+ }
133
+
134
+ public async visitContainer<T extends J>(container: J.Container<T>, p: P): Promise<J.Container<T>> {
135
+ const parentIndent = this.cursor.messages.get("myIndent") as string ?? "";
136
+ const elementsIndent = container.before.whitespace.includes("\n")
137
+ ? parentIndent + this.singleIndent
138
+ : parentIndent;
139
+
140
+ const savedMyIndent = this.cursor.messages.get("myIndent");
141
+ this.cursor.messages.set("myIndent", elementsIndent);
142
+ let ret = await super.visitContainer(container, p);
143
+ if (savedMyIndent !== undefined) {
144
+ this.cursor.messages.set("myIndent", savedMyIndent);
145
+ }
146
+
147
+ if (ret.before.whitespace.includes("\n")) {
148
+ ret = produce(ret, draft => {
149
+ draft.before.whitespace = this.combineIndent(draft.before.whitespace, elementsIndent);
150
+ });
151
+ }
152
+
153
+ if (ret.elements.length > 0) {
154
+ const effectiveLastWs = lastWhitespace(ret.elements[ret.elements.length - 1].after);
155
+ if (effectiveLastWs.includes("\n")) {
156
+ ret = produce(ret, draft => {
157
+ const lastDraft = draft.elements[draft.elements.length - 1];
158
+ lastDraft.after = replaceLastWhitespace(lastDraft.after, ws => this.combineIndent(ws, parentIndent));
159
+ });
160
+ }
161
+ }
162
+
163
+ return ret;
164
+ }
165
+
166
+ public async visitLeftPadded<T extends J | J.Space | number | string | boolean>(
167
+ left: J.LeftPadded<T>,
168
+ p: P
169
+ ): Promise<J.LeftPadded<T> | undefined> {
170
+ const ret = await super.visitLeftPadded(left, p);
171
+ if (ret === undefined || !ret.before.whitespace.includes("\n")) {
172
+ return ret;
173
+ }
174
+ const parentIndent = this.cursor.messages.get("myIndent") as string ?? "";
175
+ return produce(ret, draft => {
176
+ draft.before.whitespace = this.combineIndent(draft.before.whitespace, parentIndent + this.singleIndent);
177
+ });
178
+ }
179
+
180
+ async visit<R extends J>(tree: Tree, p: P, parent?: Cursor): Promise<R | undefined> {
181
+ if (this.cursor?.getNearestMessage("stop") != null) {
182
+ return tree as R;
183
+ }
184
+
185
+ if (parent) {
186
+ this.cursor = new Cursor(tree, parent);
187
+ this.setupAncestorIndents();
188
+ }
189
+
190
+ return await super.visit(tree, p) as R;
191
+ }
192
+
193
+ private setupAncestorIndents(): void {
194
+ const path: Cursor[] = [];
195
+ let anchorCursor: Cursor | undefined;
196
+ let anchorIndent = "";
197
+
198
+ for (let c = this.cursor.parent; c; c = c.parent) {
199
+ path.push(c);
200
+ const v = c.value;
201
+
202
+ if (this.isActualJNode(v) && !anchorCursor && v.prefix) {
203
+ const ws = lastWhitespace(v.prefix);
204
+ const idx = ws.lastIndexOf('\n');
205
+ if (idx !== -1) {
206
+ anchorCursor = c;
207
+ anchorIndent = ws.substring(idx + 1);
208
+ }
209
+ }
210
+
211
+ if (v.kind === JS.Kind.CompilationUnit) {
212
+ if (!anchorCursor) {
213
+ anchorCursor = c;
214
+ anchorIndent = "";
215
+ }
216
+ break;
217
+ }
218
+ }
219
+
220
+ if (path.length === 0) return;
221
+ path.reverse();
222
+
223
+ for (const c of path) {
224
+ const v = c.value;
225
+ if (!this.isActualJNode(v)) continue;
226
+
227
+ const savedCursor = this.cursor;
228
+ this.cursor = c;
229
+ if (c === anchorCursor) {
230
+ c.messages.set("myIndent", anchorIndent);
231
+ c.messages.set("indentKind", this.computeIndentKind(v));
232
+ } else {
233
+ this.setupCursorMessagesForTree(c, v);
234
+ }
235
+ this.cursor = savedCursor;
236
+ }
237
+ }
238
+
239
+ private isActualJNode(v: any): v is J {
240
+ return (isJava(v) || isJavaScript(v)) &&
241
+ v.kind !== J.Kind.Container &&
242
+ v.kind !== J.Kind.LeftPadded &&
243
+ v.kind !== J.Kind.RightPadded;
244
+ }
245
+
246
+ private combineIndent(oldWs: string, newIndent: string): string {
247
+ const lastNewline = oldWs.lastIndexOf("\n");
248
+ return oldWs.substring(0, lastNewline + 1) + newIndent;
249
+ }
250
+ }