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;