1#![allow(clippy::enum_glob_use, clippy::ref_option, clippy::wildcard_imports)]
8
9use termcolor::{Color, StandardStream, WriteColor};
10
11use crate::node::Node;
12use crate::output::ColorMode;
13use crate::tools::{color, intense_color};
14
15pub fn dump_node(
46 code: &[u8],
47 node: &Node,
48 depth: i32,
49 line_start: Option<usize>,
50 line_end: Option<usize>,
51) -> std::io::Result<()> {
52 dump_node_with_color(code, node, depth, line_start, line_end, ColorMode::Always)
53}
54
55pub fn dump_node_with_color(
75 code: &[u8],
76 node: &Node,
77 depth: i32,
78 line_start: Option<usize>,
79 line_end: Option<usize>,
80 color_mode: ColorMode,
81) -> std::io::Result<()> {
82 let stdout = StandardStream::stdout(color_mode.to_color_choice());
83 let mut stdout = stdout.lock();
84 let mut state = DumpState {
85 code,
86 line_start: &line_start,
87 line_end: &line_end,
88 stdout: &mut stdout,
89 };
90 let ret = dump_tree_helper(&mut state, node, "", true, depth);
91
92 color(&mut stdout, Color::White)?;
93
94 ret
95}
96
97struct DumpState<'a> {
104 code: &'a [u8],
105 line_start: &'a Option<usize>,
106 line_end: &'a Option<usize>,
107 stdout: &'a mut dyn WriteColor,
108}
109
110struct Frame<'a> {
115 node: Node<'a>,
116 prefix: String,
117 last: bool,
118 depth: i32,
119}
120
121fn dump_tree_helper(
129 state: &mut DumpState,
130 node: &Node,
131 prefix: &str,
132 last: bool,
133 depth: i32,
134) -> std::io::Result<()> {
135 let mut stack: Vec<Frame> = vec![Frame {
136 node: *node,
137 prefix: prefix.to_owned(),
138 last,
139 depth,
140 }];
141
142 while let Some(Frame {
143 node,
144 prefix,
145 last,
146 depth,
147 }) = stack.pop()
148 {
149 if depth == 0 {
150 continue;
151 }
152
153 let (pref_child, pref) = branch_glyphs(&node, last);
154
155 if line_in_range(node.start_row() + 1, state.line_start, state.line_end) {
156 write_node_line(state.stdout, state.code, &node, &prefix, pref)?;
157 }
158
159 let count = node.child_count();
160 if count == 0 {
161 continue;
162 }
163
164 let child_prefix = format!("{prefix}{pref_child}");
170 let child_depth = depth - 1;
171 let children: Vec<Node> = node.children().collect();
172 for (i, child) in children.into_iter().enumerate().rev() {
173 stack.push(Frame {
174 node: child,
175 prefix: child_prefix.clone(),
176 last: i + 1 == count,
177 depth: child_depth,
178 });
179 }
180 }
181
182 Ok(())
183}
184
185fn branch_glyphs(node: &Node, last: bool) -> (&'static str, &'static str) {
189 if node.parent().is_none() {
190 ("", "")
191 } else if last {
192 (" ", "╰─ ")
193 } else {
194 ("│ ", "├─ ")
195 }
196}
197
198fn line_in_range(row: usize, line_start: &Option<usize>, line_end: &Option<usize>) -> bool {
202 line_start.is_none_or(|start| row >= start) && line_end.is_none_or(|end| row <= end)
203}
204
205fn paint(stdout: &mut dyn WriteColor, c: Color, args: std::fmt::Arguments) -> std::io::Result<()> {
209 color(stdout, c)?;
210 stdout.write_fmt(args)
211}
212
213fn write_node_line(
217 stdout: &mut dyn WriteColor,
218 code: &[u8],
219 node: &Node,
220 prefix: &str,
221 pref: &str,
222) -> std::io::Result<()> {
223 write_node_header(stdout, node, prefix, pref)?;
224 write_node_location(stdout, node)?;
225 write_node_snippet(stdout, code, node)?;
226 writeln!(stdout)
227}
228
229fn write_node_header(
231 stdout: &mut dyn WriteColor,
232 node: &Node,
233 prefix: &str,
234 pref: &str,
235) -> std::io::Result<()> {
236 paint(stdout, Color::Blue, format_args!("{prefix}{pref}"))?;
237 intense_color(stdout, Color::Yellow)?;
238 write!(stdout, "{{{}:{}}} ", node.kind(), node.kind_id())
239}
240
241fn write_node_location(stdout: &mut dyn WriteColor, node: &Node) -> std::io::Result<()> {
243 paint(stdout, Color::White, format_args!("from "))?;
244 let (row, column) = node.start_position();
245 paint(
246 stdout,
247 Color::Green,
248 format_args!("({}, {}) ", row + 1, column + 1),
249 )?;
250 paint(stdout, Color::White, format_args!("to "))?;
251 let (row, column) = node.end_position();
252 paint(
253 stdout,
254 Color::Green,
255 format_args!("({}, {}) ", row + 1, column + 1),
256 )
257}
258
259fn write_node_snippet(
264 stdout: &mut dyn WriteColor,
265 code: &[u8],
266 node: &Node,
267) -> std::io::Result<()> {
268 if node.start_row() != node.end_row() {
269 return Ok(());
270 }
271
272 paint(stdout, Color::White, format_args!(": "))?;
273 intense_color(stdout, Color::Red)?;
274 let snippet = &code[node.start_byte()..node.end_byte()];
275 match str::from_utf8(snippet) {
276 Ok(text) => write!(stdout, "{text} "),
277 Err(_) => stdout.write_all(snippet),
278 }
279}
280
281#[cfg(test)]
282#[allow(
283 clippy::float_cmp,
284 clippy::cast_precision_loss,
285 clippy::cast_possible_truncation,
286 clippy::cast_sign_loss,
287 clippy::similar_names,
288 clippy::doc_markdown,
289 clippy::needless_raw_string_hashes,
290 clippy::too_many_lines
291)]
292mod tests {
293 use std::path::PathBuf;
294
295 use termcolor::NoColor;
296
297 use crate::{CppParser, ParserTrait};
298
299 use super::*;
300
301 #[test]
302 fn dump_node_non_utf8_source_does_not_panic() {
303 let code = b"char c = '\xff';";
306 let path = PathBuf::from("test.c");
307 let parser = CppParser::new(code.to_vec(), &path, None);
308 let root = parser.root();
309 assert!(dump_node(code, &root, -1, None, None).is_ok());
310 }
311
312 #[test]
313 fn line_in_range_unbounded_always_shows() {
314 assert!(line_in_range(5, &None, &None));
316 assert!(line_in_range(1, &None, &None));
317 }
318
319 #[test]
320 fn line_in_range_respects_inclusive_bounds() {
321 assert!(line_in_range(5, &Some(3), &None));
323 assert!(!line_in_range(2, &Some(3), &None));
324 assert!(line_in_range(5, &None, &Some(6)));
326 assert!(!line_in_range(7, &None, &Some(6)));
327 assert!(line_in_range(5, &Some(3), &Some(6)));
329 assert!(!line_in_range(5, &Some(6), &Some(9))); assert!(!line_in_range(5, &Some(1), &Some(4))); assert!(line_in_range(3, &Some(3), &Some(3)));
333 }
334
335 #[test]
336 fn branch_glyphs_root_is_flush_left_regardless_of_last() {
337 let code = b"int a = 42;\n";
338 let parser = CppParser::new(code.to_vec(), &PathBuf::from("t.c"), None);
339 let root = parser.root();
340 assert_eq!(branch_glyphs(&root, true), ("", ""));
342 assert_eq!(branch_glyphs(&root, false), ("", ""));
343
344 let child = root
345 .children()
346 .next()
347 .expect("translation_unit has a child");
348 assert_eq!(branch_glyphs(&child, true), (" ", "╰─ "));
349 assert_eq!(branch_glyphs(&child, false), ("│ ", "├─ "));
350 }
351
352 #[test]
353 fn dump_output_matches_expected_tree() {
354 let code = b"int a = 42;\n";
359 let parser = CppParser::new(code.to_vec(), &PathBuf::from("t.c"), None);
360 let root = parser.root();
361
362 let no_start: Option<usize> = None;
363 let no_end: Option<usize> = None;
364 let mut sink = NoColor::new(Vec::new());
365 {
366 let mut state = DumpState {
367 code,
368 line_start: &no_start,
369 line_end: &no_end,
370 stdout: &mut sink,
371 };
372 dump_tree_helper(&mut state, &root, "", true, -1).expect("dump to in-memory sink");
373 }
374 let rendered = String::from_utf8(sink.into_inner()).expect("dump output is utf-8");
375
376 let expected = concat!(
377 "{translation_unit:219} from (1, 1) to (2, 1) \n",
378 "╰─ {declaration:255} from (1, 1) to (1, 12) : int a = 42; \n",
379 " ├─ {primitive_type:96} from (1, 1) to (1, 4) : int \n",
380 " ├─ {init_declarator:294} from (1, 5) to (1, 11) : a = 42 \n",
381 " │ ├─ {identifier:1} from (1, 5) to (1, 6) : a \n",
382 " │ ├─ {=:74} from (1, 7) to (1, 8) : = \n",
383 " │ ╰─ {number_literal:158} from (1, 9) to (1, 11) : 42 \n",
384 " ╰─ {;:42} from (1, 11) to (1, 12) : ; \n",
385 );
386 assert_eq!(rendered, expected);
387 }
388
389 #[test]
390 fn dump_output_line_range_filters_rows() {
391 let code = b"int a = 1;\nint b = 2;\n";
394 let parser = CppParser::new(code.to_vec(), &PathBuf::from("t.c"), None);
395 let root = parser.root();
396
397 let start: Option<usize> = Some(2);
398 let end: Option<usize> = Some(2);
399 let mut sink = NoColor::new(Vec::new());
400 {
401 let mut state = DumpState {
402 code,
403 line_start: &start,
404 line_end: &end,
405 stdout: &mut sink,
406 };
407 dump_tree_helper(&mut state, &root, "", true, -1).expect("dump to in-memory sink");
408 }
409 let rendered = String::from_utf8(sink.into_inner()).expect("dump output is utf-8");
410
411 assert!(
414 !rendered.contains("(1, "),
415 "row-1 nodes should be hidden:\n{rendered}"
416 );
417 assert!(
418 rendered.contains("int b = 2;"),
419 "row-2 declaration should show:\n{rendered}"
420 );
421 }
422
423 #[test]
424 fn deeply_nested_ast_dumps_without_stack_overflow() {
425 const DEPTH: usize = 4_000;
434 let mut src = Vec::with_capacity(DEPTH * 2 + 8);
435 src.extend_from_slice(b"int a = ");
436 src.extend(std::iter::repeat_n(b'(', DEPTH));
437 src.push(b'1');
438 src.extend(std::iter::repeat_n(b')', DEPTH));
439 src.extend_from_slice(b";\n");
440
441 let handle = std::thread::Builder::new()
442 .stack_size(512 * 1024)
443 .spawn(move || {
444 let parser = CppParser::new(src.clone(), &PathBuf::from("deep.c"), None);
445 let root = parser.root();
446 let no_start: Option<usize> = None;
447 let no_end: Option<usize> = None;
448 let mut sink = NoColor::new(Vec::new());
449 let mut state = DumpState {
450 code: &src,
451 line_start: &no_start,
452 line_end: &no_end,
453 stdout: &mut sink,
454 };
455 dump_tree_helper(&mut state, &root, "", true, -1).is_ok()
456 })
457 .expect("spawn dump thread");
458 assert!(
459 handle
460 .join()
461 .expect("dump thread must not overflow the stack"),
462 "deep AST must dump successfully"
463 );
464 }
465
466 #[test]
467 fn dump_output_depth_limits_recursion() {
468 let code = b"int a = 42;\n";
475 let parser = CppParser::new(code.to_vec(), &PathBuf::from("t.c"), None);
476 let root = parser.root();
477 let no_start: Option<usize> = None;
478 let no_end: Option<usize> = None;
479
480 let mut sink = NoColor::new(Vec::new());
482 {
483 let mut state = DumpState {
484 code,
485 line_start: &no_start,
486 line_end: &no_end,
487 stdout: &mut sink,
488 };
489 dump_tree_helper(&mut state, &root, "", true, 1).expect("dump to in-memory sink");
490 }
491 let rendered = String::from_utf8(sink.into_inner()).expect("dump output is utf-8");
492 assert!(
493 rendered.contains("{translation_unit:"),
494 "depth=1 should render the root:\n{rendered}"
495 );
496 assert!(
497 !rendered.contains("{declaration:"),
498 "depth=1 must not recurse into children:\n{rendered}"
499 );
500 assert_eq!(
501 rendered.lines().count(),
502 1,
503 "depth=1 renders exactly one node:\n{rendered}"
504 );
505
506 let mut sink_zero = NoColor::new(Vec::new());
508 {
509 let mut state = DumpState {
510 code,
511 line_start: &no_start,
512 line_end: &no_end,
513 stdout: &mut sink_zero,
514 };
515 dump_tree_helper(&mut state, &root, "", true, 0).expect("dump to in-memory sink");
516 }
517 assert!(sink_zero.into_inner().is_empty(), "depth=0 renders nothing");
518 }
519}