Skip to content

Commit 9a25466

Browse files
committed
JavaScript: Include markers in LstDebugPrinter output
1 parent bdac8e0 commit 9a25466

2 files changed

Lines changed: 191 additions & 84 deletions

File tree

rewrite-javascript/rewrite/src/javascript/tree-debug.ts

Lines changed: 170 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
2634
export 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
*/
386422
function 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?
11431188
export 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

Comments
 (0)