Skip to main content

padlock_core/
findings.rs

1// padlock-core/src/findings.rs
2
3use crate::analysis::{false_sharing, locality, padding, reorder, scorer};
4use crate::ir::{PaddingGap, SharingConflict, StructLayout};
5
6#[derive(Debug, Clone, PartialEq, serde::Serialize)]
7pub enum Severity {
8    Low,
9    Medium,
10    High,
11}
12
13#[derive(Debug, Clone, serde::Serialize)]
14#[serde(tag = "kind")]
15pub enum Finding {
16    PaddingWaste {
17        struct_name: String,
18        total_size: usize,
19        wasted_bytes: usize,
20        waste_pct: f64,
21        gaps: Vec<PaddingGap>,
22        severity: Severity,
23    },
24    FalseSharing {
25        struct_name: String,
26        conflicts: Vec<SharingConflict>,
27        severity: Severity,
28    },
29    ReorderSuggestion {
30        struct_name: String,
31        original_size: usize,
32        optimized_size: usize,
33        savings: usize,
34        suggested_order: Vec<String>,
35        severity: Severity,
36    },
37    LocalityIssue {
38        struct_name: String,
39        hot_fields: Vec<String>,
40        cold_fields: Vec<String>,
41        severity: Severity,
42    },
43}
44
45impl Finding {
46    pub fn severity(&self) -> &Severity {
47        match self {
48            Finding::PaddingWaste { severity, .. } => severity,
49            Finding::FalseSharing { severity, .. } => severity,
50            Finding::ReorderSuggestion { severity, .. } => severity,
51            Finding::LocalityIssue { severity, .. } => severity,
52        }
53    }
54
55    pub fn struct_name(&self) -> &str {
56        match self {
57            Finding::PaddingWaste { struct_name, .. } => struct_name,
58            Finding::FalseSharing { struct_name, .. } => struct_name,
59            Finding::ReorderSuggestion { struct_name, .. } => struct_name,
60            Finding::LocalityIssue { struct_name, .. } => struct_name,
61        }
62    }
63}
64
65#[derive(Debug, serde::Serialize)]
66pub struct StructReport {
67    pub struct_name: String,
68    pub source_file: Option<String>,
69    pub source_line: Option<u32>,
70    pub total_size: usize,
71    /// Number of data fields (excludes padding pseudo-fields).
72    pub num_fields: usize,
73    /// Number of byte-level padding gaps (holes) in the layout.
74    pub num_holes: usize,
75    pub wasted_bytes: usize,
76    pub score: f64,
77    pub findings: Vec<Finding>,
78    /// Mirrors `StructLayout::is_repr_rust`. When true, findings describe
79    /// declared-order waste; the compiler may have already eliminated it.
80    pub is_repr_rust: bool,
81}
82
83#[derive(Debug, serde::Serialize)]
84pub struct Report {
85    pub structs: Vec<StructReport>,
86    pub total_structs: usize,
87    pub total_wasted_bytes: usize,
88    /// Paths that were analyzed to produce this report (populated by the CLI).
89    #[serde(skip_serializing_if = "Vec::is_empty")]
90    pub analyzed_paths: Vec<String>,
91}
92
93impl Report {
94    /// Run all analysis passes over `layouts` and assemble the full report.
95    pub fn from_layouts(layouts: &[StructLayout]) -> Report {
96        let structs: Vec<StructReport> = layouts.iter().map(analyze_one).collect();
97        let total_wasted_bytes = structs.iter().map(|s| s.wasted_bytes).sum();
98        Report {
99            total_structs: structs.len(),
100            total_wasted_bytes,
101            structs,
102            analyzed_paths: Vec::new(),
103        }
104    }
105}
106
107fn analyze_one(layout: &StructLayout) -> StructReport {
108    let mut findings = Vec::new();
109
110    // ── padding waste ────────────────────────────────────────────────────────
111    let gaps = padding::find_padding(layout);
112    let num_holes = gaps.len();
113    let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
114    // Unions: is_union suppresses padding at the find_padding level; no extra check needed.
115    if wasted > 0 {
116        let waste_pct = wasted as f64 / layout.total_size as f64 * 100.0;
117        let severity = if waste_pct >= 30.0 {
118            Severity::High
119        } else if waste_pct >= 10.0 {
120            Severity::Medium
121        } else {
122            Severity::Low
123        };
124        findings.push(Finding::PaddingWaste {
125            struct_name: layout.name.clone(),
126            total_size: layout.total_size,
127            wasted_bytes: wasted,
128            waste_pct,
129            gaps,
130            severity,
131        });
132    }
133
134    // ── reorder suggestion ───────────────────────────────────────────────────
135    // Packed structs have no padding to eliminate; union field order is irrelevant.
136    let (optimized_size, savings) = reorder::reorder_savings(layout);
137    if savings > 0 && !layout.is_packed && !layout.is_union {
138        let suggested_order = reorder::optimal_order(layout)
139            .iter()
140            .map(|f| f.name.clone())
141            .collect();
142        findings.push(Finding::ReorderSuggestion {
143            struct_name: layout.name.clone(),
144            original_size: layout.total_size,
145            optimized_size,
146            savings,
147            suggested_order,
148            severity: if savings >= 8 {
149                Severity::High
150            } else {
151                Severity::Medium
152            },
153        });
154    }
155
156    // ── false sharing ────────────────────────────────────────────────────────
157    // Unions place all fields at offset 0 by definition; that is not false sharing.
158    if !layout.is_union && false_sharing::has_false_sharing(layout) {
159        let conflicts = false_sharing::find_sharing_conflicts(layout);
160        findings.push(Finding::FalseSharing {
161            struct_name: layout.name.clone(),
162            conflicts,
163            severity: Severity::High,
164        });
165    }
166
167    // ── locality ─────────────────────────────────────────────────────────────
168    if locality::has_locality_issue(layout) {
169        let (hot, cold) = locality::partition_hot_cold(layout);
170        findings.push(Finding::LocalityIssue {
171            struct_name: layout.name.clone(),
172            hot_fields: hot,
173            cold_fields: cold,
174            severity: Severity::Medium,
175        });
176    }
177
178    let score = scorer::score(layout);
179
180    StructReport {
181        struct_name: layout.name.clone(),
182        source_file: layout.source_file.clone(),
183        source_line: layout.source_line,
184        total_size: layout.total_size,
185        num_fields: layout.fields.len(),
186        num_holes,
187        wasted_bytes: wasted,
188        score,
189        findings,
190        is_repr_rust: layout.is_repr_rust,
191    }
192}
193
194// ── tests ─────────────────────────────────────────────────────────────────────
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::ir::test_fixtures::{connection_layout, packed_layout};
200
201    #[test]
202    fn report_from_misaligned_has_padding_finding() {
203        let report = Report::from_layouts(&[connection_layout()]);
204        assert_eq!(report.total_structs, 1);
205        let sr = &report.structs[0];
206        assert!(sr.wasted_bytes > 0);
207        assert!(
208            sr.findings
209                .iter()
210                .any(|f| matches!(f, Finding::PaddingWaste { .. }))
211        );
212    }
213
214    #[test]
215    fn report_from_packed_has_no_padding_finding() {
216        let report = Report::from_layouts(&[packed_layout()]);
217        let sr = &report.structs[0];
218        assert_eq!(sr.wasted_bytes, 0);
219        assert!(
220            !sr.findings
221                .iter()
222                .any(|f| matches!(f, Finding::PaddingWaste { .. }))
223        );
224    }
225
226    #[test]
227    fn report_from_misaligned_has_reorder_suggestion() {
228        let report = Report::from_layouts(&[connection_layout()]);
229        let sr = &report.structs[0];
230        assert!(
231            sr.findings
232                .iter()
233                .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
234        );
235    }
236
237    #[test]
238    fn severity_high_when_waste_over_30_pct() {
239        let report = Report::from_layouts(&[connection_layout()]);
240        let sr = &report.structs[0];
241        // Connection wastes 10/24 = 41% → High
242        let padding_finding = sr
243            .findings
244            .iter()
245            .find(|f| matches!(f, Finding::PaddingWaste { .. }))
246            .unwrap();
247        assert_eq!(padding_finding.severity(), &Severity::High);
248    }
249
250    #[test]
251    fn total_wasted_bytes_sums_across_structs() {
252        let report = Report::from_layouts(&[connection_layout(), packed_layout()]);
253        assert_eq!(report.total_structs, 2);
254        assert_eq!(report.total_wasted_bytes, 10); // only Connection wastes bytes
255    }
256}