1use crate::analysis::OptimizedLayout;
2use crate::diff::DiffResult;
3use crate::types::{SourceLocation, StructLayout};
4use serde::Serialize;
5use serde_json::{Value, json};
6use std::collections::BTreeSet;
7
8const SARIF_VERSION: &str = "2.1.0";
9const SARIF_SCHEMA: &str = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json";
10const TOOL_NAME: &str = "layout-audit";
11const TOOL_URI: &str = "https://github.com/avifenesh/layout-audit";
12
13const RULE_SIZE_INCREASE: &str = "LAYOUT-SIZE-INCREASE";
14const RULE_PADDING_INCREASE: &str = "LAYOUT-PADDING-INCREASE";
15const RULE_BUDGET_SIZE: &str = "LAYOUT-BUDGET-SIZE";
16const RULE_BUDGET_PADDING: &str = "LAYOUT-BUDGET-PADDING";
17const RULE_BUDGET_PADDING_PERCENT: &str = "LAYOUT-BUDGET-PADDING-PERCENT";
18const RULE_BUDGET_FALSE_SHARING: &str = "LAYOUT-BUDGET-FALSE-SHARING";
19const RULE_PADDING: &str = "LAYOUT-PADDING";
20const RULE_FALSE_SHARING: &str = "LAYOUT-FALSE-SHARING";
21const RULE_REORDER_SUGGESTION: &str = "LAYOUT-REORDER-SUGGESTION";
22
23#[derive(Debug, Clone, Copy, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum CheckViolationKind {
26 MaxSize,
27 MaxPaddingBytes,
28 MaxPaddingPercent,
29 MaxFalseSharingWarnings,
30}
31
32#[derive(Debug, Clone, Serialize)]
33pub struct CheckViolation {
34 pub struct_name: String,
35 pub kind: CheckViolationKind,
36 pub message: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub source_location: Option<SourceLocation>,
39}
40
41pub struct SarifFormatter {
42 tool_version: &'static str,
43}
44
45impl SarifFormatter {
46 pub fn new() -> Self {
47 Self { tool_version: env!("CARGO_PKG_VERSION") }
48 }
49
50 pub fn format_diff(&self, diff: &DiffResult, error_on_regression: bool) -> String {
51 let mut results: Vec<Value> = Vec::new();
52 let mut used_rules: BTreeSet<&'static str> = BTreeSet::new();
53 let level = if error_on_regression { "error" } else { "warning" };
54
55 for change in &diff.changed {
56 if change.size_delta > 0 {
57 used_rules.insert(RULE_SIZE_INCREASE);
58 let message = format!(
59 "Struct {} size increased from {} to {} (+{} bytes)",
60 change.name, change.old_size, change.new_size, change.size_delta
61 );
62 results.push(make_result(
63 RULE_SIZE_INCREASE,
64 level,
65 message,
66 change.source_location.as_ref(),
67 Some(json!({
68 "struct": change.name,
69 "old_size": change.old_size,
70 "new_size": change.new_size,
71 "delta": change.size_delta,
72 })),
73 ));
74 }
75
76 if change.padding_delta > 0 {
77 used_rules.insert(RULE_PADDING_INCREASE);
78 let message = format!(
79 "Struct {} padding increased from {} to {} (+{} bytes)",
80 change.name, change.old_padding, change.new_padding, change.padding_delta
81 );
82 results.push(make_result(
83 RULE_PADDING_INCREASE,
84 level,
85 message,
86 change.source_location.as_ref(),
87 Some(json!({
88 "struct": change.name,
89 "old_padding": change.old_padding,
90 "new_padding": change.new_padding,
91 "delta": change.padding_delta,
92 })),
93 ));
94 }
95 }
96
97 let rules = build_rules(&used_rules);
98 render_sarif(self.tool_version, rules, results)
99 }
100
101 pub fn format_check(&self, violations: &[CheckViolation]) -> String {
102 let mut results: Vec<Value> = Vec::new();
103 let mut used_rules: BTreeSet<&'static str> = BTreeSet::new();
104
105 for v in violations {
106 let rule_id = rule_id_for_kind(v.kind);
107 used_rules.insert(rule_id);
108 results.push(make_result(
109 rule_id,
110 "error",
111 v.message.clone(),
112 v.source_location.as_ref(),
113 Some(json!({ "struct": v.struct_name })),
114 ));
115 }
116
117 let rules = build_rules(&used_rules);
118 render_sarif(self.tool_version, rules, results)
119 }
120
121 pub fn format_inspect(&self, layouts: &[StructLayout]) -> String {
122 let mut results: Vec<Value> = Vec::new();
123 let mut used_rules: BTreeSet<&'static str> = BTreeSet::new();
124
125 for layout in layouts {
126 if layout.metrics.padding_bytes > 0 {
127 used_rules.insert(RULE_PADDING);
128 let message = format!(
129 "Struct {} has {} padding bytes ({:.1}% of {} bytes)",
130 layout.name,
131 layout.metrics.padding_bytes,
132 layout.metrics.padding_percentage,
133 layout.size
134 );
135 results.push(make_result(
136 RULE_PADDING,
137 "warning",
138 message,
139 layout.source_location.as_ref(),
140 Some(json!({
141 "struct": layout.name,
142 "size": layout.size,
143 "padding_bytes": layout.metrics.padding_bytes,
144 "padding_percent": layout.metrics.padding_percentage,
145 "cache_lines_spanned": layout.metrics.cache_lines_spanned,
146 })),
147 ));
148 }
149
150 if let Some(fs) = layout.metrics.false_sharing.as_ref() {
151 if !fs.warnings.is_empty() || !fs.spanning_warnings.is_empty() {
152 used_rules.insert(RULE_FALSE_SHARING);
153 let warning_count = fs.warnings.len();
154 let spanning_count = fs.spanning_warnings.len();
155 let message = format!(
156 "Struct {} has {} potential false sharing warning(s) and {} cache-line spanning atomic(s)",
157 layout.name, warning_count, spanning_count
158 );
159 results.push(make_result(
160 RULE_FALSE_SHARING,
161 "warning",
162 message,
163 layout.source_location.as_ref(),
164 Some(json!({
165 "struct": layout.name,
166 "false_sharing_warnings": warning_count,
167 "spanning_warnings": spanning_count,
168 })),
169 ));
170 }
171 }
172 }
173
174 let rules = build_rules(&used_rules);
175 render_sarif(self.tool_version, rules, results)
176 }
177
178 pub fn format_suggest(
179 &self,
180 suggestions: &[OptimizedLayout],
181 locations: &[Option<SourceLocation>],
182 ) -> String {
183 let mut results: Vec<Value> = Vec::new();
184 let mut used_rules: BTreeSet<&'static str> = BTreeSet::new();
185
186 for (idx, suggestion) in suggestions.iter().enumerate() {
187 if suggestion.savings_bytes == 0 {
188 continue;
189 }
190 let location = locations.get(idx).and_then(|loc| loc.as_ref());
191 used_rules.insert(RULE_REORDER_SUGGESTION);
192 let message = format!(
193 "Struct {} can save {} bytes ({:.1}%) by reordering fields",
194 suggestion.name, suggestion.savings_bytes, suggestion.savings_percent
195 );
196 results.push(make_result(
197 RULE_REORDER_SUGGESTION,
198 "note",
199 message,
200 location,
201 Some(json!({
202 "struct": suggestion.name,
203 "original_size": suggestion.original_size,
204 "optimized_size": suggestion.optimized_size,
205 "savings_bytes": suggestion.savings_bytes,
206 "savings_percent": suggestion.savings_percent,
207 })),
208 ));
209 }
210
211 let rules = build_rules(&used_rules);
212 render_sarif(self.tool_version, rules, results)
213 }
214}
215
216impl Default for SarifFormatter {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222fn rule_id_for_kind(kind: CheckViolationKind) -> &'static str {
223 match kind {
224 CheckViolationKind::MaxSize => RULE_BUDGET_SIZE,
225 CheckViolationKind::MaxPaddingBytes => RULE_BUDGET_PADDING,
226 CheckViolationKind::MaxPaddingPercent => RULE_BUDGET_PADDING_PERCENT,
227 CheckViolationKind::MaxFalseSharingWarnings => RULE_BUDGET_FALSE_SHARING,
228 }
229}
230
231fn build_rules(rule_ids: &BTreeSet<&'static str>) -> Vec<Value> {
232 rule_ids
233 .iter()
234 .map(|id| {
235 let (name, short) = rule_metadata(id);
236 json!({
237 "id": id,
238 "name": name,
239 "shortDescription": { "text": short },
240 })
241 })
242 .collect()
243}
244
245fn rule_metadata(rule_id: &str) -> (&'static str, &'static str) {
246 match rule_id {
247 RULE_SIZE_INCREASE => {
248 ("Struct size increased", "Struct size increased relative to baseline")
249 }
250 RULE_PADDING_INCREASE => {
251 ("Struct padding increased", "Struct padding increased relative to baseline")
252 }
253 RULE_BUDGET_SIZE => ("Budget: size", "Struct size exceeded budget"),
254 RULE_BUDGET_PADDING => ("Budget: padding bytes", "Struct padding bytes exceeded budget"),
255 RULE_BUDGET_PADDING_PERCENT => {
256 ("Budget: padding percent", "Struct padding percentage exceeded budget")
257 }
258 RULE_BUDGET_FALSE_SHARING => {
259 ("Budget: false sharing", "Struct false sharing warnings exceeded budget")
260 }
261 RULE_PADDING => ("Padding detected", "Struct contains padding bytes"),
262 RULE_FALSE_SHARING => ("Potential false sharing", "Atomic members share cache lines"),
263 RULE_REORDER_SUGGESTION => {
264 ("Reorder suggestion", "Struct can be reordered to reduce padding")
265 }
266 _ => ("Layout issue", "Layout-audit reported an issue"),
267 }
268}
269
270fn make_result(
271 rule_id: &str,
272 level: &str,
273 message: String,
274 source_location: Option<&SourceLocation>,
275 properties: Option<Value>,
276) -> Value {
277 let mut result = json!({
278 "ruleId": rule_id,
279 "level": level,
280 "message": { "text": message },
281 });
282
283 if let Some(location) = source_location {
284 let loc = json!({
285 "physicalLocation": {
286 "artifactLocation": { "uri": location.file },
287 "region": { "startLine": location.line },
288 }
289 });
290 result["locations"] = json!([loc]);
291 }
292
293 if let Some(props) = properties {
294 result["properties"] = props;
295 }
296
297 result
298}
299
300fn render_sarif(tool_version: &str, rules: Vec<Value>, results: Vec<Value>) -> String {
301 let sarif = json!({
302 "version": SARIF_VERSION,
303 "$schema": SARIF_SCHEMA,
304 "runs": [{
305 "tool": {
306 "driver": {
307 "name": TOOL_NAME,
308 "version": tool_version,
309 "informationUri": TOOL_URI,
310 "rules": rules,
311 }
312 },
313 "results": results,
314 }]
315 });
316
317 serde_json::to_string_pretty(&sarif).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::diff::{DiffResult, MemberChange, MemberChangeKind, StructChange, StructSummary};
324 use crate::types::{
325 CacheLineSpanningWarning, FalseSharingAnalysis, FalseSharingWarning, LayoutMetrics,
326 SourceLocation, StructLayout,
327 };
328
329 fn parse_sarif(s: &str) -> Value {
330 serde_json::from_str(s).expect("valid SARIF JSON")
331 }
332
333 fn basic_layout(name: &str) -> StructLayout {
334 let mut layout = StructLayout::new(name.to_string(), 16, Some(8));
335 layout.metrics = LayoutMetrics::default();
336 layout
337 }
338
339 #[test]
340 fn diff_sarif_empty_results() {
341 let formatter = SarifFormatter::new();
342 let diff = DiffResult {
343 added: Vec::new(),
344 removed: Vec::new(),
345 changed: Vec::new(),
346 unchanged_count: 0,
347 };
348 let sarif = formatter.format_diff(&diff, false);
349 let parsed = parse_sarif(&sarif);
350 let results = parsed["runs"][0]["results"].as_array().unwrap();
351 assert!(results.is_empty());
352 }
353
354 #[test]
355 fn diff_sarif_includes_location_and_rules() {
356 let formatter = SarifFormatter::new();
357 let change = StructChange {
358 name: "Foo".to_string(),
359 old_size: 8,
360 new_size: 16,
361 size_delta: 8,
362 old_padding: 0,
363 new_padding: 4,
364 padding_delta: 4,
365 member_changes: vec![MemberChange {
366 kind: MemberChangeKind::Added,
367 name: "x".to_string(),
368 details: "offset Some(8), size Some(4)".to_string(),
369 }],
370 source_location: Some(SourceLocation { file: "src/foo.c".to_string(), line: 10 }),
371 old_source_location: None,
372 };
373 let diff = DiffResult {
374 added: vec![StructSummary {
375 name: "Bar".to_string(),
376 size: 8,
377 padding_bytes: 0,
378 source_location: None,
379 }],
380 removed: Vec::new(),
381 changed: vec![change],
382 unchanged_count: 0,
383 };
384
385 let sarif = formatter.format_diff(&diff, true);
386 let parsed = parse_sarif(&sarif);
387 let results = parsed["runs"][0]["results"].as_array().unwrap();
388 assert_eq!(results.len(), 2);
389 for result in results {
390 assert!(result["ruleId"].is_string());
391 assert_eq!(result["level"], "error");
392 let locations = result["locations"].as_array().unwrap();
393 assert_eq!(locations[0]["physicalLocation"]["artifactLocation"]["uri"], "src/foo.c");
394 }
395 }
396
397 #[test]
398 fn check_sarif_empty() {
399 let formatter = SarifFormatter::new();
400 let sarif = formatter.format_check(&[]);
401 let parsed = parse_sarif(&sarif);
402 let results = parsed["runs"][0]["results"].as_array().unwrap();
403 assert!(results.is_empty());
404 }
405
406 #[test]
407 fn check_sarif_maps_rules() {
408 let formatter = SarifFormatter::new();
409 let violations = vec![
410 CheckViolation {
411 struct_name: "Foo".to_string(),
412 kind: CheckViolationKind::MaxSize,
413 message: "Foo: size 16 exceeds budget 8 (+8 bytes)".to_string(),
414 source_location: Some(SourceLocation { file: "src/foo.c".to_string(), line: 5 }),
415 },
416 CheckViolation {
417 struct_name: "Bar".to_string(),
418 kind: CheckViolationKind::MaxPaddingPercent,
419 message: "Bar: padding 50.0% exceeds budget 10.0% (+40.0 percentage points)"
420 .to_string(),
421 source_location: None,
422 },
423 ];
424 let sarif = formatter.format_check(&violations);
425 let parsed = parse_sarif(&sarif);
426 let results = parsed["runs"][0]["results"].as_array().unwrap();
427 assert_eq!(results.len(), 2);
428 assert_eq!(results[0]["ruleId"], RULE_BUDGET_SIZE);
429 assert_eq!(results[1]["ruleId"], RULE_BUDGET_PADDING_PERCENT);
430 }
431
432 #[test]
433 fn inspect_sarif_padding_and_false_sharing() {
434 let formatter = SarifFormatter::new();
435 let mut layout = basic_layout("Foo");
436 layout.metrics.padding_bytes = 4;
437 layout.metrics.padding_percentage = 25.0;
438 layout.metrics.cache_lines_spanned = 1;
439 layout.source_location = Some(SourceLocation { file: "src/foo.c".to_string(), line: 3 });
440 layout.metrics.false_sharing = Some(FalseSharingAnalysis {
441 warnings: vec![FalseSharingWarning {
442 member_a: "a".to_string(),
443 member_b: "b".to_string(),
444 cache_line: 0,
445 gap_bytes: 0,
446 }],
447 spanning_warnings: vec![CacheLineSpanningWarning {
448 member: "a".to_string(),
449 type_name: "AtomicU64".to_string(),
450 offset: 0,
451 size: 8,
452 start_cache_line: 0,
453 end_cache_line: 1,
454 lines_spanned: 2,
455 }],
456 atomic_members: Vec::new(),
457 });
458
459 let sarif = formatter.format_inspect(&[layout]);
460 let parsed = parse_sarif(&sarif);
461 let results = parsed["runs"][0]["results"].as_array().unwrap();
462 assert_eq!(results.len(), 2);
463 }
464
465 #[test]
466 fn suggest_sarif_skips_zero_savings() {
467 let formatter = SarifFormatter::new();
468 let no_savings = OptimizedLayout {
469 name: "NoSavings".to_string(),
470 original_size: 16,
471 optimized_size: 16,
472 savings_bytes: 0,
473 savings_percent: 0.0,
474 struct_alignment: 8,
475 original_members: Vec::new(),
476 optimized_members: Vec::new(),
477 skipped_members: Vec::new(),
478 has_bitfields: false,
479 };
480 let mut savings = no_savings.clone();
481 savings.name = "Savings".to_string();
482 savings.optimized_size = 12;
483 savings.savings_bytes = 4;
484 savings.savings_percent = 25.0;
485
486 let sarif = formatter.format_suggest(
487 &[no_savings, savings],
488 &[None, Some(SourceLocation { file: "src/foo.c".to_string(), line: 12 })],
489 );
490 let parsed = parse_sarif(&sarif);
491 let results = parsed["runs"][0]["results"].as_array().unwrap();
492 assert_eq!(results.len(), 1);
493 assert_eq!(results[0]["ruleId"], RULE_REORDER_SUGGESTION);
494 }
495}