layout_audit/output/
suggest.rs1use 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 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 output.push_str("Current layout:\n");
54 output.push_str(&self.format_members_table(&s.original_members));
55 output.push('\n');
56
57 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 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 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(¬e.cyan().to_string());
86 }
87 output.push('\n');
88 }
89
90 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}