@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.
- package/dist/java/formatting-utils.d.ts +15 -0
- package/dist/java/formatting-utils.d.ts.map +1 -1
- package/dist/java/formatting-utils.js +32 -0
- package/dist/java/formatting-utils.js.map +1 -1
- package/dist/java/index.d.ts +1 -0
- package/dist/java/index.d.ts.map +1 -1
- package/dist/java/index.js +1 -0
- package/dist/java/index.js.map +1 -1
- package/dist/javascript/format.d.ts +2 -14
- package/dist/javascript/format.d.ts.map +1 -1
- package/dist/javascript/format.js +61 -212
- package/dist/javascript/format.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +3 -1
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/tabs-and-indents-visitor.d.ts +25 -0
- package/dist/javascript/tabs-and-indents-visitor.d.ts.map +1 -0
- package/dist/javascript/tabs-and-indents-visitor.js +257 -0
- package/dist/javascript/tabs-and-indents-visitor.js.map +1 -0
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/java/formatting-utils.ts +31 -0
- package/src/java/index.ts +1 -0
- package/src/javascript/format.ts +63 -194
- package/src/javascript/parser.ts +3 -1
- package/src/javascript/tabs-and-indents-visitor.ts +250 -0
package/src/javascript/format.ts
CHANGED
|
@@ -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 {
|
|
16
|
+
import {JS} from "./tree";
|
|
17
17
|
import {JavaScriptVisitor} from "./visitor";
|
|
18
|
-
import {Comment,
|
|
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
|
-
|
|
174
|
-
draft.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/javascript/parser.ts
CHANGED
|
@@ -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:
|
|
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
|
+
}
|