Skip to main content

big_code_analysis/output/
dump_ops.rs

1use termcolor::{Color, StandardStream, WriteColor};
2
3use crate::ops::Ops;
4use crate::output::ColorMode;
5
6use crate::tools::{color, intense_color};
7
8/// Dumps all operands and operators of a code.
9///
10/// Returns a [`Result`] value, when an error occurs.
11///
12/// # Errors
13///
14/// Propagates any [`std::io::Error`] produced by the color-aware
15/// writer that backs `stdout` (broken pipe, write failure, …).
16///
17/// # Examples
18///
19/// ```
20/// use big_code_analysis::{dump_ops, Ast, LANG, Source};
21///
22/// let source_code = "int a = 42;";
23///
24/// // Retrieve all operands and operators via the `Ast::ops` seam.
25/// let ops = Ast::parse(
26///     Source::new(LANG::Cpp, source_code.as_bytes())
27///         .with_name(Some("foo.c".to_owned())),
28/// )
29/// .expect("cpp feature enabled")
30/// .ops()
31/// .unwrap();
32///
33/// // Dump all operands and operators
34/// dump_ops(&ops).unwrap();
35/// ```
36pub fn dump_ops(ops: &Ops) -> std::io::Result<()> {
37    dump_ops_with_color(ops, ColorMode::Always)
38}
39
40/// Like [`dump_ops`], but the caller selects the [`ColorMode`].
41///
42/// `bca` resolves a `--color` flag, the `NO_COLOR` convention, and
43/// stdout tty detection into a mode and passes it here so piped output
44/// is escape-free by default. The bare [`dump_ops`] keeps the
45/// historical always-colored behavior for backward compatibility.
46///
47/// # Errors
48///
49/// Propagates any [`std::io::Error`] produced by the color-aware
50/// writer that backs `stdout` (broken pipe, write failure, …).
51pub fn dump_ops_with_color(ops: &Ops, color_mode: ColorMode) -> std::io::Result<()> {
52    let stdout = StandardStream::stdout(color_mode.to_color_choice());
53    let mut stdout = stdout.lock();
54    dump_space(ops, "", true, &mut stdout)?;
55    color(&mut stdout, Color::White)?;
56
57    Ok(())
58}
59
60/// Dump the `Ops` space tree with an explicit work stack rather than
61/// recursion, so a pathologically deep space nesting (closures within
62/// closures) cannot overflow the thread stack at dump time — an
63/// uncatchable abort, forbidden by the no-panic rule (#700). Traversal
64/// order and per-node glyphs are byte-identical to the prior recursive
65/// form.
66fn dump_space(
67    space: &Ops,
68    prefix: &str,
69    last: bool,
70    stdout: &mut dyn WriteColor,
71) -> std::io::Result<()> {
72    let mut stack: Vec<(&Ops, String, bool)> = vec![(space, prefix.to_owned(), last)];
73
74    while let Some((space, prefix, last)) = stack.pop() {
75        let (pref_child, pref) = if last { ("   ", "`- ") } else { ("|  ", "|- ") };
76
77        color(stdout, Color::Blue)?;
78        write!(stdout, "{prefix}{pref}")?;
79
80        intense_color(stdout, Color::Yellow)?;
81        write!(stdout, "{}: ", space.kind)?;
82
83        intense_color(stdout, Color::Cyan)?;
84        write!(stdout, "{}", space.name.as_ref().map_or("", |name| name))?;
85
86        intense_color(stdout, Color::Red)?;
87        writeln!(stdout, " (@{})", space.start_line)?;
88
89        let child_prefix = format!("{prefix}{pref_child}");
90        dump_space_ops(space, &child_prefix, space.spaces.is_empty(), stdout)?;
91
92        // Push children in reverse so `pop()` visits them in source
93        // order; the final child carries `last = true` for the closing
94        // `` `- `` glyph, matching the recursive `split_last` form.
95        let count = space.spaces.len();
96        for (i, child) in space.spaces.iter().enumerate().rev() {
97            stack.push((child, child_prefix.clone(), i + 1 == count));
98        }
99    }
100
101    Ok(())
102}
103
104fn dump_space_ops(
105    ops: &Ops,
106    prefix: &str,
107    last: bool,
108    stdout: &mut dyn WriteColor,
109) -> std::io::Result<()> {
110    // `operands` always follows `operators` within a space's op block, so
111    // `operators` is never the last child and must render the mid-child
112    // connector (`|-`), regardless of whether the block itself is the
113    // last child of the space. Passing the block's `last` to both made
114    // `operators` draw the closing `` `- `` glyph and mis-indent its
115    // operand subtree (#700). Only `operands` inherits the block's
116    // `last`.
117    dump_ops_values("operators", &ops.operators, prefix, false, stdout)?;
118    dump_ops_values("operands", &ops.operands, prefix, last, stdout)
119}
120
121fn dump_ops_values(
122    name: &str,
123    ops: &[String],
124    prefix: &str,
125    last: bool,
126    stdout: &mut dyn WriteColor,
127) -> std::io::Result<()> {
128    let (pref_child, pref) = if last { ("   ", "`- ") } else { ("|  ", "|- ") };
129
130    color(stdout, Color::Blue)?;
131    write!(stdout, "{prefix}{pref}")?;
132
133    intense_color(stdout, Color::Green)?;
134    writeln!(stdout, "{name}")?;
135
136    let Some((last_op, rest)) = ops.split_last() else {
137        return Ok(());
138    };
139
140    let prefix = format!("{prefix}{pref_child}");
141    for op in rest {
142        color(stdout, Color::Blue)?;
143        write!(stdout, "{prefix}|- ")?;
144
145        color(stdout, Color::White)?;
146        writeln!(stdout, "{op}")?;
147    }
148
149    color(stdout, Color::Blue)?;
150    write!(stdout, "{prefix}`- ")?;
151
152    color(stdout, Color::White)?;
153    writeln!(stdout, "{last_op}")
154}
155
156#[cfg(test)]
157#[allow(
158    clippy::float_cmp,
159    clippy::cast_precision_loss,
160    clippy::cast_possible_truncation,
161    clippy::cast_sign_loss,
162    clippy::similar_names,
163    clippy::doc_markdown,
164    clippy::needless_raw_string_hashes,
165    clippy::too_many_lines
166)]
167mod tests {
168    use super::*;
169    use crate::spaces::SpaceKind;
170    use termcolor::NoColor;
171
172    fn leaf_ops(operators: Vec<String>, operands: Vec<String>) -> Ops {
173        Ops {
174            name: Some("unit".to_string()),
175            name_was_lossy: false,
176            start_line: 1,
177            end_line: 1,
178            kind: SpaceKind::Unit,
179            spaces: vec![],
180            operators,
181            operands,
182        }
183    }
184
185    fn render(ops: &Ops) -> String {
186        let mut sink = NoColor::new(Vec::new());
187        dump_space(ops, "", true, &mut sink).expect("dump to in-memory sink");
188        String::from_utf8(sink.into_inner()).expect("utf-8 dump")
189    }
190
191    #[test]
192    fn dump_ops_empty_operators_and_operands_does_not_panic() {
193        // Regression: `ops.len() - 1` underflowed (usize) when ops was empty,
194        // then `ops.last().unwrap()` panicked. A space with no Halstead
195        // operators or operands is a realistic input.
196        assert!(dump_ops(&leaf_ops(vec![], vec![])).is_ok());
197    }
198
199    #[test]
200    fn operators_render_mid_child_connector_not_last() {
201        // `operands` always follows `operators`, so in a leaf space the
202        // `operators` line must use the mid-child glyph `|-` and indent
203        // its children under `|  `; the closing `` `- `` belongs to
204        // `operands`. The pre-fix code passed the block's `last` to both,
205        // so `operators` drew `` `- `` and mis-indented its operator
206        // subtree under `   ` (#700).
207        let ops = leaf_ops(vec!["+".to_string()], vec!["a".to_string()]);
208        let out = render(&ops);
209        assert!(
210            out.contains("|- operators"),
211            "operators must use the mid-child connector:\n{out}"
212        );
213        assert!(
214            out.contains("`- operands"),
215            "operands must use the last-child connector:\n{out}"
216        );
217        // The operator leaf indents under `|  ` (operators is not last),
218        // not under the `   ` the buggy last-child glyph would produce.
219        assert!(
220            out.contains("|  `- +"),
221            "operator leaf must indent under the mid-child rail:\n{out}"
222        );
223    }
224
225    #[test]
226    fn deeply_nested_spaces_dump_without_stack_overflow() {
227        // The space walk is iterative (#700): a deep chain of nested
228        // spaces must dump without overflowing the thread stack. Built by
229        // hand so the test is grammar-independent; run on a small-stack
230        // thread so a recursion regression fails loudly.
231        const DEPTH: usize = 8_000;
232        let handle = std::thread::Builder::new()
233            .stack_size(512 * 1024)
234            .spawn(|| {
235                let mut root = leaf_ops(vec!["+".to_string()], vec!["a".to_string()]);
236                let mut cursor = &mut root;
237                for _ in 0..DEPTH {
238                    cursor
239                        .spaces
240                        .push(leaf_ops(vec!["+".to_string()], vec!["a".to_string()]));
241                    cursor = cursor.spaces.last_mut().expect("just pushed");
242                }
243                let mut sink = NoColor::new(Vec::new());
244                let ok = dump_space(&root, "", true, &mut sink).is_ok();
245                // Flatten the chain before it drops: `Ops`'s derived
246                // `Drop` recurses through `spaces`, so a deep tree would
247                // overflow the small stack on teardown and mask the dump
248                // result. Hoisting each level's children out turns the
249                // drop into an iterative one.
250                let mut node = root;
251                while let Some(child) = node.spaces.pop() {
252                    node = child;
253                }
254                ok
255            })
256            .expect("spawn dump thread");
257        assert!(
258            handle.join().expect("dump thread must not overflow"),
259            "deep space nesting must dump successfully"
260        );
261    }
262}