Skip to main content

big_code_analysis/
function.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(
8    clippy::enum_glob_use,
9    clippy::needless_pass_by_value,
10    clippy::wildcard_imports
11)]
12
13use std::path::Path;
14
15use termcolor::{Color, WriteColor};
16
17use crate::output::ColorMode;
18use crate::output::color::print_to_stdout;
19use crate::traits::{ParserTrait, Search};
20
21use crate::checker::Checker;
22use crate::getter::Getter;
23
24use crate::tools::{color, intense_color};
25
26/// Function span data.
27#[derive(Debug)]
28pub struct FunctionSpan {
29    /// The function name, or `None` when the name could not be
30    /// resolved from the AST. Mirrors the `Option<String>` name
31    /// convention used by [`crate::FuncSpace`] and [`crate::ops::Ops`].
32    pub name: Option<String>,
33    /// The first line of a function
34    pub start_line: usize,
35    /// The last line of a function
36    pub end_line: usize,
37}
38
39impl FunctionSpan {
40    /// Project this span into its [`crate::wire::FunctionSpan`] form —
41    /// the plain, `Deserialize`-capable record that defines the serialized
42    /// shape.
43    #[must_use]
44    pub fn to_wire(&self) -> crate::wire::FunctionSpan {
45        crate::wire::FunctionSpan::from(self)
46    }
47}
48
49/// Detects the span of each function in a code. Crate-internal walk
50/// core reached through the [`crate::Ast::functions`] seam.
51///
52/// Returns a vector containing the [`FunctionSpan`] of each function
53///
54/// [`FunctionSpan`]: struct.FunctionSpan.html
55pub(crate) fn function<T: ParserTrait>(parser: &T) -> Vec<FunctionSpan> {
56    let root = parser.root();
57    let code = parser.code();
58    let mut spans = Vec::new();
59    root.act_on_node(&mut |n, ancestors| {
60        // `is_func_with_code`, not `promotes_to_func_space_with_code`:
61        // this seam enumerates *functions*, so an Elixir `defmodule`
62        // stays out for the same reason a Rust `impl` block and a Java
63        // class do. Every language but Elixir inherits the byte-less
64        // `is_func` through the default impl (#1162).
65        if T::Checker::is_func_with_code(n, code, ancestors) {
66            let start_line = n.start_row() + 1;
67            // `Node::end_line`, not a blanket `end_row() + 1`: a function
68            // ending at column 0 does not occupy the row it ends on, and
69            // reporting that row put Perl's trailing `sub` past EOF and
70            // out of step with the span `bca metrics` gives the same
71            // function (#1163).
72            let end_line = n.end_line();
73            spans.push(FunctionSpan {
74                name: T::Getter::get_func_name(n, code, ancestors).map(str::to_string),
75                start_line,
76                end_line,
77            });
78        }
79    });
80
81    spans
82}
83
84fn dump_span(span: FunctionSpan, stdout: &mut dyn WriteColor, last: bool) -> std::io::Result<()> {
85    // Build the six (color, intense, segment) entries once, then write them
86    // in one loop. The original 25-line body called color() / intense_color()
87    // and write!()/writeln!() in alternation, scattering 13 `?` exits across
88    // the function — over the per-fn nexits cap. Collapsing into a table
89    // keeps the rendered byte sequence identical (verified by the
90    // `dump_span_ansi_layout_*` tests).
91    //
92    // The `Seg` enum lets the dynamic chunks (span name, start/end
93    // line numbers) reach the writer via `write!` without intermediate
94    // heap allocations, matching the streaming form of the
95    // pre-refactor code.
96    let prefix = if last { "   `- " } else { "   |- " };
97    let (label_color, label) = match &span.name {
98        Some(name) => (Color::Magenta, Seg::NameColon(name)),
99        None => (Color::Red, Seg::Text("error: ")),
100    };
101    // Only the label is intense; the other five entries use `color()`.
102    let segments: [(Color, bool, Seg<'_>); 6] = [
103        (Color::Blue, false, Seg::Text(prefix)),
104        (label_color, true, label),
105        (Color::Green, false, Seg::Text("from line ")),
106        (Color::White, false, Seg::Int(span.start_line)),
107        (Color::Green, false, Seg::Text(" to line ")),
108        (Color::White, false, Seg::IntDot(span.end_line)),
109    ];
110    for (col, intense, seg) in segments {
111        write_seg(stdout, col, intense, seg)?;
112    }
113    Ok(())
114}
115
116/// One segment of `dump_span`'s rendered output. The dynamic chunks
117/// (span name, line numbers) flow straight through `write!` to the
118/// writer — no intermediate `String` allocation.
119#[derive(Clone, Copy)]
120enum Seg<'a> {
121    /// Static text fragment (prefixes, " to line ", etc.).
122    Text(&'a str),
123    /// `start_line` rendered as a plain integer.
124    Int(usize),
125    /// `end_line` rendered as `<n>.\n` — the trailing punctuation and
126    /// newline that close the line.
127    IntDot(usize),
128    /// `<name>: ` — the span name followed by the colon-space label
129    /// separator. Used when the span is not an error.
130    NameColon(&'a str),
131}
132
133fn write_seg(
134    stdout: &mut dyn WriteColor,
135    col: Color,
136    intense: bool,
137    seg: Seg<'_>,
138) -> std::io::Result<()> {
139    if intense {
140        intense_color(stdout, col)?;
141    } else {
142        color(stdout, col)?;
143    }
144    match seg {
145        Seg::Text(s) => stdout.write_all(s.as_bytes()),
146        Seg::Int(n) => write!(stdout, "{n}"),
147        Seg::IntDot(n) => writeln!(stdout, "{n}."),
148        Seg::NameColon(s) => write!(stdout, "{s}: "),
149    }
150}
151
152// Trait-object writer so production passes a locked `StandardStream`
153// (colored stdout) and tests capture rendered bytes via `termcolor::NoColor`
154// over a `Vec<u8>` — matches the dispatch shape of `dump_span` and the
155// `color` / `intense_color` helpers in `tools.rs`.
156fn dump_spans(
157    spans: Vec<FunctionSpan>,
158    path: &Path,
159    stdout: &mut dyn WriteColor,
160) -> std::io::Result<()> {
161    if spans.is_empty() {
162        return Ok(());
163    }
164    intense_color(stdout, Color::Yellow)?;
165    writeln!(stdout, "In file {}", path.display())?;
166    // Consume `spans` by value: cloning to use `split_last` would
167    // allocate each `FunctionSpan`'s `name: String` unnecessarily.
168    let last_idx = spans.len() - 1;
169    for (i, span) in spans.into_iter().enumerate() {
170        dump_span(span, stdout, i == last_idx)?;
171    }
172    color(stdout, Color::White)
173}
174
175/// Render `spans` for `path` to colored stdout (nothing when `spans`
176/// is empty). Callers obtain `spans` from [`crate::Ast::functions`]
177/// and render them without naming the parser surface. Mirrors the
178/// self-contained-stdout shape of [`crate::dump_root`] /
179/// [`crate::dump_ops`].
180///
181/// # Errors
182///
183/// Propagates any [`std::io::Error`] from writing to stdout.
184pub fn dump_function_spans(spans: Vec<FunctionSpan>, path: &Path) -> std::io::Result<()> {
185    dump_function_spans_with_color(spans, path, ColorMode::Always)
186}
187
188/// Like [`dump_function_spans`], but the caller selects the
189/// [`ColorMode`].
190///
191/// `bca` resolves a `--color` flag, the `NO_COLOR` convention, and
192/// stdout tty detection into a mode and passes it here so piped output
193/// is escape-free by default. The bare [`dump_function_spans`] keeps the
194/// historical always-colored behavior for backward compatibility.
195///
196/// # Errors
197///
198/// Propagates any [`std::io::Error`] from writing to stdout.
199pub fn dump_function_spans_with_color(
200    spans: Vec<FunctionSpan>,
201    path: &Path,
202    color_mode: ColorMode,
203) -> std::io::Result<()> {
204    // Skip the stdout lock entirely when there are no spans (the common
205    // case for config / data files in a whole-repo run). `dump_spans`
206    // self-guards too, so direct callers with an empty Vec are safe.
207    if spans.is_empty() {
208        return Ok(());
209    }
210    print_to_stdout(color_mode, |stdout| dump_spans(spans, path, stdout))
211}
212
213#[cfg(test)]
214#[path = "function_tests.rs"]
215mod tests;