@openrewrite/rewrite 8.69.0-20251212-132620 → 8.69.0-20251212-150738

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.
@@ -0,0 +1,937 @@
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
+
17
+ import {Cursor, Tree} from "../tree";
18
+ import {Comment, isIdentifier, isJava, isLiteral, J, TextComment} from "../java";
19
+ import {JS} from "./tree";
20
+ import {JavaScriptVisitor} from "./visitor";
21
+ import * as fs from "fs";
22
+
23
+ /**
24
+ * Options for controlling LST debug output.
25
+ */
26
+ export interface LstDebugOptions {
27
+ /** Include cursor messages in output. Default: true */
28
+ includeCursorMessages?: boolean;
29
+ /** Include markers in output. Default: false */
30
+ includeMarkers?: boolean;
31
+ /** Include IDs in output. Default: false */
32
+ includeIds?: boolean;
33
+ /** Maximum depth to traverse. Default: unlimited (-1) */
34
+ maxDepth?: number;
35
+ /** Properties to always exclude (in addition to defaults). */
36
+ excludeProperties?: string[];
37
+ /** Output destination: 'console' or a file path. Default: 'console' */
38
+ output?: 'console' | string;
39
+ /** Indent string for nested output. Default: ' ' */
40
+ indent?: string;
41
+ }
42
+
43
+ const DEFAULT_OPTIONS: Required<LstDebugOptions> = {
44
+ includeCursorMessages: true,
45
+ includeMarkers: false,
46
+ includeIds: false,
47
+ maxDepth: -1,
48
+ excludeProperties: [],
49
+ output: 'console',
50
+ indent: ' ',
51
+ };
52
+
53
+ /**
54
+ * Properties to always exclude from debug output (noisy/large).
55
+ */
56
+ const EXCLUDED_PROPERTIES = new Set([
57
+ 'type', // JavaType - very verbose
58
+ 'methodType', // JavaType.Method
59
+ 'variableType', // JavaType.Variable
60
+ 'fieldType', // JavaType.Variable
61
+ 'constructorType', // JavaType.Method
62
+ ]);
63
+
64
+ /**
65
+ * Subscript digits for counts.
66
+ */
67
+ const SUBSCRIPTS = ['', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
68
+
69
+ /**
70
+ * Format a count as subscript. Count of 1 is implicit (empty string).
71
+ */
72
+ function subscript(count: number): string {
73
+ if (count <= 1) return '';
74
+ if (count < 10) return SUBSCRIPTS[count];
75
+ // For counts >= 10, build from individual digits
76
+ return String(count).split('').map(d => SUBSCRIPTS[parseInt(d)]).join('');
77
+ }
78
+
79
+ /**
80
+ * Format whitespace string for readable debug output.
81
+ * Uses compact notation with subscript counts:
82
+ * - '\n' = 1 newline (implicit ₁)
83
+ * - '\n₂' = 2 newlines
84
+ * - '·₄' = 4 spaces
85
+ * - '-₂' = 2 tabs
86
+ * - '\n·₄' = newline + 4 spaces
87
+ * - '\n·₄-₂' = newline + 4 spaces + 2 tabs
88
+ */
89
+ export function formatWhitespace(whitespace: string | undefined): string {
90
+ if (whitespace === undefined || whitespace === '') {
91
+ return "''";
92
+ }
93
+
94
+ let result = '';
95
+ let newlineCount = 0;
96
+ let spaceCount = 0;
97
+ let tabCount = 0;
98
+
99
+ const flushCounts = () => {
100
+ if (newlineCount > 0) {
101
+ result += '\\n' + subscript(newlineCount);
102
+ newlineCount = 0;
103
+ }
104
+ if (spaceCount > 0) {
105
+ result += '·' + subscript(spaceCount);
106
+ spaceCount = 0;
107
+ }
108
+ if (tabCount > 0) {
109
+ result += '-' + subscript(tabCount);
110
+ tabCount = 0;
111
+ }
112
+ };
113
+
114
+ for (let i = 0; i < whitespace.length; i++) {
115
+ const c = whitespace[i];
116
+ if (c === '\n') {
117
+ // Flush spaces/tabs before starting newline count
118
+ if (spaceCount > 0 || tabCount > 0) {
119
+ flushCounts();
120
+ }
121
+ newlineCount++;
122
+ } else if (c === '\r') {
123
+ flushCounts();
124
+ result += '\\r';
125
+ } else if (c === ' ') {
126
+ // Flush newlines and tabs before counting spaces
127
+ if (newlineCount > 0) {
128
+ result += '\\n' + subscript(newlineCount);
129
+ newlineCount = 0;
130
+ }
131
+ if (tabCount > 0) {
132
+ result += '-' + subscript(tabCount);
133
+ tabCount = 0;
134
+ }
135
+ spaceCount++;
136
+ } else if (c === '\t') {
137
+ // Flush newlines and spaces before counting tabs
138
+ if (newlineCount > 0) {
139
+ result += '\\n' + subscript(newlineCount);
140
+ newlineCount = 0;
141
+ }
142
+ if (spaceCount > 0) {
143
+ result += '·' + subscript(spaceCount);
144
+ spaceCount = 0;
145
+ }
146
+ tabCount++;
147
+ } else {
148
+ flushCounts();
149
+ // Unexpected character (probably a bug in the parser)
150
+ result += c;
151
+ }
152
+ }
153
+
154
+ flushCounts();
155
+
156
+ return `'${result}'`;
157
+ }
158
+
159
+ /**
160
+ * Format a single comment for debug output.
161
+ */
162
+ function formatComment(comment: Comment): string {
163
+ const textComment = comment as TextComment;
164
+ const text = textComment.text ?? '';
165
+ // Truncate long comments
166
+ const truncated = text.length > 20 ? text.substring(0, 17) + '...' : text;
167
+
168
+ if (textComment.multiline) {
169
+ return `/*${truncated}*/`;
170
+ } else {
171
+ return `//${truncated}`;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Format a J.Space for debug output.
177
+ *
178
+ * Compact format:
179
+ * - Empty space: `''`
180
+ * - Whitespace only: `'\n·₄'`
181
+ * - Comment only: `//comment`
182
+ * - Comment with suffix: `//comment'\n'`
183
+ * - Multiple comments: `//c1'\n' + //c2'\n·₄'`
184
+ */
185
+ export function formatSpace(space: J.Space | undefined): string {
186
+ if (space === undefined) {
187
+ return '<undefined>';
188
+ }
189
+
190
+ const hasComments = space.comments.length > 0;
191
+ const hasWhitespace = space.whitespace !== undefined && space.whitespace !== '';
192
+
193
+ // Completely empty
194
+ if (!hasComments && !hasWhitespace) {
195
+ return "''";
196
+ }
197
+
198
+ // Whitespace only (common case) - just show the formatted whitespace
199
+ if (!hasComments) {
200
+ return formatWhitespace(space.whitespace);
201
+ }
202
+
203
+ // Format comments with their suffixes
204
+ const parts: string[] = [];
205
+ for (const comment of space.comments) {
206
+ let part = formatComment(comment);
207
+ // Add suffix if present
208
+ if (comment.suffix && comment.suffix !== '') {
209
+ part += formatWhitespace(comment.suffix);
210
+ }
211
+ parts.push(part);
212
+ }
213
+
214
+ // Add trailing whitespace if present
215
+ if (hasWhitespace) {
216
+ parts.push(formatWhitespace(space.whitespace));
217
+ }
218
+
219
+ return parts.join(' + ');
220
+ }
221
+
222
+ /**
223
+ * Format cursor messages for debug output.
224
+ * Returns '<no messages>' if no messages, otherwise returns '⟨key=value, ...⟩'
225
+ */
226
+ export function formatCursorMessages(cursor: Cursor | undefined): string {
227
+ if (!cursor || cursor.messages.size === 0) {
228
+ return '<no messages>';
229
+ }
230
+
231
+ const entries: string[] = [];
232
+ cursor.messages.forEach((value, key) => {
233
+ let valueStr: string;
234
+ if (Array.isArray(value)) {
235
+ valueStr = `[${value.map(v => JSON.stringify(v)).join(', ')}]`;
236
+ } else if (typeof value === 'object' && value !== null) {
237
+ valueStr = JSON.stringify(value);
238
+ } else {
239
+ valueStr = String(value);
240
+ }
241
+ const keyStr = typeof key === 'symbol' ? key.toString() : String(key);
242
+ entries.push(`${keyStr}=${valueStr}`);
243
+ });
244
+
245
+ return `⟨${entries.join(', ')}⟩`;
246
+ }
247
+
248
+ /**
249
+ * Get a short type name from a kind string.
250
+ */
251
+ function shortTypeName(kind: string | undefined): string {
252
+ if (!kind) return 'Unknown';
253
+ // Extract the last part after the last dot
254
+ const lastDot = kind.lastIndexOf('.');
255
+ return lastDot >= 0 ? kind.substring(lastDot + 1) : kind;
256
+ }
257
+
258
+ /**
259
+ * Find which property of the parent contains the given child element.
260
+ * Returns the property name, or property name with array index if in an array.
261
+ * Returns undefined if the relationship cannot be determined.
262
+ *
263
+ * @param cursor - The cursor at the current position
264
+ * @param child - Optional: the actual child node being visited (for RightPadded/LeftPadded/Container visits where cursor.value is the parent)
265
+ */
266
+ export function findPropertyPath(cursor: Cursor | undefined, child?: any): string | undefined {
267
+ if (!cursor) {
268
+ return undefined;
269
+ }
270
+
271
+ // If child is provided, use cursor.value as parent; otherwise use cursor.parent.value
272
+ const actualChild = child ?? cursor.value;
273
+ const parent = child ? cursor.value : cursor.parent?.value;
274
+
275
+ if (!parent || typeof parent !== 'object') {
276
+ return undefined;
277
+ }
278
+
279
+ // Properties to skip when searching
280
+ const skipProps = new Set(['kind', 'id', 'prefix', 'markers', 'type', 'methodType', 'variableType', 'fieldType', 'constructorType']);
281
+
282
+ // Special case: if parent is a Container, we need to look at grandparent to find the property name
283
+ if ((parent as any).kind === J.Kind.Container) {
284
+ const container = parent as J.Container<any>;
285
+ const grandparent = child ? cursor.parent?.value : cursor.parent?.parent?.value;
286
+
287
+ // Find the index of actualChild in container.elements
288
+ let childIndex = -1;
289
+ if (container.elements) {
290
+ for (let i = 0; i < container.elements.length; i++) {
291
+ if (container.elements[i] === actualChild) {
292
+ childIndex = i;
293
+ break;
294
+ }
295
+ }
296
+ }
297
+
298
+ // Find which property of grandparent holds this container
299
+ if (grandparent && typeof grandparent === 'object') {
300
+ for (const [key, value] of Object.entries(grandparent)) {
301
+ if (skipProps.has(key)) continue;
302
+ if (value === parent) {
303
+ if (childIndex >= 0) {
304
+ return `${key}[${childIndex}]`;
305
+ }
306
+ return key;
307
+ }
308
+ }
309
+ }
310
+
311
+ // Fallback: just show the index
312
+ if (childIndex >= 0) {
313
+ return `[${childIndex}]`;
314
+ }
315
+ }
316
+
317
+ for (const [key, value] of Object.entries(parent)) {
318
+ if (skipProps.has(key)) continue;
319
+
320
+ // Direct match
321
+ if (value === actualChild) {
322
+ return key;
323
+ }
324
+
325
+ // Check if child is in an array
326
+ if (Array.isArray(value)) {
327
+ for (let i = 0; i < value.length; i++) {
328
+ if (value[i] === actualChild) {
329
+ return `${key}[${i}]`;
330
+ }
331
+ // Check inside RightPadded/LeftPadded wrappers
332
+ if (value[i] && typeof value[i] === 'object') {
333
+ if (value[i].element === actualChild) {
334
+ return `${key}[${i}].element`;
335
+ }
336
+ }
337
+ }
338
+ }
339
+
340
+ // Check inside Container
341
+ if (value && typeof value === 'object' && (value as any).kind === J.Kind.Container) {
342
+ const container = value as J.Container<any>;
343
+ if (container.elements) {
344
+ for (let i = 0; i < container.elements.length; i++) {
345
+ const rp = container.elements[i];
346
+ if (rp === actualChild) {
347
+ return `${key}[${i}]`;
348
+ }
349
+ if (rp.element === actualChild) {
350
+ return `${key}[${i}].element`;
351
+ }
352
+ }
353
+ }
354
+ }
355
+
356
+ // Check inside LeftPadded/RightPadded
357
+ if (value && typeof value === 'object') {
358
+ if ((value as any).kind === J.Kind.LeftPadded || (value as any).kind === J.Kind.RightPadded) {
359
+ if ((value as any).element === actualChild) {
360
+ return `${key}.element`;
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ return undefined;
367
+ }
368
+
369
+ /**
370
+ * Format a literal value for inline display.
371
+ */
372
+ function formatLiteralValue(lit: J.Literal): string {
373
+ if (lit.valueSource !== undefined) {
374
+ // Truncate long literals
375
+ return lit.valueSource.length > 20
376
+ ? lit.valueSource.substring(0, 17) + '...'
377
+ : lit.valueSource;
378
+ }
379
+ return String(lit.value);
380
+ }
381
+
382
+ /**
383
+ * Get a compact inline summary for certain node types.
384
+ * - For Identifier: shows "simpleName"
385
+ * - For Literal: shows the value
386
+ * - For other nodes: shows all Identifier/Literal properties inline
387
+ */
388
+ function getNodeSummary(node: J): string | undefined {
389
+ if (isIdentifier(node)) {
390
+ return `"${node.simpleName}"`;
391
+ }
392
+ if (isLiteral(node)) {
393
+ return formatLiteralValue(node);
394
+ }
395
+
396
+ // For other nodes, find all Identifier and Literal properties
397
+ const parts: string[] = [];
398
+ const skipProps = new Set(['kind', 'id', 'prefix', 'markers', 'type', 'methodType', 'variableType', 'fieldType', 'constructorType']);
399
+
400
+ for (const [key, value] of Object.entries(node)) {
401
+ if (skipProps.has(key)) continue;
402
+ if (value === null || value === undefined) continue;
403
+
404
+ if (isIdentifier(value)) {
405
+ parts.push(`${key}='${value.simpleName}'`);
406
+ } else if (isLiteral(value)) {
407
+ parts.push(`${key}=${formatLiteralValue(value)}`);
408
+ }
409
+ }
410
+
411
+ return parts.length > 0 ? parts.join(' ') : undefined;
412
+ }
413
+
414
+ /**
415
+ * LST Debug Printer - prints LST nodes in a readable format.
416
+ *
417
+ * Usage from within a visitor:
418
+ * ```typescript
419
+ * class MyVisitor extends JavaScriptVisitor<P> {
420
+ * private debug = new LstDebugPrinter({ includeCursorMessages: true });
421
+ *
422
+ * async visitMethodInvocation(mi: J.MethodInvocation, p: P) {
423
+ * this.debug.print(mi, this.cursor);
424
+ * return super.visitMethodInvocation(mi, p);
425
+ * }
426
+ * }
427
+ * ```
428
+ */
429
+ export class LstDebugPrinter {
430
+ private readonly options: Required<LstDebugOptions>;
431
+ private outputLines: string[] = [];
432
+
433
+ constructor(options: LstDebugOptions = {}) {
434
+ this.options = {...DEFAULT_OPTIONS, ...options};
435
+ }
436
+
437
+ /**
438
+ * Print a tree node with optional cursor context.
439
+ * @param tree The tree node to print
440
+ * @param cursor Optional cursor for context
441
+ * @param label Optional label to identify this debug output (shown as comment before output)
442
+ */
443
+ print(tree: Tree | J.Container<any> | J.LeftPadded<any> | J.RightPadded<any>, cursor?: Cursor, label?: string): void {
444
+ this.outputLines = [];
445
+ if (label) {
446
+ this.outputLines.push(`// ${label}`);
447
+ }
448
+ this.printNode(tree, cursor, 0);
449
+ this.flush();
450
+ }
451
+
452
+ /**
453
+ * Print the cursor path from root to current position.
454
+ */
455
+ printCursorPath(cursor: Cursor): void {
456
+ this.outputLines = [];
457
+ this.outputLines.push('=== Cursor Path ===');
458
+
459
+ const path: Cursor[] = [];
460
+ for (let c: Cursor | undefined = cursor; c; c = c.parent) {
461
+ path.unshift(c);
462
+ }
463
+
464
+ for (let i = 0; i < path.length; i++) {
465
+ const c = path[i];
466
+ const indent = this.options.indent.repeat(i);
467
+ const kind = (c.value as any)?.kind;
468
+ const typeName = shortTypeName(kind);
469
+
470
+ let line = `${indent}${typeName}`;
471
+ if (this.options.includeCursorMessages && c.messages.size > 0) {
472
+ line += ` [${formatCursorMessages(c)}]`;
473
+ }
474
+ this.outputLines.push(line);
475
+ }
476
+
477
+ this.flush();
478
+ }
479
+
480
+ private printNode(
481
+ node: any,
482
+ cursor: Cursor | undefined,
483
+ depth: number
484
+ ): void {
485
+ if (this.options.maxDepth >= 0 && depth > this.options.maxDepth) {
486
+ this.outputLines.push(`${this.indent(depth)}...`);
487
+ return;
488
+ }
489
+
490
+ if (node === null || node === undefined) {
491
+ this.outputLines.push(`${this.indent(depth)}null`);
492
+ return;
493
+ }
494
+
495
+ // Handle special types
496
+ if (this.isSpace(node)) {
497
+ this.outputLines.push(`${this.indent(depth)}${formatSpace(node)}`);
498
+ return;
499
+ }
500
+
501
+ if (this.isContainer(node)) {
502
+ this.printContainer(node, cursor, depth);
503
+ return;
504
+ }
505
+
506
+ if (this.isLeftPadded(node)) {
507
+ this.printLeftPadded(node, cursor, depth);
508
+ return;
509
+ }
510
+
511
+ if (this.isRightPadded(node)) {
512
+ this.printRightPadded(node, cursor, depth);
513
+ return;
514
+ }
515
+
516
+ if (isJava(node)) {
517
+ this.printJavaNode(node, cursor, depth);
518
+ return;
519
+ }
520
+
521
+ // Primitive or unknown type
522
+ if (typeof node !== 'object') {
523
+ this.outputLines.push(`${this.indent(depth)}${JSON.stringify(node)}`);
524
+ return;
525
+ }
526
+
527
+ // Generic object - print as JSON-like
528
+ this.printGenericObject(node, depth);
529
+ }
530
+
531
+ private printJavaNode(node: J, cursor: Cursor | undefined, depth: number): void {
532
+ const typeName = shortTypeName(node.kind);
533
+ let header = `${this.indent(depth)}${typeName}`;
534
+
535
+ // Add inline summary for certain types (Identifier, Literal)
536
+ const summary = getNodeSummary(node);
537
+ if (summary) {
538
+ header += ` ${summary}`;
539
+ }
540
+
541
+ // Add cursor messages if available
542
+ if (this.options.includeCursorMessages && cursor) {
543
+ const messages = formatCursorMessages(cursor);
544
+ if (messages !== '<no messages>') {
545
+ header += ` [${messages}]`;
546
+ }
547
+ }
548
+
549
+ // Add ID if requested
550
+ if (this.options.includeIds && node.id) {
551
+ header += ` (id=${node.id.substring(0, 8)}...)`;
552
+ }
553
+
554
+ this.outputLines.push(header);
555
+
556
+ // Print prefix
557
+ if (node.prefix) {
558
+ this.outputLines.push(`${this.indent(depth + 1)}prefix: ${formatSpace(node.prefix)}`);
559
+ }
560
+
561
+ // Print markers if requested
562
+ if (this.options.includeMarkers && node.markers?.markers?.length > 0) {
563
+ this.outputLines.push(`${this.indent(depth + 1)}markers: [${node.markers.markers.map((m: any) => shortTypeName(m.kind)).join(', ')}]`);
564
+ }
565
+
566
+ // Print other properties
567
+ this.printNodeProperties(node, cursor, depth + 1);
568
+ }
569
+
570
+ private printNodeProperties(node: any, cursor: Cursor | undefined, depth: number): void {
571
+ const excludedProps = new Set([
572
+ ...EXCLUDED_PROPERTIES,
573
+ ...this.options.excludeProperties,
574
+ 'kind',
575
+ 'id',
576
+ 'prefix',
577
+ 'markers',
578
+ ]);
579
+
580
+ for (const [key, value] of Object.entries(node)) {
581
+ if (excludedProps.has(key)) continue;
582
+ if (value === undefined || value === null) continue;
583
+
584
+ if (this.isSpace(value)) {
585
+ this.outputLines.push(`${this.indent(depth)}${key}: ${formatSpace(value)}`);
586
+ } else if (this.isContainer(value)) {
587
+ this.outputLines.push(`${this.indent(depth)}${key}:`);
588
+ this.printContainer(value, undefined, depth + 1);
589
+ } else if (this.isLeftPadded(value)) {
590
+ this.outputLines.push(`${this.indent(depth)}${key}:`);
591
+ this.printLeftPadded(value, undefined, depth + 1);
592
+ } else if (this.isRightPadded(value)) {
593
+ this.outputLines.push(`${this.indent(depth)}${key}:`);
594
+ this.printRightPadded(value, undefined, depth + 1);
595
+ } else if (Array.isArray(value)) {
596
+ if (value.length === 0) {
597
+ this.outputLines.push(`${this.indent(depth)}${key}: []`);
598
+ } else if (value.every(v => this.isRightPadded(v))) {
599
+ this.outputLines.push(`${this.indent(depth)}${key}: [${value.length} RightPadded elements]`);
600
+ for (let i = 0; i < value.length; i++) {
601
+ this.outputLines.push(`${this.indent(depth + 1)}[${i}]:`);
602
+ this.printRightPadded(value[i], undefined, depth + 2);
603
+ }
604
+ } else if (value.every(v => isJava(v))) {
605
+ this.outputLines.push(`${this.indent(depth)}${key}: [${value.length} elements]`);
606
+ for (let i = 0; i < value.length; i++) {
607
+ this.outputLines.push(`${this.indent(depth + 1)}[${i}]:`);
608
+ this.printNode(value[i], undefined, depth + 2);
609
+ }
610
+ } else {
611
+ this.outputLines.push(`${this.indent(depth)}${key}: [${value.length} items]`);
612
+ }
613
+ } else if (isJava(value)) {
614
+ this.outputLines.push(`${this.indent(depth)}${key}:`);
615
+ this.printNode(value, undefined, depth + 1);
616
+ } else if (typeof value === 'object') {
617
+ // Skip complex objects that aren't J nodes
618
+ this.outputLines.push(`${this.indent(depth)}${key}: <object>`);
619
+ } else {
620
+ this.outputLines.push(`${this.indent(depth)}${key}: ${JSON.stringify(value)}`);
621
+ }
622
+ }
623
+ }
624
+
625
+ private printContainer(container: J.Container<any>, cursor: Cursor | undefined, depth: number): void {
626
+ const elemCount = container.elements?.length ?? 0;
627
+ let header = `${this.indent(depth)}Container<${elemCount} elements>`;
628
+
629
+ if (this.options.includeCursorMessages && cursor) {
630
+ const messages = formatCursorMessages(cursor);
631
+ if (messages !== '<no messages>') {
632
+ header += ` [${messages}]`;
633
+ }
634
+ }
635
+
636
+ this.outputLines.push(header);
637
+ this.outputLines.push(`${this.indent(depth + 1)}before: ${formatSpace(container.before)}`);
638
+
639
+ if (container.elements && container.elements.length > 0) {
640
+ this.outputLines.push(`${this.indent(depth + 1)}elements:`);
641
+ for (let i = 0; i < container.elements.length; i++) {
642
+ this.outputLines.push(`${this.indent(depth + 2)}[${i}]:`);
643
+ this.printRightPadded(container.elements[i], undefined, depth + 3);
644
+ }
645
+ }
646
+ }
647
+
648
+ private printLeftPadded(lp: J.LeftPadded<any>, cursor: Cursor | undefined, depth: number): void {
649
+ let header = `${this.indent(depth)}LeftPadded`;
650
+
651
+ if (this.options.includeCursorMessages && cursor) {
652
+ const messages = formatCursorMessages(cursor);
653
+ if (messages !== '<no messages>') {
654
+ header += ` [${messages}]`;
655
+ }
656
+ }
657
+
658
+ this.outputLines.push(header);
659
+ this.outputLines.push(`${this.indent(depth + 1)}before: ${formatSpace(lp.before)}`);
660
+
661
+ if (lp.element !== undefined) {
662
+ if (isJava(lp.element)) {
663
+ this.outputLines.push(`${this.indent(depth + 1)}element:`);
664
+ this.printNode(lp.element, undefined, depth + 2);
665
+ } else if (this.isSpace(lp.element)) {
666
+ this.outputLines.push(`${this.indent(depth + 1)}element: ${formatSpace(lp.element)}`);
667
+ } else {
668
+ this.outputLines.push(`${this.indent(depth + 1)}element: ${JSON.stringify(lp.element)}`);
669
+ }
670
+ }
671
+ }
672
+
673
+ private printRightPadded(rp: J.RightPadded<any>, cursor: Cursor | undefined, depth: number): void {
674
+ let header = `${this.indent(depth)}RightPadded`;
675
+
676
+ if (this.options.includeCursorMessages && cursor) {
677
+ const messages = formatCursorMessages(cursor);
678
+ if (messages !== '<no messages>') {
679
+ header += ` [${messages}]`;
680
+ }
681
+ }
682
+
683
+ this.outputLines.push(header);
684
+
685
+ if (rp.element !== undefined) {
686
+ if (isJava(rp.element)) {
687
+ this.outputLines.push(`${this.indent(depth + 1)}element:`);
688
+ this.printNode(rp.element, undefined, depth + 2);
689
+ } else {
690
+ this.outputLines.push(`${this.indent(depth + 1)}element: ${JSON.stringify(rp.element)}`);
691
+ }
692
+ }
693
+
694
+ this.outputLines.push(`${this.indent(depth + 1)}after: ${formatSpace(rp.after)}`);
695
+ }
696
+
697
+ private printGenericObject(obj: any, depth: number): void {
698
+ this.outputLines.push(`${this.indent(depth)}{`);
699
+ for (const [key, value] of Object.entries(obj)) {
700
+ if (typeof value === 'object' && value !== null) {
701
+ this.outputLines.push(`${this.indent(depth + 1)}${key}: <object>`);
702
+ } else {
703
+ this.outputLines.push(`${this.indent(depth + 1)}${key}: ${JSON.stringify(value)}`);
704
+ }
705
+ }
706
+ this.outputLines.push(`${this.indent(depth)}}`);
707
+ }
708
+
709
+ private isSpace(value: any): value is J.Space {
710
+ return value !== null &&
711
+ typeof value === 'object' &&
712
+ 'whitespace' in value &&
713
+ 'comments' in value &&
714
+ !('kind' in value);
715
+ }
716
+
717
+ private isContainer(value: any): value is J.Container<any> {
718
+ return value !== null &&
719
+ typeof value === 'object' &&
720
+ value.kind === J.Kind.Container;
721
+ }
722
+
723
+ private isLeftPadded(value: any): value is J.LeftPadded<any> {
724
+ return value !== null &&
725
+ typeof value === 'object' &&
726
+ value.kind === J.Kind.LeftPadded;
727
+ }
728
+
729
+ private isRightPadded(value: any): value is J.RightPadded<any> {
730
+ return value !== null &&
731
+ typeof value === 'object' &&
732
+ value.kind === J.Kind.RightPadded;
733
+ }
734
+
735
+ private indent(depth: number): string {
736
+ return this.options.indent.repeat(depth);
737
+ }
738
+
739
+ private flush(): void {
740
+ const output = this.outputLines.join('\n');
741
+
742
+ if (this.options.output === 'console') {
743
+ console.info(output);
744
+ } else {
745
+ fs.appendFileSync(this.options.output, output + '\n\n');
746
+ }
747
+
748
+ this.outputLines = [];
749
+ }
750
+ }
751
+
752
+ /**
753
+ * A visitor that prints the LST structure as it traverses.
754
+ * Useful for debugging the entire tree or a subtree.
755
+ *
756
+ * Usage:
757
+ * ```typescript
758
+ * const debugVisitor = new LstDebugVisitor({ maxDepth: 3 });
759
+ * await debugVisitor.visit(tree, ctx);
760
+ * ```
761
+ */
762
+ export class LstDebugVisitor<P> extends JavaScriptVisitor<P> {
763
+ private readonly printer: LstDebugPrinter;
764
+ private readonly printPreVisit: boolean;
765
+ private readonly printPostVisit: boolean;
766
+ private depth = 0;
767
+
768
+ constructor(
769
+ options: LstDebugOptions = {},
770
+ config: { printPreVisit?: boolean; printPostVisit?: boolean } = {}
771
+ ) {
772
+ super();
773
+ this.printer = new LstDebugPrinter(options);
774
+ this.printPreVisit = config.printPreVisit ?? true;
775
+ this.printPostVisit = config.printPostVisit ?? false;
776
+ }
777
+
778
+ protected async preVisit(tree: J, _p: P): Promise<J | undefined> {
779
+ if (this.printPreVisit) {
780
+ const typeName = shortTypeName(tree.kind);
781
+ const indent = ' '.repeat(this.depth);
782
+ const messages = formatCursorMessages(this.cursor);
783
+ const prefix = formatSpace(tree.prefix);
784
+ const summary = getNodeSummary(tree);
785
+ const propPath = findPropertyPath(this.cursor);
786
+
787
+ let line = indent;
788
+ if (propPath) {
789
+ line += `${propPath}: `;
790
+ }
791
+ line += `${typeName}{`;
792
+ if (summary) {
793
+ line += `${summary} `;
794
+ }
795
+ line += `prefix=${prefix}}`;
796
+
797
+ console.info(line);
798
+
799
+ // Show cursor messages on a separate indented line
800
+ if (messages !== '<no messages>') {
801
+ console.info(`${indent} ⤷ ${messages}`);
802
+ }
803
+ }
804
+ this.depth++;
805
+ return tree;
806
+ }
807
+
808
+ protected async postVisit(tree: J, _p: P): Promise<J | undefined> {
809
+ this.depth--;
810
+ if (this.printPostVisit) {
811
+ const typeName = shortTypeName(tree.kind);
812
+ const indent = ' '.repeat(this.depth);
813
+ console.info(`${indent}← ${typeName}`);
814
+ }
815
+ return tree;
816
+ }
817
+
818
+ public async visitContainer<T extends J>(container: J.Container<T>, p: P): Promise<J.Container<T>> {
819
+ if (this.printPreVisit) {
820
+ const indent = ' '.repeat(this.depth);
821
+ const messages = formatCursorMessages(this.cursor);
822
+ const before = formatSpace(container.before);
823
+ // Pass container as the child since cursor.value is the parent node
824
+ const propPath = findPropertyPath(this.cursor, container);
825
+
826
+ let line = indent;
827
+ if (propPath) {
828
+ line += `${propPath}: `;
829
+ }
830
+ line += `Container<${container.elements.length}>{before=${before}}`;
831
+
832
+ console.info(line);
833
+
834
+ if (messages !== '<no messages>') {
835
+ console.info(`${indent} ⤷ ${messages}`);
836
+ }
837
+ }
838
+ this.depth++;
839
+ const result = await super.visitContainer(container, p);
840
+ this.depth--;
841
+ return result;
842
+ }
843
+
844
+ public async visitLeftPadded<T extends J | J.Space | number | string | boolean>(
845
+ left: J.LeftPadded<T>,
846
+ p: P
847
+ ): Promise<J.LeftPadded<T> | undefined> {
848
+ if (this.printPreVisit) {
849
+ const indent = ' '.repeat(this.depth);
850
+ const messages = formatCursorMessages(this.cursor);
851
+ const before = formatSpace(left.before);
852
+ // Pass left as the child since cursor.value is the parent node
853
+ const propPath = findPropertyPath(this.cursor, left);
854
+
855
+ let line = indent;
856
+ if (propPath) {
857
+ line += `${propPath}: `;
858
+ }
859
+ line += `LeftPadded{before=${before}`;
860
+
861
+ // Show element value if it's a primitive (string, number, boolean)
862
+ if (left.element !== null && left.element !== undefined) {
863
+ const elemType = typeof left.element;
864
+ if (elemType === 'string' || elemType === 'number' || elemType === 'boolean') {
865
+ line += ` element=${JSON.stringify(left.element)}`;
866
+ }
867
+ }
868
+ line += '}';
869
+
870
+ console.info(line);
871
+
872
+ if (messages !== '<no messages>') {
873
+ console.info(`${indent} ⤷ ${messages}`);
874
+ }
875
+ }
876
+ this.depth++;
877
+ const result = await super.visitLeftPadded(left, p);
878
+ this.depth--;
879
+ return result;
880
+ }
881
+
882
+ public async visitRightPadded<T extends J | boolean>(
883
+ right: J.RightPadded<T>,
884
+ p: P
885
+ ): Promise<J.RightPadded<T> | undefined> {
886
+ if (this.printPreVisit) {
887
+ const indent = ' '.repeat(this.depth);
888
+ const messages = formatCursorMessages(this.cursor);
889
+ const after = formatSpace(right.after);
890
+ // Pass right as the child since cursor.value is the parent node
891
+ const propPath = findPropertyPath(this.cursor, right);
892
+
893
+ let line = indent;
894
+ if (propPath) {
895
+ line += `${propPath}: `;
896
+ }
897
+ line += `RightPadded{after=${after}`;
898
+
899
+ // Show element value if it's a primitive (string, number, boolean)
900
+ if (right.element !== null && right.element !== undefined) {
901
+ const elemType = typeof right.element;
902
+ if (elemType === 'string' || elemType === 'number' || elemType === 'boolean') {
903
+ line += ` element=${JSON.stringify(right.element)}`;
904
+ }
905
+ }
906
+ line += '}';
907
+
908
+ console.info(line);
909
+
910
+ if (messages !== '<no messages>') {
911
+ console.info(`${indent} ⤷ ${messages}`);
912
+ }
913
+ }
914
+ this.depth++;
915
+ const result = await super.visitRightPadded(right, p);
916
+ this.depth--;
917
+ return result;
918
+ }
919
+ }
920
+
921
+ /**
922
+ * Convenience function to print a tree node.
923
+ * @param tree The tree node to print
924
+ * @param cursor Optional cursor for context
925
+ * @param label Optional label to identify this debug output
926
+ * @param options Optional debug options
927
+ */
928
+ export function debugPrint(tree: Tree, cursor?: Cursor, label?: string, options?: LstDebugOptions): void {
929
+ new LstDebugPrinter(options).print(tree, cursor, label);
930
+ }
931
+
932
+ /**
933
+ * Convenience function to print cursor path.
934
+ */
935
+ export function debugPrintCursorPath(cursor: Cursor, options?: LstDebugOptions): void {
936
+ new LstDebugPrinter(options).printCursorPath(cursor);
937
+ }