layout_audit/output/
suggest.rs

1//! Output formatters for suggest command.
2
3use crate::analysis::OptimizedLayout;
4use colored::Colorize;
5use comfy_table::{Cell, Color, Table, presets::UTF8_FULL_CONDENSED};
6use serde::Serialize;
7
8pub struct SuggestTableFormatter {
9    no_color: bool,
10}
11
12impl SuggestTableFormatter {
13    pub fn new(no_color: bool) -> Self {
14        Self { no_color }
15    }
16
17    pub fn format(&self, suggestions: &[OptimizedLayout]) -> String {
18        let mut output = String::new();
19
20        for (i, suggestion) in suggestions.iter().enumerate() {
21            if i > 0 {
22                output.push_str("\n\n");
23            }
24            output.push_str(&self.format_suggestion(suggestion));
25        }
26
27        output
28    }
29
30    fn format_suggestion(&self, s: &OptimizedLayout) -> String {
31        let mut output = String::new();
32
33        // Header with savings summary
34        let header = if s.savings_bytes > 0 {
35            format!(
36                "struct {} ({} bytes -> {} bytes, saves {} bytes / {:.1}%)",
37                s.name, s.original_size, s.optimized_size, s.savings_bytes, s.savings_percent
38            )
39        } else {
40            format!("struct {} ({} bytes, already optimal)", s.name, s.original_size)
41        };
42
43        if self.no_color {
44            output.push_str(&header);
45        } else if s.savings_bytes > 0 {
46            output.push_str(&header.green().bold().to_string());
47        } else {
48            output.push_str(&header.bold().to_string());
49        }
50        output.push_str("\n\n");
51
52        // Current layout
53        output.push_str("Current layout:\n");
54        output.push_str(&self.format_members_table(&s.original_members));
55        output.push('\n');
56
57        // Suggested layout (only if there are savings)
58        if s.savings_bytes > 0 {
59            output.push_str("\nSuggested layout:\n");
60            output.push_str(&self.format_members_table_colored(&s.optimized_members));
61            output.push('\n');
62        }
63
64        // Warnings for skipped members
65        if !s.skipped_members.is_empty() {
66            let warning = format!(
67                "\nWarning: {} member(s) skipped due to missing size/offset: {}",
68                s.skipped_members.len(),
69                s.skipped_members.join(", ")
70            );
71            if self.no_color {
72                output.push_str(&warning);
73            } else {
74                output.push_str(&warning.yellow().to_string());
75            }
76            output.push('\n');
77        }
78
79        // Note about bitfields
80        if s.has_bitfields {
81            let note = "\nNote: Bitfield members kept together in their storage units.";
82            if self.no_color {
83                output.push_str(note);
84            } else {
85                output.push_str(&note.cyan().to_string());
86            }
87            output.push('\n');
88        }
89
90        // FFI warning (always show for optimizable structs)
91        if s.savings_bytes > 0 {
92            let ffi_warning = "\nReordering may affect serialization/FFI compatibility";
93            if self.no_color {
94                output.push_str(ffi_warning);
95            } else {
96                output.push_str(&ffi_warning.yellow().to_string());
97            }
98            output.push('\n');
99        }
100
101        output
102    }
103
104    fn format_members_table(&self, members: &[crate::analysis::OptimizedMember]) -> String {
105        let mut table = Table::new();
106        table.load_preset(UTF8_FULL_CONDENSED);
107        table.set_header(vec!["Offset", "Size", "Align", "Type", "Field"]);
108
109        for m in members {
110            table.add_row(vec![
111                Cell::new(m.offset.to_string()),
112                Cell::new(m.size.to_string()),
113                Cell::new(m.alignment.to_string()),
114                Cell::new(&m.type_name),
115                Cell::new(&m.name),
116            ]);
117        }
118
119        table.to_string()
120    }
121
122    fn format_members_table_colored(&self, members: &[crate::analysis::OptimizedMember]) -> String {
123        let mut table = Table::new();
124        table.load_preset(UTF8_FULL_CONDENSED);
125        table.set_header(vec!["Offset", "Size", "Align", "Type", "Field"]);
126
127        for m in members {
128            let row = if self.no_color {
129                vec![
130                    Cell::new(m.offset.to_string()),
131                    Cell::new(m.size.to_string()),
132                    Cell::new(m.alignment.to_string()),
133                    Cell::new(&m.type_name),
134                    Cell::new(&m.name),
135                ]
136            } else {
137                vec![
138                    Cell::new(m.offset.to_string()).fg(Color::Green),
139                    Cell::new(m.size.to_string()).fg(Color::Green),
140                    Cell::new(m.alignment.to_string()).fg(Color::Green),
141                    Cell::new(&m.type_name).fg(Color::Green),
142                    Cell::new(&m.name).fg(Color::Green),
143                ]
144            };
145            table.add_row(row);
146        }
147
148        table.to_string()
149    }
150}
151
152#[derive(Serialize)]
153struct SuggestJsonOutput<'a> {
154    version: &'static str,
155    suggestions: &'a [OptimizedLayout],
156    summary: SuggestSummary,
157}
158
159#[derive(Serialize)]
160struct SuggestSummary {
161    total_structs: usize,
162    optimizable_structs: usize,
163    total_savings_bytes: u64,
164}
165
166pub struct SuggestJsonFormatter {
167    pretty: bool,
168}
169
170impl SuggestJsonFormatter {
171    pub fn new(pretty: bool) -> Self {
172        Self { pretty }
173    }
174
175    pub fn format(&self, suggestions: &[OptimizedLayout]) -> String {
176        let optimizable = suggestions.iter().filter(|s| s.savings_bytes > 0).count();
177        let total_savings: u64 = suggestions.iter().map(|s| s.savings_bytes).sum();
178
179        let output = SuggestJsonOutput {
180            version: env!("CARGO_PKG_VERSION"),
181            suggestions,
182            summary: SuggestSummary {
183                total_structs: suggestions.len(),
184                optimizable_structs: optimizable,
185                total_savings_bytes: total_savings,
186            },
187        };
188
189        if self.pretty {
190            serde_json::to_string_pretty(&output)
191                .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
192        } else {
193            serde_json::to_string(&output).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    fn suggestion(name: &str, savings: u64) -> OptimizedLayout {
203        OptimizedLayout {
204            name: name.to_string(),
205            original_size: 16,
206            optimized_size: 16 - savings,
207            savings_bytes: savings,
208            savings_percent: if savings == 0 { 0.0 } else { (savings as f64 / 16.0) * 100.0 },
209            struct_alignment: 8,
210            original_members: Vec::new(),
211            optimized_members: Vec::new(),
212            skipped_members: Vec::new(),
213            has_bitfields: false,
214        }
215    }
216
217    #[test]
218    fn suggest_table_includes_warning_and_ffi_note() {
219        let mut s = suggestion("Foo", 4);
220        s.skipped_members = vec!["missing".to_string()];
221        s.has_bitfields = true;
222        let formatter = SuggestTableFormatter::new(true);
223        let out = formatter.format(&[s]);
224        assert!(out.contains("Reordering may affect"));
225        assert!(out.contains("Warning"));
226        assert!(out.contains("Bitfield"));
227    }
228
229    #[test]
230    fn suggest_table_handles_no_savings() {
231        let formatter = SuggestTableFormatter::new(true);
232        let out = formatter.format(&[suggestion("Bar", 0)]);
233        assert!(out.contains("already optimal"));
234    }
235
236    #[test]
237    fn suggest_json_summary_fields() {
238        let formatter = SuggestJsonFormatter::new(true);
239        let out = formatter.format(&[suggestion("A", 4), suggestion("B", 0)]);
240        let parsed: serde_json::Value = serde_json::from_str(&out).expect("valid JSON");
241        assert_eq!(parsed["summary"]["total_structs"], 2);
242        assert_eq!(parsed["summary"]["optimizable_structs"], 1);
243    }
244}