Skip to main content

aptu_coder_core/formatter/
render.rs

1// SPDX-FileCopyrightText: 2026 aptu-coder contributors
2// SPDX-License-Identifier: Apache-2.0
3//! Per-entity formatters for file details, module info, and summary file details.
4
5use crate::types::{ModuleInfo, SemanticAnalysis};
6use std::fmt::Write;
7use std::path::Path;
8use thiserror::Error;
9use tracing::instrument;
10
11use super::emit;
12
13#[derive(Debug, Error)]
14pub enum FormatterError {
15    #[error("Graph error: {0}")]
16    GraphError(#[from] crate::graph::GraphError),
17}
18
19/// Format file-level semantic analysis results.
20#[instrument(skip_all)]
21pub fn format_file_details(
22    path: &str,
23    analysis: &SemanticAnalysis,
24    line_count: usize,
25    is_test: bool,
26    base_path: Option<&Path>,
27) -> String {
28    let mut output = String::new();
29
30    let display_path = emit::strip_base_path(Path::new(path), base_path);
31    let fn_count = analysis.functions.len();
32    let class_count = analysis.classes.len();
33    let import_count = analysis.imports.len();
34    if is_test {
35        let _ = writeln!(
36            output,
37            "FILE [TEST] {display_path}({line_count}L, {fn_count}F, {class_count}C, {import_count}I)"
38        );
39    } else {
40        let _ = writeln!(
41            output,
42            "FILE: {display_path}({line_count}L, {fn_count}F, {class_count}C, {import_count}I)"
43        );
44    }
45
46    output.push_str(&emit::format_classes_section(
47        &analysis.classes,
48        &analysis.functions,
49    ));
50
51    let top_level_functions: Vec<&crate::types::FunctionInfo> = analysis
52        .functions
53        .iter()
54        .filter(|func| {
55            !analysis
56                .classes
57                .iter()
58                .any(|class| emit::is_method_of_class(func, class))
59        })
60        .collect();
61
62    if !top_level_functions.is_empty() {
63        output.push_str("F:\n");
64        output.push_str(&emit::format_function_list_wrapped(
65            top_level_functions.iter().copied(),
66            &analysis.call_frequency,
67        ));
68    }
69
70    output.push_str(&emit::format_imports_section(&analysis.imports));
71
72    output
73}
74
75/// Format a compact summary of file details for large `FileDetails` output.
76#[instrument(skip_all)]
77pub fn format_file_details_summary(
78    semantic: &SemanticAnalysis,
79    path: &str,
80    line_count: usize,
81) -> String {
82    let mut output = String::new();
83
84    output.push_str("FILE:\n");
85    let _ = writeln!(output, "  path: {path}");
86    let fn_count = semantic.functions.len();
87    let class_count = semantic.classes.len();
88    let _ = writeln!(output, "  {line_count}L, {fn_count}F, {class_count}C");
89    output.push('\n');
90
91    if !semantic.functions.is_empty() {
92        output.push_str("TOP FUNCTIONS BY SIZE:\n");
93        let mut funcs: Vec<&crate::types::FunctionInfo> = semantic.functions.iter().collect();
94        let k = funcs.len().min(10);
95        if k > 0 {
96            funcs.select_nth_unstable_by(k.saturating_sub(1), |a, b| {
97                let a_span = a.end_line.saturating_sub(a.line);
98                let b_span = b.end_line.saturating_sub(b.line);
99                b_span.cmp(&a_span)
100            });
101            funcs[..k].sort_by(|a, b| {
102                let a_span = a.end_line.saturating_sub(a.line);
103                let b_span = b.end_line.saturating_sub(b.line);
104                b_span.cmp(&a_span)
105            });
106        }
107
108        for func in &funcs[..k] {
109            let span = func.end_line.saturating_sub(func.line);
110            let params = if func.parameters.is_empty() {
111                String::new()
112            } else {
113                format!("({})", func.parameters.join(", "))
114            };
115            let _ = writeln!(
116                output,
117                "  {}:{}: {} {} [{}L]",
118                func.line, func.end_line, func.name, params, span
119            );
120        }
121        output.push('\n');
122    }
123
124    if !semantic.classes.is_empty() {
125        output.push_str("CLASSES:\n");
126        if semantic.classes.len() <= 10 {
127            let class_strs: Vec<String> = semantic
128                .classes
129                .iter()
130                .map(|c| {
131                    if c.inherits.is_empty() {
132                        format!("{}:{}-{}", c.name, c.line, c.end_line)
133                    } else {
134                        format!(
135                            "{}:{}-{} ({})",
136                            c.name,
137                            c.line,
138                            c.end_line,
139                            c.inherits.join(", ")
140                        )
141                    }
142                })
143                .collect();
144            let _ = writeln!(output, "  {}", class_strs.join("; "));
145        } else {
146            let _ = writeln!(output, "  {} classes total", semantic.classes.len());
147            for class in semantic.classes.iter().take(5) {
148                let _ = writeln!(output, "    {}", class.name);
149            }
150            if semantic.classes.len() > 5 {
151                let _ = writeln!(output, "    ... and {} more", semantic.classes.len() - 5);
152            }
153        }
154        output.push('\n');
155    }
156
157    if !semantic.imports.is_empty() {
158        output.push_str("IMPORTS:\n");
159        let import_strs: Vec<String> = semantic
160            .imports
161            .iter()
162            .map(|i| {
163                if i.items.is_empty() {
164                    format!("  {}", i.module)
165                } else {
166                    format!("  {}: {}", i.module, i.items.join(", "))
167                }
168            })
169            .collect();
170        for s in import_strs {
171            let _ = writeln!(output, "{s}");
172        }
173        output.push('\n');
174    }
175
176    output.push_str("SUGGESTION:\n");
177    output.push_str("Use analyze_file for full function list, signatures, imports, and classes\n");
178
179    output
180}
181
182/// Format a [`ModuleInfo`] into a compact single-block string.
183#[instrument(skip_all)]
184pub fn format_module_info(info: &ModuleInfo) -> String {
185    use std::fmt::Write as _;
186    let fn_count = info.functions.len();
187    let import_count = info.imports.len();
188    let mut out = String::with_capacity(64 + fn_count * 24 + import_count * 32);
189    let _ = writeln!(
190        out,
191        "FILE: {} ({}L, {}F, {}I)",
192        info.name, info.line_count, fn_count, import_count
193    );
194    if !info.functions.is_empty() {
195        out.push_str("F:\n  ");
196        let parts: Vec<String> = info
197            .functions
198            .iter()
199            .map(|f| format!("{}:{}", f.name, f.line))
200            .collect();
201        out.push_str(&parts.join(", "));
202        out.push('\n');
203    }
204    if !info.imports.is_empty() {
205        out.push_str("I:\n  ");
206        let parts: Vec<String> = info
207            .imports
208            .iter()
209            .map(|i| {
210                if i.items.is_empty() {
211                    i.module.clone()
212                } else {
213                    format!("{}:{}", i.module, i.items.join(", "))
214                }
215            })
216            .collect();
217        out.push_str(&parts.join("; "));
218        out.push('\n');
219    }
220    out
221}