@@ -22,21 +22,29 @@ import * as fs from "fs";
2222
2323/**
2424 * Options for controlling LST debug output.
25+ *
26+ * @example
27+ * // Output to a file instead of console
28+ * const options: LstDebugOptions = { output: '/tmp/debug.txt' };
29+ *
30+ * @example
31+ * // Minimal output - just the tree structure
32+ * const options: LstDebugOptions = { includeCursorMessages: false };
2533 */
2634export interface LstDebugOptions {
27- /** Include cursor messages in output. Default: true */
35+ /** Include cursor messages (like indentContext) in output. Default: true */
2836 includeCursorMessages ?: boolean ;
2937 /** Include markers in output. Default: false */
3038 includeMarkers ?: boolean ;
31- /** Include IDs in output. Default: false */
39+ /** Include node IDs in output. Default: false */
3240 includeIds ?: boolean ;
33- /** Maximum depth to traverse. Default: unlimited (-1) */
41+ /** Maximum depth to traverse (for print/recursive methods) . Default: unlimited (-1) */
3442 maxDepth ?: number ;
35- /** Properties to always exclude (in addition to defaults). */
43+ /** Properties to always exclude (in addition to defaults like 'type' ). */
3644 excludeProperties ?: string [ ] ;
3745 /** Output destination: 'console' or a file path. Default: 'console' */
3846 output ?: 'console' | string ;
39- /** Indent string for nested output. Default: ' ' */
47+ /** Indent string for nested output. Default: ' ' (2 spaces) */
4048 indent ?: string ;
4149}
4250
@@ -233,6 +241,10 @@ export function formatCursorMessages(cursor: Cursor | undefined): string {
233241 let valueStr : string ;
234242 if ( Array . isArray ( value ) ) {
235243 valueStr = `[${ value . map ( v => JSON . stringify ( v ) ) . join ( ', ' ) } ]` ;
244+ } else if ( value instanceof Set ) {
245+ // Handle Set - convert to array notation
246+ const items = Array . from ( value ) . map ( v => JSON . stringify ( v ) ) . join ( ', ' ) ;
247+ valueStr = `{${ items } }` ;
236248 } else if ( typeof value === 'object' && value !== null ) {
237249 valueStr = JSON . stringify ( value ) ;
238250 } else {
@@ -255,6 +267,19 @@ function shortTypeName(kind: string | undefined): string {
255267 return lastDot >= 0 ? kind . substring ( lastDot + 1 ) : kind ;
256268}
257269
270+ /**
271+ * Format markers for debug output.
272+ * Returns empty string if no markers, otherwise returns ' markers=[Name1, Name2]'
273+ */
274+ function formatMarkers ( node : any ) : string {
275+ const markers = node ?. markers ?. markers ;
276+ if ( ! markers || ! Array . isArray ( markers ) || markers . length === 0 ) {
277+ return '' ;
278+ }
279+ const names = markers . map ( ( m : any ) => shortTypeName ( m . kind ) ) ;
280+ return ` markers=[${ names . join ( ', ' ) } ]` ;
281+ }
282+
258283/**
259284 * Find which property of the parent contains the given child element.
260285 * Returns the property name, or property name with array index if in an array.
@@ -380,17 +405,31 @@ export function findPropertyPath(cursor: Cursor | undefined, child?: any): strin
380405 return undefined ;
381406}
382407
408+ /**
409+ * Escape special characters in a string for display.
410+ */
411+ function escapeString ( str : string ) : string {
412+ return str
413+ . replace ( / \\ / g, '\\\\' )
414+ . replace ( / \n / g, '\\n' )
415+ . replace ( / \r / g, '\\r' )
416+ . replace ( / \t / g, '\\t' ) ;
417+ }
418+
383419/**
384420 * Format a literal value for inline display.
385421 */
386422function formatLiteralValue ( lit : J . Literal ) : string {
423+ let value : string ;
387424 if ( lit . valueSource !== undefined ) {
388- // Truncate long literals
389- return lit . valueSource . length > 20
390- ? lit . valueSource . substring ( 0 , 17 ) + '...'
391- : lit . valueSource ;
425+ value = lit . valueSource ;
426+ } else {
427+ value = String ( lit . value ) ;
392428 }
393- return String ( lit . value ) ;
429+ // Escape special characters
430+ value = escapeString ( value ) ;
431+ // Truncate long literals
432+ return value . length > 20 ? value . substring ( 0 , 17 ) + '...' : value ;
394433}
395434
396435/**
@@ -486,34 +525,10 @@ export class LstDebugPrinter {
486525
487526 constructor ( options : LstDebugOptions = { } ) {
488527 this . options = { ...DEFAULT_OPTIONS , ...options } ;
489- }
490-
491- /**
492- * Calculate the depth of the cursor by counting parent chain length.
493- * Uses caching to avoid repeated traversals.
494- */
495- private calculateDepth ( cursor : Cursor | undefined ) : number {
496- if ( ! cursor ) {
497- return 0 ;
528+ // Truncate output file at start of session
529+ if ( this . options . output !== 'console' ) {
530+ fs . writeFileSync ( this . options . output , '' ) ;
498531 }
499-
500- // Check cache first
501- const cached = this . depthCache . get ( cursor ) ;
502- if ( cached !== undefined ) {
503- return cached ;
504- }
505-
506- // Count depth by walking parent chain
507- let depth = 0 ;
508- for ( let c : Cursor | undefined = cursor ; c ; c = c . parent ) {
509- depth ++ ;
510- }
511- // Subtract 2 to skip root cursor and start CompilationUnit at depth 0
512- depth = Math . max ( 0 , depth - 2 ) ;
513-
514- // Cache the result
515- this . depthCache . set ( cursor , depth ) ;
516- return depth ;
517532 }
518533
519534 /**
@@ -553,15 +568,17 @@ export class LstDebugPrinter {
553568 let line = indent ;
554569
555570 // Find property path from cursor
556- const propPath = findPropertyPath ( cursor , node ) ;
571+ // When node === cursor.value, don't pass node - we want to find cursor.value in cursor.parent.value
572+ // When node !== cursor.value (e.g., for RightPadded/LeftPadded/Container), pass node to find it in cursor.value
573+ const propPath = findPropertyPath ( cursor , node === cursor ?. value ? undefined : node ) ;
557574 if ( propPath ) {
558575 line += `${ propPath } : ` ;
559576 }
560577
561578 if ( this . isContainer ( node ) ) {
562579 const container = node as J . Container < any > ;
563580 const before = formatSpace ( container . before ) ;
564- line += `Container<${ container . elements ?. length ?? 0 } >{before=${ before } }` ;
581+ line += `Container<${ container . elements ?. length ?? 0 } >{before=${ before } ${ formatMarkers ( container ) } }` ;
565582 } else if ( this . isLeftPadded ( node ) ) {
566583 const lp = node as J . LeftPadded < any > ;
567584 const before = formatSpace ( lp . before ) ;
@@ -573,7 +590,7 @@ export class LstDebugPrinter {
573590 line += ` element=${ JSON . stringify ( lp . element ) } ` ;
574591 }
575592 }
576- line += '}' ;
593+ line += ` ${ formatMarkers ( lp ) } }` ;
577594 } else if ( this . isRightPadded ( node ) ) {
578595 const rp = node as J . RightPadded < any > ;
579596 const after = formatSpace ( rp . after ) ;
@@ -585,7 +602,7 @@ export class LstDebugPrinter {
585602 line += ` element=${ JSON . stringify ( rp . element ) } ` ;
586603 }
587604 }
588- line += '}' ;
605+ line += ` ${ formatMarkers ( rp ) } }` ;
589606 } else if ( isJava ( node ) ) {
590607 const jNode = node as J ;
591608 const typeName = shortTypeName ( jNode . kind ) ;
@@ -594,7 +611,7 @@ export class LstDebugPrinter {
594611 if ( summary ) {
595612 line += `${ summary } ` ;
596613 }
597- line += `prefix=${ formatSpace ( jNode . prefix ) } }` ;
614+ line += `prefix=${ formatSpace ( jNode . prefix ) } ${ formatMarkers ( jNode ) } }` ;
598615 } else {
599616 line += `<unknown: ${ typeof node } >` ;
600617 }
@@ -657,6 +674,34 @@ export class LstDebugPrinter {
657674 this . flush ( ) ;
658675 }
659676
677+ /**
678+ * Calculate the depth of the cursor by counting parent chain length.
679+ * Uses caching to avoid repeated traversals.
680+ */
681+ private calculateDepth ( cursor : Cursor | undefined ) : number {
682+ if ( ! cursor ) {
683+ return 0 ;
684+ }
685+
686+ // Check cache first
687+ const cached = this . depthCache . get ( cursor ) ;
688+ if ( cached !== undefined ) {
689+ return cached ;
690+ }
691+
692+ // Count depth by walking parent chain
693+ let depth = 0 ;
694+ for ( let c : Cursor | undefined = cursor ; c ; c = c . parent ) {
695+ depth ++ ;
696+ }
697+ // Subtract 2 to skip root cursor and start CompilationUnit at depth 0
698+ depth = Math . max ( 0 , depth - 2 ) ;
699+
700+ // Cache the result
701+ this . depthCache . set ( cursor , depth ) ;
702+ return depth ;
703+ }
704+
660705 private printNode (
661706 node : any ,
662707 cursor : Cursor | undefined ,
@@ -969,45 +1014,6 @@ export class LstDebugVisitor<P> extends JavaScriptVisitor<P> {
9691014 this . printPostVisit = config . printPostVisit ?? false ;
9701015 }
9711016
972- protected async preVisit ( tree : J , _p : P ) : Promise < J | undefined > {
973- if ( this . printPreVisit ) {
974- const typeName = shortTypeName ( tree . kind ) ;
975- const indent = ' ' . repeat ( this . depth ) ;
976- const messages = formatCursorMessages ( this . cursor ) ;
977- const prefix = formatSpace ( tree . prefix ) ;
978- const summary = getNodeSummary ( tree ) ;
979- const propPath = findPropertyPath ( this . cursor ) ;
980-
981- let line = indent ;
982- if ( propPath ) {
983- line += `${ propPath } : ` ;
984- }
985- line += `${ typeName } {` ;
986- if ( summary ) {
987- line += `${ summary } ` ;
988- }
989- line += `prefix=${ prefix } }` ;
990-
991- // Append cursor messages on same line to avoid empty line issues
992- if ( messages !== '<no messages>' ) {
993- line += ` ${ messages } ` ;
994- }
995- console . info ( line ) ;
996- }
997- this . depth ++ ;
998- return tree ;
999- }
1000-
1001- protected async postVisit ( tree : J , _p : P ) : Promise < J | undefined > {
1002- this . depth -- ;
1003- if ( this . printPostVisit ) {
1004- const typeName = shortTypeName ( tree . kind ) ;
1005- const indent = ' ' . repeat ( this . depth ) ;
1006- console . info ( `${ indent } ← ${ typeName } ` ) ;
1007- }
1008- return tree ;
1009- }
1010-
10111017 public async visitContainer < T extends J > ( container : J . Container < T > , p : P ) : Promise < J . Container < T > > {
10121018 if ( this . printPreVisit ) {
10131019 const indent = ' ' . repeat ( this . depth ) ;
@@ -1020,7 +1026,7 @@ export class LstDebugVisitor<P> extends JavaScriptVisitor<P> {
10201026 if ( propPath ) {
10211027 line += `${ propPath } : ` ;
10221028 }
1023- line += `Container<${ container . elements . length } >{before=${ before } }` ;
1029+ line += `Container<${ container . elements . length } >{before=${ before } ${ formatMarkers ( container ) } }` ;
10241030
10251031 // Append cursor messages on same line to avoid empty line issues
10261032 if ( messages !== '<no messages>' ) {
@@ -1058,7 +1064,7 @@ export class LstDebugVisitor<P> extends JavaScriptVisitor<P> {
10581064 line += ` element=${ JSON . stringify ( left . element ) } ` ;
10591065 }
10601066 }
1061- line += '}' ;
1067+ line += ` ${ formatMarkers ( left ) } }` ;
10621068
10631069 // Append cursor messages on same line to avoid empty line issues
10641070 if ( messages !== '<no messages>' ) {
@@ -1096,7 +1102,7 @@ export class LstDebugVisitor<P> extends JavaScriptVisitor<P> {
10961102 line += ` element=${ JSON . stringify ( right . element ) } ` ;
10971103 }
10981104 }
1099- line += '}' ;
1105+ line += ` ${ formatMarkers ( right ) } }` ;
11001106
11011107 // Append cursor messages on same line to avoid empty line issues
11021108 if ( messages !== '<no messages>' ) {
@@ -1109,6 +1115,45 @@ export class LstDebugVisitor<P> extends JavaScriptVisitor<P> {
11091115 this . depth -- ;
11101116 return result ;
11111117 }
1118+
1119+ protected async preVisit ( tree : J , _p : P ) : Promise < J | undefined > {
1120+ if ( this . printPreVisit ) {
1121+ const typeName = shortTypeName ( tree . kind ) ;
1122+ const indent = ' ' . repeat ( this . depth ) ;
1123+ const messages = formatCursorMessages ( this . cursor ) ;
1124+ const prefix = formatSpace ( tree . prefix ) ;
1125+ const summary = getNodeSummary ( tree ) ;
1126+ const propPath = findPropertyPath ( this . cursor ) ;
1127+
1128+ let line = indent ;
1129+ if ( propPath ) {
1130+ line += `${ propPath } : ` ;
1131+ }
1132+ line += `${ typeName } {` ;
1133+ if ( summary ) {
1134+ line += `${ summary } ` ;
1135+ }
1136+ line += `prefix=${ prefix } ${ formatMarkers ( tree ) } }` ;
1137+
1138+ // Append cursor messages on same line to avoid empty line issues
1139+ if ( messages !== '<no messages>' ) {
1140+ line += ` ${ messages } ` ;
1141+ }
1142+ console . info ( line ) ;
1143+ }
1144+ this . depth ++ ;
1145+ return tree ;
1146+ }
1147+
1148+ protected async postVisit ( tree : J , _p : P ) : Promise < J | undefined > {
1149+ this . depth -- ;
1150+ if ( this . printPostVisit ) {
1151+ const typeName = shortTypeName ( tree . kind ) ;
1152+ const indent = ' ' . repeat ( this . depth ) ;
1153+ console . info ( `${ indent } ← ${ typeName } ` ) ;
1154+ }
1155+ return tree ;
1156+ }
11121157}
11131158
11141159/**
@@ -1143,3 +1188,44 @@ export function debugPrint(tree: Tree, cursor?: Cursor, label?: string, options?
11431188export function debugPrintCursorPath ( cursor : Cursor , options ?: LstDebugOptions ) : void {
11441189 new LstDebugPrinter ( options ) . printCursorPath ( cursor ) ;
11451190}
1191+
1192+ /**
1193+ * Create a debug printer if debugging is enabled, otherwise return undefined.
1194+ *
1195+ * This is useful for visitors that want to optionally enable debugging via
1196+ * constructor parameters or configuration.
1197+ *
1198+ * @param enabled Whether debugging is enabled
1199+ * @param options Debug options (including output file path)
1200+ * @returns LstDebugPrinter if enabled, undefined otherwise
1201+ *
1202+ * @example
1203+ * class MyVisitor extends JavaScriptVisitor<P> {
1204+ * private debug?: LstDebugPrinter;
1205+ *
1206+ * constructor(enableDebug?: boolean | LstDebugOptions) {
1207+ * super();
1208+ * this.debug = createDebugPrinter(enableDebug);
1209+ * }
1210+ *
1211+ * async visitBlock(block: J.Block, p: P) {
1212+ * this.debug?.log(block, this.cursor);
1213+ * return super.visitBlock(block, p);
1214+ * }
1215+ * }
1216+ *
1217+ * // Usage:
1218+ * new MyVisitor(true); // Enable with defaults
1219+ * new MyVisitor({ output: '/tmp/debug.txt' }); // Enable with options
1220+ * new MyVisitor(false); // Disabled
1221+ * new MyVisitor(); // Disabled (default)
1222+ */
1223+ export function createDebugPrinter ( enabled ?: boolean | LstDebugOptions ) : LstDebugPrinter | undefined {
1224+ if ( enabled === undefined || enabled === false ) {
1225+ return undefined ;
1226+ }
1227+ if ( enabled === true ) {
1228+ return new LstDebugPrinter ( ) ;
1229+ }
1230+ return new LstDebugPrinter ( enabled ) ;
1231+ }
0 commit comments