#![allow(clippy::enum_glob_use, clippy::ref_option, clippy::wildcard_imports)]
use termcolor::{Color, StandardStream, WriteColor};
use crate::node::Node;
use crate::output::ColorMode;
use crate::tools::{color, intense_color};
pub fn dump_node(
code: &[u8],
node: &Node,
depth: i32,
line_start: Option<usize>,
line_end: Option<usize>,
) -> std::io::Result<()> {
dump_node_with_color(code, node, depth, line_start, line_end, ColorMode::Always)
}
pub fn dump_node_with_color(
code: &[u8],
node: &Node,
depth: i32,
line_start: Option<usize>,
line_end: Option<usize>,
color_mode: ColorMode,
) -> std::io::Result<()> {
let stdout = StandardStream::stdout(color_mode.to_color_choice());
let mut stdout = stdout.lock();
let mut state = DumpState {
code,
line_start: &line_start,
line_end: &line_end,
stdout: &mut stdout,
};
let ret = dump_tree_helper(&mut state, node, "", true, depth);
color(&mut stdout, Color::White)?;
ret
}
struct DumpState<'a> {
code: &'a [u8],
line_start: &'a Option<usize>,
line_end: &'a Option<usize>,
stdout: &'a mut dyn WriteColor,
}
struct Frame<'a> {
node: Node<'a>,
prefix: String,
last: bool,
depth: i32,
}
fn dump_tree_helper(
state: &mut DumpState,
node: &Node,
prefix: &str,
last: bool,
depth: i32,
) -> std::io::Result<()> {
let mut stack: Vec<Frame> = vec![Frame {
node: *node,
prefix: prefix.to_owned(),
last,
depth,
}];
while let Some(Frame {
node,
prefix,
last,
depth,
}) = stack.pop()
{
if depth == 0 {
continue;
}
let (pref_child, pref) = branch_glyphs(&node, last);
if line_in_range(node.start_row() + 1, state.line_start, state.line_end) {
write_node_line(state.stdout, state.code, &node, &prefix, pref)?;
}
let count = node.child_count();
if count == 0 {
continue;
}
let child_prefix = format!("{prefix}{pref_child}");
let child_depth = depth - 1;
let children: Vec<Node> = node.children().collect();
for (i, child) in children.into_iter().enumerate().rev() {
stack.push(Frame {
node: child,
prefix: child_prefix.clone(),
last: i + 1 == count,
depth: child_depth,
});
}
}
Ok(())
}
fn branch_glyphs(node: &Node, last: bool) -> (&'static str, &'static str) {
if node.parent().is_none() {
("", "")
} else if last {
(" ", "╰─ ")
} else {
("│ ", "├─ ")
}
}
fn line_in_range(row: usize, line_start: &Option<usize>, line_end: &Option<usize>) -> bool {
line_start.is_none_or(|start| row >= start) && line_end.is_none_or(|end| row <= end)
}
fn paint(stdout: &mut dyn WriteColor, c: Color, args: std::fmt::Arguments) -> std::io::Result<()> {
color(stdout, c)?;
stdout.write_fmt(args)
}
fn write_node_line(
stdout: &mut dyn WriteColor,
code: &[u8],
node: &Node,
prefix: &str,
pref: &str,
) -> std::io::Result<()> {
write_node_header(stdout, node, prefix, pref)?;
write_node_location(stdout, node)?;
write_node_snippet(stdout, code, node)?;
writeln!(stdout)
}
fn write_node_header(
stdout: &mut dyn WriteColor,
node: &Node,
prefix: &str,
pref: &str,
) -> std::io::Result<()> {
paint(stdout, Color::Blue, format_args!("{prefix}{pref}"))?;
intense_color(stdout, Color::Yellow)?;
write!(stdout, "{{{}:{}}} ", node.kind(), node.kind_id())
}
fn write_node_location(stdout: &mut dyn WriteColor, node: &Node) -> std::io::Result<()> {
paint(stdout, Color::White, format_args!("from "))?;
let (row, column) = node.start_position();
paint(
stdout,
Color::Green,
format_args!("({}, {}) ", row + 1, column + 1),
)?;
paint(stdout, Color::White, format_args!("to "))?;
let (row, column) = node.end_position();
paint(
stdout,
Color::Green,
format_args!("({}, {}) ", row + 1, column + 1),
)
}
fn write_node_snippet(
stdout: &mut dyn WriteColor,
code: &[u8],
node: &Node,
) -> std::io::Result<()> {
if node.start_row() != node.end_row() {
return Ok(());
}
paint(stdout, Color::White, format_args!(": "))?;
intense_color(stdout, Color::Red)?;
let snippet = &code[node.start_byte()..node.end_byte()];
match str::from_utf8(snippet) {
Ok(text) => write!(stdout, "{text} "),
Err(_) => stdout.write_all(snippet),
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
mod tests {
use std::path::PathBuf;
use termcolor::NoColor;
use crate::{CppParser, ParserTrait};
use super::*;
#[test]
fn dump_node_non_utf8_source_does_not_panic() {
let code = b"char c = '\xff';";
let path = PathBuf::from("test.c");
let parser = CppParser::new(code.to_vec(), &path, None);
let root = parser.root();
assert!(dump_node(code, &root, -1, None, None).is_ok());
}
#[test]
fn line_in_range_unbounded_always_shows() {
assert!(line_in_range(5, &None, &None));
assert!(line_in_range(1, &None, &None));
}
#[test]
fn line_in_range_respects_inclusive_bounds() {
assert!(line_in_range(5, &Some(3), &None));
assert!(!line_in_range(2, &Some(3), &None));
assert!(line_in_range(5, &None, &Some(6)));
assert!(!line_in_range(7, &None, &Some(6)));
assert!(line_in_range(5, &Some(3), &Some(6)));
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)));
}
#[test]
fn branch_glyphs_root_is_flush_left_regardless_of_last() {
let code = b"int a = 42;\n";
let parser = CppParser::new(code.to_vec(), &PathBuf::from("t.c"), None);
let root = parser.root();
assert_eq!(branch_glyphs(&root, true), ("", ""));
assert_eq!(branch_glyphs(&root, false), ("", ""));
let child = root
.children()
.next()
.expect("translation_unit has a child");
assert_eq!(branch_glyphs(&child, true), (" ", "╰─ "));
assert_eq!(branch_glyphs(&child, false), ("│ ", "├─ "));
}
#[test]
fn dump_output_matches_expected_tree() {
let code = b"int a = 42;\n";
let parser = CppParser::new(code.to_vec(), &PathBuf::from("t.c"), None);
let root = parser.root();
let no_start: Option<usize> = None;
let no_end: Option<usize> = None;
let mut sink = NoColor::new(Vec::new());
{
let mut state = DumpState {
code,
line_start: &no_start,
line_end: &no_end,
stdout: &mut sink,
};
dump_tree_helper(&mut state, &root, "", true, -1).expect("dump to in-memory sink");
}
let rendered = String::from_utf8(sink.into_inner()).expect("dump output is utf-8");
let expected = concat!(
"{translation_unit:219} from (1, 1) to (2, 1) \n",
"╰─ {declaration:255} from (1, 1) to (1, 12) : int a = 42; \n",
" ├─ {primitive_type:96} from (1, 1) to (1, 4) : int \n",
" ├─ {init_declarator:294} from (1, 5) to (1, 11) : a = 42 \n",
" │ ├─ {identifier:1} from (1, 5) to (1, 6) : a \n",
" │ ├─ {=:74} from (1, 7) to (1, 8) : = \n",
" │ ╰─ {number_literal:158} from (1, 9) to (1, 11) : 42 \n",
" ╰─ {;:42} from (1, 11) to (1, 12) : ; \n",
);
assert_eq!(rendered, expected);
}
#[test]
fn dump_output_line_range_filters_rows() {
let code = b"int a = 1;\nint b = 2;\n";
let parser = CppParser::new(code.to_vec(), &PathBuf::from("t.c"), None);
let root = parser.root();
let start: Option<usize> = Some(2);
let end: Option<usize> = Some(2);
let mut sink = NoColor::new(Vec::new());
{
let mut state = DumpState {
code,
line_start: &start,
line_end: &end,
stdout: &mut sink,
};
dump_tree_helper(&mut state, &root, "", true, -1).expect("dump to in-memory sink");
}
let rendered = String::from_utf8(sink.into_inner()).expect("dump output is utf-8");
assert!(
!rendered.contains("(1, "),
"row-1 nodes should be hidden:\n{rendered}"
);
assert!(
rendered.contains("int b = 2;"),
"row-2 declaration should show:\n{rendered}"
);
}
#[test]
fn deeply_nested_ast_dumps_without_stack_overflow() {
const DEPTH: usize = 4_000;
let mut src = Vec::with_capacity(DEPTH * 2 + 8);
src.extend_from_slice(b"int a = ");
src.extend(std::iter::repeat_n(b'(', DEPTH));
src.push(b'1');
src.extend(std::iter::repeat_n(b')', DEPTH));
src.extend_from_slice(b";\n");
let handle = std::thread::Builder::new()
.stack_size(512 * 1024)
.spawn(move || {
let parser = CppParser::new(src.clone(), &PathBuf::from("deep.c"), None);
let root = parser.root();
let no_start: Option<usize> = None;
let no_end: Option<usize> = None;
let mut sink = NoColor::new(Vec::new());
let mut state = DumpState {
code: &src,
line_start: &no_start,
line_end: &no_end,
stdout: &mut sink,
};
dump_tree_helper(&mut state, &root, "", true, -1).is_ok()
})
.expect("spawn dump thread");
assert!(
handle
.join()
.expect("dump thread must not overflow the stack"),
"deep AST must dump successfully"
);
}
#[test]
fn dump_output_depth_limits_recursion() {
let code = b"int a = 42;\n";
let parser = CppParser::new(code.to_vec(), &PathBuf::from("t.c"), None);
let root = parser.root();
let no_start: Option<usize> = None;
let no_end: Option<usize> = None;
let mut sink = NoColor::new(Vec::new());
{
let mut state = DumpState {
code,
line_start: &no_start,
line_end: &no_end,
stdout: &mut sink,
};
dump_tree_helper(&mut state, &root, "", true, 1).expect("dump to in-memory sink");
}
let rendered = String::from_utf8(sink.into_inner()).expect("dump output is utf-8");
assert!(
rendered.contains("{translation_unit:"),
"depth=1 should render the root:\n{rendered}"
);
assert!(
!rendered.contains("{declaration:"),
"depth=1 must not recurse into children:\n{rendered}"
);
assert_eq!(
rendered.lines().count(),
1,
"depth=1 renders exactly one node:\n{rendered}"
);
let mut sink_zero = NoColor::new(Vec::new());
{
let mut state = DumpState {
code,
line_start: &no_start,
line_end: &no_end,
stdout: &mut sink_zero,
};
dump_tree_helper(&mut state, &root, "", true, 0).expect("dump to in-memory sink");
}
assert!(sink_zero.into_inner().is_empty(), "depth=0 renders nothing");
}
}