@openrewrite/rewrite 8.69.0-20251207-230203 → 8.69.0-20251208-174001
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 +22 -0
- package/dist/java/formatting-utils.d.ts.map +1 -1
- package/dist/java/formatting-utils.js +60 -0
- package/dist/java/formatting-utils.js.map +1 -1
- package/dist/javascript/format.d.ts +1 -0
- package/dist/javascript/format.d.ts.map +1 -1
- package/dist/javascript/format.js +72 -11
- package/dist/javascript/format.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +12 -2
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/tabs-and-indents-visitor.d.ts +17 -2
- package/dist/javascript/tabs-and-indents-visitor.d.ts.map +1 -1
- package/dist/javascript/tabs-and-indents-visitor.js +339 -91
- package/dist/javascript/tabs-and-indents-visitor.js.map +1 -1
- package/dist/run.d.ts +6 -5
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +36 -37
- package/dist/run.js.map +1 -1
- package/dist/test/rewrite-test.js +5 -5
- package/dist/test/rewrite-test.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/java/formatting-utils.ts +66 -0
- package/src/javascript/format.ts +64 -9
- package/src/javascript/parser.ts +12 -2
- package/src/javascript/tabs-and-indents-visitor.ts +378 -82
- package/src/run.ts +20 -20
- package/src/test/rewrite-test.ts +6 -6
|
@@ -13,12 +13,25 @@
|
|
|
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, JSX} from "./tree";
|
|
17
17
|
import {JavaScriptVisitor} from "./visitor";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
isJava,
|
|
20
|
+
isSpace,
|
|
21
|
+
J,
|
|
22
|
+
lastWhitespace,
|
|
23
|
+
normalizeSpaceIndent,
|
|
24
|
+
replaceIndentAfterLastNewline,
|
|
25
|
+
replaceLastWhitespace,
|
|
26
|
+
spaceContainsNewline,
|
|
27
|
+
stripLeadingIndent
|
|
28
|
+
} from "../java";
|
|
19
29
|
import {produce} from "immer";
|
|
20
|
-
import {Cursor, isScope, Tree} from "../tree";
|
|
30
|
+
import {Cursor, isScope, isTree, Tree} from "../tree";
|
|
31
|
+
import {mapAsync} from "../util";
|
|
32
|
+
import {produceAsync} from "../visitor";
|
|
21
33
|
import {TabsAndIndentsStyle} from "./style";
|
|
34
|
+
import {findMarker} from "../markers";
|
|
22
35
|
|
|
23
36
|
type IndentKind = 'block' | 'continuation' | 'align';
|
|
24
37
|
export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
@@ -48,43 +61,95 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
48
61
|
if (tree.kind === J.Kind.Binary) {
|
|
49
62
|
const hasNewline = tree.prefix?.whitespace?.includes("\n") ||
|
|
50
63
|
tree.prefix?.comments?.some(c => c.suffix.includes("\n"));
|
|
51
|
-
// If Binary has newline
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
cursor.messages.set("leftPaddedContinuation", hasNewline ? 'align' : 'propagate');
|
|
64
|
+
// If Binary has newline OR parent is in align mode, children align
|
|
65
|
+
const shouldAlign = hasNewline || parentIndentKind === 'align';
|
|
66
|
+
cursor.messages.set("indentKind", shouldAlign ? 'align' : 'continuation');
|
|
55
67
|
} else {
|
|
56
68
|
cursor.messages.set("indentKind", this.computeIndentKind(tree));
|
|
57
|
-
cursor.messages.set("leftPaddedContinuation", 'propagate');
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
private getParentIndentContext(cursor: Cursor): [string, IndentKind] {
|
|
73
|
+
// Find the nearest myIndent and the nearest indentKind separately
|
|
74
|
+
// Container/RightPadded/LeftPadded inherit myIndent but don't have indentKind
|
|
75
|
+
let parentIndent: string | undefined;
|
|
76
|
+
let parentKind: IndentKind | undefined;
|
|
77
|
+
|
|
62
78
|
for (let c = cursor.parent; c != null; c = c.parent) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
if (parentIndent === undefined) {
|
|
80
|
+
const indent = c.messages.get("myIndent") as string | undefined;
|
|
81
|
+
if (indent !== undefined) {
|
|
82
|
+
parentIndent = indent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (parentKind === undefined) {
|
|
87
|
+
const kind = c.messages.get("indentKind") as IndentKind | undefined;
|
|
88
|
+
if (kind !== undefined) {
|
|
89
|
+
parentKind = kind;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Found both, we can stop
|
|
94
|
+
if (parentIndent !== undefined && parentKind !== undefined) {
|
|
95
|
+
break;
|
|
67
96
|
}
|
|
68
97
|
}
|
|
69
|
-
|
|
98
|
+
|
|
99
|
+
return [parentIndent ?? "", parentKind ?? 'continuation'];
|
|
70
100
|
}
|
|
71
101
|
|
|
72
102
|
private computeMyIndent(tree: J, parentMyIndent: string, parentIndentKind: IndentKind): string {
|
|
103
|
+
// CompilationUnit is the root - it always has myIndent="" regardless of prefix content
|
|
104
|
+
if (tree.kind === JS.Kind.CompilationUnit) {
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
73
107
|
if (tree.kind === J.Kind.IfElse || parentIndentKind === 'align') {
|
|
74
108
|
return parentMyIndent;
|
|
75
109
|
}
|
|
76
|
-
if
|
|
77
|
-
|
|
110
|
+
// Only add indent if this element starts on a new line
|
|
111
|
+
// Check both the element's prefix and any Spread marker's prefix
|
|
112
|
+
const hasNewline = this.prefixContainsNewline(tree);
|
|
113
|
+
if (!hasNewline) {
|
|
114
|
+
// Special case for JSX: children of JsxTag don't have newlines in their prefix
|
|
115
|
+
// (newlines are in text Literal nodes), but nested tags should still get block indent
|
|
116
|
+
if (this.isNestedJsxTag(tree)) {
|
|
117
|
+
return parentMyIndent + this.singleIndent;
|
|
118
|
+
}
|
|
119
|
+
return parentMyIndent;
|
|
78
120
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
121
|
+
// Add indent for block children or continuation
|
|
122
|
+
return parentMyIndent + this.singleIndent;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private prefixContainsNewline(tree: J): boolean {
|
|
126
|
+
// Check the element's own prefix
|
|
127
|
+
if (tree.prefix?.whitespace?.includes("\n") ||
|
|
128
|
+
tree.prefix?.comments?.some(c => c.suffix.includes("\n"))) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
// For elements with Spread marker, check the Spread marker's prefix
|
|
132
|
+
const spreadMarker = tree.markers?.markers?.find(m => m.kind === JS.Markers.Spread) as { prefix: J.Space } | undefined;
|
|
133
|
+
if (spreadMarker && spaceContainsNewline(spreadMarker.prefix)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private isNestedJsxTag(tree: J): boolean {
|
|
140
|
+
// Check if this is a JsxTag whose parent is also a JsxTag
|
|
141
|
+
if (tree.kind !== JS.Kind.JsxTag) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const parentTree = this.cursor.parentTree();
|
|
145
|
+
return parentTree !== undefined && parentTree.value.kind === JS.Kind.JsxTag;
|
|
82
146
|
}
|
|
83
147
|
|
|
84
148
|
private computeIndentKind(tree: J): IndentKind {
|
|
85
149
|
switch (tree.kind) {
|
|
86
150
|
case J.Kind.Block:
|
|
87
151
|
case J.Kind.Case:
|
|
152
|
+
case JS.Kind.JsxTag:
|
|
88
153
|
return 'block';
|
|
89
154
|
case JS.Kind.CompilationUnit:
|
|
90
155
|
return 'align';
|
|
@@ -104,109 +169,345 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
104
169
|
}
|
|
105
170
|
|
|
106
171
|
let result = tree;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
172
|
+
|
|
173
|
+
// Check if the element has a Spread marker - if so, normalize its prefix instead
|
|
174
|
+
const spreadMarker = result.markers?.markers?.find(m => m.kind === JS.Markers.Spread) as { prefix: J.Space } | undefined;
|
|
175
|
+
if (spreadMarker && spaceContainsNewline(spreadMarker.prefix)) {
|
|
176
|
+
const normalizedPrefix = normalizeSpaceIndent(spreadMarker.prefix, myIndent);
|
|
177
|
+
if (normalizedPrefix !== spreadMarker.prefix) {
|
|
178
|
+
result = produce(result, draft => {
|
|
179
|
+
const spreadIdx = draft.markers.markers.findIndex(m => m.kind === JS.Markers.Spread);
|
|
180
|
+
if (spreadIdx !== -1) {
|
|
181
|
+
(draft.markers.markers[spreadIdx] as any).prefix = normalizedPrefix;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
} else if (result.prefix && spaceContainsNewline(result.prefix)) {
|
|
186
|
+
// Normalize the entire prefix space including comment suffixes
|
|
187
|
+
const normalizedPrefix = normalizeSpaceIndent(result.prefix, myIndent);
|
|
188
|
+
if (normalizedPrefix !== result.prefix) {
|
|
189
|
+
result = produce(result, draft => {
|
|
190
|
+
draft.prefix = normalizedPrefix;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
111
193
|
}
|
|
112
194
|
|
|
113
195
|
if (result.kind === J.Kind.Block) {
|
|
114
196
|
result = this.normalizeBlockEnd(result as J.Block, myIndent);
|
|
115
|
-
} else if (result.kind ===
|
|
116
|
-
result = this.
|
|
197
|
+
} else if (result.kind === J.Kind.Literal && this.isInsideJsxTag()) {
|
|
198
|
+
result = this.normalizeJsxTextContent(result as J.Literal, myIndent);
|
|
117
199
|
}
|
|
118
200
|
|
|
119
201
|
return result;
|
|
120
202
|
}
|
|
121
203
|
|
|
122
|
-
private
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
204
|
+
private isInsideJsxTag(): boolean {
|
|
205
|
+
const parentTree = this.cursor.parentTree();
|
|
206
|
+
return parentTree !== undefined && parentTree.value.kind === JS.Kind.JsxTag;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private normalizeJsxTextContent(literal: J.Literal, myIndent: string): J.Literal {
|
|
210
|
+
if (!literal.valueSource || !literal.valueSource.includes("\n")) {
|
|
211
|
+
return literal;
|
|
126
212
|
}
|
|
127
|
-
|
|
128
|
-
|
|
213
|
+
|
|
214
|
+
// Check if this literal is the last child of a JsxTag - if so, its trailing whitespace
|
|
215
|
+
// should use the parent tag's indent, not the content indent
|
|
216
|
+
const parentIndent = this.cursor.parentTree()!.messages.get("myIndent") as string | undefined;
|
|
217
|
+
const isLastChild = parentIndent !== undefined && this.isLastChildOfJsxTag(literal);
|
|
218
|
+
|
|
219
|
+
// For JSX text content, the newline is in the value, not the prefix.
|
|
220
|
+
// Since the content IS effectively on a new line, it should get block child indent.
|
|
221
|
+
// myIndent is the parent's indent (because Literal prefix has no newline),
|
|
222
|
+
// so we need to add singleIndent for content lines.
|
|
223
|
+
const contentIndent = myIndent + this.singleIndent;
|
|
224
|
+
|
|
225
|
+
// Split by newlines and normalize each line's indentation
|
|
226
|
+
const lines = literal.valueSource.split('\n');
|
|
227
|
+
const result: string[] = [];
|
|
228
|
+
|
|
229
|
+
for (let i = 0; i < lines.length; i++) {
|
|
230
|
+
if (i === 0) {
|
|
231
|
+
// Content before first newline stays as-is
|
|
232
|
+
result.push(lines[i]);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const content = stripLeadingIndent(lines[i]);
|
|
237
|
+
|
|
238
|
+
if (content === '') {
|
|
239
|
+
// Line has only whitespace (or is empty)
|
|
240
|
+
if (isLastChild && i === lines.length - 1) {
|
|
241
|
+
// Trailing whitespace of last child - use parent indent for closing tag alignment
|
|
242
|
+
result.push(parentIndent!);
|
|
243
|
+
} else if (i < lines.length - 1) {
|
|
244
|
+
// Empty line in the middle (followed by more lines) - keep empty
|
|
245
|
+
result.push('');
|
|
246
|
+
} else {
|
|
247
|
+
// Trailing whitespace of non-last-child - add content indent
|
|
248
|
+
result.push(contentIndent);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// Line has content - add proper indent
|
|
252
|
+
result.push(contentIndent + content);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const normalizedValueSource = result.join('\n');
|
|
257
|
+
if (normalizedValueSource === literal.valueSource) {
|
|
258
|
+
return literal;
|
|
259
|
+
}
|
|
260
|
+
return produce(literal, draft => {
|
|
261
|
+
draft.valueSource = normalizedValueSource;
|
|
129
262
|
});
|
|
130
263
|
}
|
|
131
264
|
|
|
132
|
-
private
|
|
133
|
-
|
|
134
|
-
|
|
265
|
+
private isLastChildOfJsxTag(literal: J.Literal): boolean {
|
|
266
|
+
const parentCursor = this.cursor.parentTree();
|
|
267
|
+
if (parentCursor && parentCursor.value.kind === JS.Kind.JsxTag) {
|
|
268
|
+
const tag = parentCursor.value as JSX.Tag;
|
|
269
|
+
if (tag.children && tag.children.length > 0) {
|
|
270
|
+
const lastChild = tag.children[tag.children.length - 1];
|
|
271
|
+
// Compare by id since object references might differ after transformations
|
|
272
|
+
return lastChild.kind === J.Kind.Literal && (lastChild as J.Literal).id === literal.id;
|
|
273
|
+
}
|
|
135
274
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private normalizeBlockEnd(block: J.Block, myIndent: string): J.Block {
|
|
279
|
+
const effectiveLastWs = lastWhitespace(block.end);
|
|
280
|
+
if (!effectiveLastWs.includes("\n")) {
|
|
281
|
+
return block;
|
|
139
282
|
}
|
|
140
|
-
return produce(
|
|
141
|
-
|
|
142
|
-
lastChildDraft.prefix.whitespace = this.combineIndent(lastChildDraft.prefix.whitespace, myIndent);
|
|
283
|
+
return produce(block, draft => {
|
|
284
|
+
draft.end = replaceLastWhitespace(draft.end, ws => replaceIndentAfterLastNewline(ws, myIndent));
|
|
143
285
|
});
|
|
144
286
|
}
|
|
145
287
|
|
|
146
288
|
public async visitContainer<T extends J>(container: J.Container<T>, p: P): Promise<J.Container<T>> {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
? parentIndent + this.singleIndent
|
|
150
|
-
: parentIndent;
|
|
289
|
+
// Create cursor for this container
|
|
290
|
+
this.cursor = new Cursor(container, this.cursor);
|
|
151
291
|
|
|
152
|
-
|
|
153
|
-
this.
|
|
154
|
-
let ret = await super.visitContainer(container, p);
|
|
155
|
-
if (savedMyIndent !== undefined) {
|
|
156
|
-
this.cursor.messages.set("myIndent", savedMyIndent);
|
|
157
|
-
}
|
|
292
|
+
// Pre-visit hook: set up cursor messages
|
|
293
|
+
this.preVisitContainer(container);
|
|
158
294
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
295
|
+
// Visit children (similar to base visitor but without cursor management)
|
|
296
|
+
let ret = (await produceAsync<J.Container<T>>(container, async draft => {
|
|
297
|
+
draft.before = await this.visitSpace(container.before, p);
|
|
298
|
+
(draft.elements as J.RightPadded<J>[]) = await mapAsync(container.elements, e => this.visitRightPadded(e, p));
|
|
299
|
+
draft.markers = await this.visitMarkers(container.markers, p);
|
|
300
|
+
}))!;
|
|
164
301
|
|
|
165
|
-
|
|
166
|
-
|
|
302
|
+
// Post-visit hook: normalize indentation
|
|
303
|
+
ret = this.postVisitContainer(ret);
|
|
304
|
+
|
|
305
|
+
// Restore cursor
|
|
306
|
+
this.cursor = this.cursor.parent!;
|
|
307
|
+
|
|
308
|
+
return ret;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private preVisitContainer<T extends J>(container: J.Container<T>): void {
|
|
312
|
+
const myIndent = this.cursor.parent?.messages.get("myIndent") as string ?? "";
|
|
313
|
+
this.cursor.messages.set("myIndent", myIndent);
|
|
314
|
+
this.cursor.messages.set("indentKind", 'continuation');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private postVisitContainer<T extends J>(container: J.Container<T>): J.Container<T> {
|
|
318
|
+
const parentIndent = this.cursor.parent?.messages.get("myIndent") as string ?? "";
|
|
319
|
+
|
|
320
|
+
// Normalize the last element's after whitespace (closing delimiter like `)`)
|
|
321
|
+
// The closing delimiter should align with the parent's indent level
|
|
322
|
+
if (container.elements.length > 0) {
|
|
323
|
+
const effectiveLastWs = lastWhitespace(container.elements[container.elements.length - 1].after);
|
|
167
324
|
if (effectiveLastWs.includes("\n")) {
|
|
168
|
-
|
|
325
|
+
return produce(container, draft => {
|
|
169
326
|
const lastDraft = draft.elements[draft.elements.length - 1];
|
|
170
|
-
lastDraft.after = replaceLastWhitespace(lastDraft.after, ws =>
|
|
327
|
+
lastDraft.after = replaceLastWhitespace(lastDraft.after, ws => replaceIndentAfterLastNewline(ws, parentIndent));
|
|
171
328
|
});
|
|
172
329
|
}
|
|
173
330
|
}
|
|
174
331
|
|
|
175
|
-
return
|
|
332
|
+
return container;
|
|
176
333
|
}
|
|
177
334
|
|
|
178
335
|
public async visitLeftPadded<T extends J | J.Space | number | string | boolean>(
|
|
179
336
|
left: J.LeftPadded<T>,
|
|
180
337
|
p: P
|
|
181
338
|
): Promise<J.LeftPadded<T> | undefined> {
|
|
182
|
-
|
|
183
|
-
|
|
339
|
+
// Create cursor for this LeftPadded
|
|
340
|
+
this.cursor = new Cursor(left, this.cursor);
|
|
341
|
+
|
|
342
|
+
// Pre-visit hook: set up cursor messages
|
|
343
|
+
this.preVisitLeftPadded(left);
|
|
344
|
+
|
|
345
|
+
// Visit children (similar to base visitor but without cursor management)
|
|
346
|
+
let ret = await produceAsync<J.LeftPadded<T>>(left, async draft => {
|
|
347
|
+
draft.before = await this.visitSpace(left.before, p);
|
|
348
|
+
if (isTree(left.element)) {
|
|
349
|
+
(draft.element as J) = await this.visitDefined(left.element, p);
|
|
350
|
+
} else if (isSpace(left.element)) {
|
|
351
|
+
(draft.element as J.Space) = await this.visitSpace(left.element, p);
|
|
352
|
+
}
|
|
353
|
+
draft.markers = await this.visitMarkers(left.markers, p);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Post-visit hook: normalize indentation
|
|
357
|
+
ret = this.postVisitLeftPadded(ret);
|
|
358
|
+
|
|
359
|
+
// Restore cursor
|
|
360
|
+
this.cursor = this.cursor.parent!;
|
|
361
|
+
|
|
362
|
+
return ret;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private preVisitLeftPadded<T extends J | J.Space | number | string | boolean>(left: J.LeftPadded<T>): void {
|
|
366
|
+
// Get parent indent from parent cursor
|
|
367
|
+
const parentIndent = this.cursor.parent?.messages.get("myIndent") as string ?? "";
|
|
184
368
|
const hasNewline = left.before.whitespace.includes("\n");
|
|
185
|
-
const shouldPropagate = hasNewline && continuation === 'propagate';
|
|
186
369
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
370
|
+
// Check if parent is a Binary in align mode - if so, don't add continuation indent
|
|
371
|
+
// (Binary sets indentKind='align' when it's already on a continuation line)
|
|
372
|
+
const parentValue = this.cursor.parent?.value;
|
|
373
|
+
const parentIndentKind = this.cursor.parent?.messages.get("indentKind");
|
|
374
|
+
const shouldAlign = parentValue?.kind === J.Kind.Binary && parentIndentKind === 'align';
|
|
375
|
+
|
|
376
|
+
// Compute myIndent INCLUDING continuation if applicable
|
|
377
|
+
// This ensures child elements see the correct parent indent
|
|
378
|
+
let myIndent = parentIndent;
|
|
379
|
+
if (hasNewline && !shouldAlign) {
|
|
380
|
+
myIndent = parentIndent + this.singleIndent;
|
|
191
381
|
}
|
|
192
382
|
|
|
193
|
-
|
|
383
|
+
this.cursor.messages.set("myIndent", myIndent);
|
|
384
|
+
this.cursor.messages.set("hasNewline", hasNewline);
|
|
385
|
+
}
|
|
194
386
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
387
|
+
private postVisitLeftPadded<T extends J | J.Space | number | string | boolean>(
|
|
388
|
+
left: J.LeftPadded<T> | undefined
|
|
389
|
+
): J.LeftPadded<T> | undefined {
|
|
390
|
+
if (left === undefined) {
|
|
391
|
+
return undefined;
|
|
200
392
|
}
|
|
201
393
|
|
|
202
|
-
|
|
203
|
-
|
|
394
|
+
const hasNewline = this.cursor.messages.get("hasNewline") as boolean ?? false;
|
|
395
|
+
if (!hasNewline) {
|
|
396
|
+
return left;
|
|
204
397
|
}
|
|
205
|
-
|
|
206
|
-
|
|
398
|
+
|
|
399
|
+
// Use the myIndent we computed in preVisitLeftPadded (which includes continuation if applicable)
|
|
400
|
+
const targetIndent = this.cursor.messages.get("myIndent") as string ?? "";
|
|
401
|
+
return produce(left, draft => {
|
|
402
|
+
draft.before.whitespace = replaceIndentAfterLastNewline(draft.before.whitespace, targetIndent);
|
|
207
403
|
});
|
|
208
404
|
}
|
|
209
405
|
|
|
406
|
+
public async visitRightPadded<T extends J | boolean>(
|
|
407
|
+
right: J.RightPadded<T>,
|
|
408
|
+
p: P
|
|
409
|
+
): Promise<J.RightPadded<T> | undefined> {
|
|
410
|
+
// Create cursor for this RightPadded
|
|
411
|
+
this.cursor = new Cursor(right, this.cursor);
|
|
412
|
+
|
|
413
|
+
// Pre-visit hook: set up cursor messages, propagate continuation if after has newline
|
|
414
|
+
this.preVisitRightPadded(right);
|
|
415
|
+
|
|
416
|
+
// Visit children (similar to base visitor but without cursor management)
|
|
417
|
+
let ret = await produceAsync<J.RightPadded<T>>(right, async draft => {
|
|
418
|
+
if (isTree(right.element)) {
|
|
419
|
+
(draft.element as J) = await this.visitDefined(right.element, p);
|
|
420
|
+
}
|
|
421
|
+
draft.after = await this.visitSpace(right.after, p);
|
|
422
|
+
draft.markers = await this.visitMarkers(right.markers, p);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Restore cursor
|
|
426
|
+
this.cursor = this.cursor.parent!;
|
|
427
|
+
|
|
428
|
+
if (ret?.element === undefined) {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
return ret;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private preVisitRightPadded<T extends J | boolean>(right: J.RightPadded<T>): void {
|
|
435
|
+
// Get parent indent from parent cursor
|
|
436
|
+
const parentIndent = this.cursor.parent?.messages.get("myIndent") as string ?? "";
|
|
437
|
+
const parentIndentKind = this.cursor.parent?.messages.get("indentKind") as string ?? "";
|
|
438
|
+
|
|
439
|
+
// Check if the `after` has a newline (e.g., in method chains like `db\n .from()`)
|
|
440
|
+
const hasNewline = right.after.whitespace.includes("\n");
|
|
441
|
+
|
|
442
|
+
// Only apply chainedIndent logic for method chains (when parent is a MethodInvocation)
|
|
443
|
+
const parentKind = this.cursor.parent?.value?.kind;
|
|
444
|
+
const isMethodChain = parentKind === J.Kind.MethodInvocation;
|
|
445
|
+
|
|
446
|
+
let myIndent = parentIndent;
|
|
447
|
+
if (hasNewline && isMethodChain) {
|
|
448
|
+
// Search up the cursor hierarchy for an existing chainedIndent (to avoid stacking in method chains)
|
|
449
|
+
const existingChainedIndent = this.cursor.parent?.messages.get("chainedIndent") as string | undefined;
|
|
450
|
+
if (existingChainedIndent === undefined) {
|
|
451
|
+
myIndent = parentIndent + this.singleIndent;
|
|
452
|
+
// Set myIndent on parent so Container siblings and further chain elements use it
|
|
453
|
+
this.cursor.parent?.messages.set("myIndent", myIndent);
|
|
454
|
+
this.cursor.parent?.messages.delete("chainedIndent");
|
|
455
|
+
} else {
|
|
456
|
+
myIndent = existingChainedIndent;
|
|
457
|
+
}
|
|
458
|
+
} else if (parentIndentKind !== 'align' && isJava(right.element) && this.elementPrefixContainsNewline(right.element as J)) {
|
|
459
|
+
myIndent = parentIndent + this.singleIndent;
|
|
460
|
+
// For spread elements with newlines, mark continuation as established
|
|
461
|
+
// This allows subsequent elements on the SAME line to inherit the continuation level
|
|
462
|
+
const element = right.element as J;
|
|
463
|
+
if (this.isSpreadElement(element)) {
|
|
464
|
+
this.cursor.parent?.messages.set("continuationIndent", myIndent);
|
|
465
|
+
}
|
|
466
|
+
} else if (isJava(right.element) && !this.elementPrefixContainsNewline(right.element as J)) {
|
|
467
|
+
// Element has no newline - check if a previous sibling established continuation
|
|
468
|
+
const continuationIndent = this.cursor.parent?.messages.get("continuationIndent") as string | undefined;
|
|
469
|
+
if (continuationIndent !== undefined) {
|
|
470
|
+
myIndent = continuationIndent;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
this.cursor.messages.set("myIndent", myIndent);
|
|
475
|
+
this.cursor.messages.set("indentKind", "align");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private elementPrefixContainsNewline(element: J): boolean {
|
|
479
|
+
// Check the element's own prefix
|
|
480
|
+
if (lastWhitespace(element.prefix).includes("\n")) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
// Also check for Spread marker's prefix
|
|
484
|
+
const spreadMarker = element.markers?.markers?.find(m => m.kind === JS.Markers.Spread) as { prefix: J.Space } | undefined;
|
|
485
|
+
if (spreadMarker && spaceContainsNewline(spreadMarker.prefix)) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Check if an element is a spread - either directly marked with Spread marker,
|
|
493
|
+
* or a PropertyAssignment whose name has a Spread marker (for object spreads).
|
|
494
|
+
*/
|
|
495
|
+
private isSpreadElement(element: J): boolean {
|
|
496
|
+
// Direct spread marker on element
|
|
497
|
+
if (findMarker(element, JS.Markers.Spread)) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
// PropertyAssignment wrapping a spread (for object spread like `...obj`)
|
|
501
|
+
if (element.kind === JS.Kind.PropertyAssignment) {
|
|
502
|
+
const propAssign = element as JS.PropertyAssignment;
|
|
503
|
+
const nameElement = propAssign.name?.element;
|
|
504
|
+
if (findMarker(nameElement, JS.Markers.Spread)) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
|
|
210
511
|
async visit<R extends J>(tree: Tree, p: P, parent?: Cursor): Promise<R | undefined> {
|
|
211
512
|
if (this.cursor?.getNearestMessage("stop") != null) {
|
|
212
513
|
return tree as R;
|
|
@@ -267,14 +568,9 @@ export class TabsAndIndentsVisitor<P> extends JavaScriptVisitor<P> {
|
|
|
267
568
|
}
|
|
268
569
|
|
|
269
570
|
private isActualJNode(v: any): v is J {
|
|
270
|
-
return
|
|
571
|
+
return isJava(v) &&
|
|
271
572
|
v.kind !== J.Kind.Container &&
|
|
272
573
|
v.kind !== J.Kind.LeftPadded &&
|
|
273
574
|
v.kind !== J.Kind.RightPadded;
|
|
274
575
|
}
|
|
275
|
-
|
|
276
|
-
private combineIndent(oldWs: string, newIndent: string): string {
|
|
277
|
-
const lastNewline = oldWs.lastIndexOf("\n");
|
|
278
|
-
return oldWs.substring(0, lastNewline + 1) + newIndent;
|
|
279
|
-
}
|
|
280
576
|
}
|
package/src/run.ts
CHANGED
|
@@ -78,13 +78,14 @@ export type ProgressCallback = (phase: 'parsing' | 'scanning' | 'processing', cu
|
|
|
78
78
|
* Streaming version of scheduleRun that yields results as soon as each file is processed.
|
|
79
79
|
* This allows callers to print diffs immediately and free memory earlier.
|
|
80
80
|
*
|
|
81
|
-
* Accepts either an array or an async iterable of source files.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* Accepts either an array or an async iterable of source files. Files are processed
|
|
82
|
+
* immediately as they're yielded from the iterable, avoiding the need to collect all
|
|
83
|
+
* files into memory before starting work.
|
|
84
84
|
*
|
|
85
|
-
* For scanning recipes,
|
|
85
|
+
* For scanning recipes, each file is scanned immediately as it's pulled from the generator,
|
|
86
|
+
* then stored for the edit phase. The scan phase completes before any results are yielded.
|
|
86
87
|
*
|
|
87
|
-
* @param onProgress Optional callback for progress updates during
|
|
88
|
+
* @param onProgress Optional callback for progress updates during scanning and processing phases.
|
|
88
89
|
*/
|
|
89
90
|
export async function* scheduleRunStreaming(
|
|
90
91
|
recipe: Recipe,
|
|
@@ -96,23 +97,20 @@ export async function* scheduleRunStreaming(
|
|
|
96
97
|
const isScanning = await hasScanningRecipe(recipe);
|
|
97
98
|
|
|
98
99
|
if (isScanning) {
|
|
99
|
-
// For scanning recipes,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
files.push(sf);
|
|
105
|
-
parseCount++;
|
|
106
|
-
onProgress?.('parsing', parseCount, -1, sf.sourcePath); // -1 = unknown total
|
|
107
|
-
}
|
|
108
|
-
}
|
|
100
|
+
// For scanning recipes, pull files from the generator and scan them immediately.
|
|
101
|
+
// Files are stored for the later edit phase.
|
|
102
|
+
const files: SourceFile[] = [];
|
|
103
|
+
const iterable = Array.isArray(before) ? before : before;
|
|
104
|
+
const knownTotal = Array.isArray(before) ? before.length : -1; // -1 = unknown total
|
|
109
105
|
|
|
110
|
-
|
|
106
|
+
// Phase 1: Pull files from generator and scan each immediately
|
|
107
|
+
let scanCount = 0;
|
|
108
|
+
for await (const b of iterable) {
|
|
109
|
+
files.push(b);
|
|
110
|
+
scanCount++;
|
|
111
|
+
onProgress?.('scanning', scanCount, knownTotal, b.sourcePath);
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < files.length; i++) {
|
|
114
|
-
const b = files[i];
|
|
115
|
-
onProgress?.('scanning', i + 1, totalFiles, b.sourcePath);
|
|
113
|
+
// Scan this file immediately
|
|
116
114
|
await recurseRecipeList(recipe, b, async (recipe, b2) => {
|
|
117
115
|
if (recipe instanceof ScanningRecipe) {
|
|
118
116
|
return (await recipe.scanner(recipe.accumulator(cursor, ctx))).visit(b2, ctx, cursor)
|
|
@@ -120,6 +118,8 @@ export async function* scheduleRunStreaming(
|
|
|
120
118
|
});
|
|
121
119
|
}
|
|
122
120
|
|
|
121
|
+
const totalFiles = files.length;
|
|
122
|
+
|
|
123
123
|
// Phase 2: Collect generated files
|
|
124
124
|
const generated = (await recurseRecipeList(recipe, [] as SourceFile[], async (recipe, generated) => {
|
|
125
125
|
if (recipe instanceof ScanningRecipe) {
|
package/src/test/rewrite-test.ts
CHANGED
|
@@ -279,9 +279,9 @@ export function dedent(s: string): string {
|
|
|
279
279
|
const str = start > 0 || end < s.length ? s.slice(start, end) : s;
|
|
280
280
|
const lines = str.split('\n');
|
|
281
281
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
const startLine =
|
|
282
|
+
// Always consider all lines for minIndent calculation
|
|
283
|
+
// If first line has content at column 0, minIndent will be 0 and no dedenting happens
|
|
284
|
+
const startLine = 0;
|
|
285
285
|
|
|
286
286
|
// Find minimum indentation
|
|
287
287
|
let minIndent = Infinity;
|
|
@@ -305,9 +305,9 @@ export function dedent(s: string): string {
|
|
|
305
305
|
return lines.join('\n');
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
// Remove common indentation from lines
|
|
309
|
-
return lines.map(
|
|
310
|
-
|
|
308
|
+
// Remove common indentation from all lines
|
|
309
|
+
return lines.map(line =>
|
|
310
|
+
line.length >= minIndent ? line.slice(minIndent) : ''
|
|
311
311
|
).join('\n');
|
|
312
312
|
}
|
|
313
313
|
|