Skip to main content

xbp_analysis/domain/
types.rs

1//! Core domain value objects.
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// Stable language identifier (not tied to any toolchain).
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
8#[serde(transparent)]
9pub struct LanguageId(pub String);
10
11impl LanguageId {
12    pub fn new(id: impl Into<String>) -> Self {
13        Self(id.into())
14    }
15
16    pub fn as_str(&self) -> &str {
17        &self.0
18    }
19
20    pub fn rust() -> Self {
21        Self("rust".into())
22    }
23}
24
25impl std::fmt::Display for LanguageId {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.write_str(&self.0)
28    }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum Severity {
34    Info,
35    Low,
36    Medium,
37    High,
38    Critical,
39}
40
41impl Severity {
42    pub fn as_str(self) -> &'static str {
43        match self {
44            Self::Info => "info",
45            Self::Low => "low",
46            Self::Medium => "medium",
47            Self::High => "high",
48            Self::Critical => "critical",
49        }
50    }
51
52    pub fn parse(s: &str) -> Option<Self> {
53        match s.to_ascii_lowercase().as_str() {
54            "info" => Some(Self::Info),
55            "low" => Some(Self::Low),
56            "medium" | "med" => Some(Self::Medium),
57            "high" => Some(Self::High),
58            "critical" | "crit" => Some(Self::Critical),
59            _ => None,
60        }
61    }
62}
63
64impl std::fmt::Display for Severity {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.write_str(self.as_str())
67    }
68}
69
70/// How certain the analyzer is about the finding.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "lowercase")]
73pub enum Confidence {
74    Low,
75    Medium,
76    High,
77    Definite,
78}
79
80impl Confidence {
81    pub fn as_str(self) -> &'static str {
82        match self {
83            Self::Low => "low",
84            Self::Medium => "medium",
85            Self::High => "high",
86            Self::Definite => "definite",
87        }
88    }
89
90    /// Definite bugs vs review-required practice.
91    pub fn is_definite_bug(self) -> bool {
92        matches!(self, Self::Definite)
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct SourceLocation {
98    pub file: PathBuf,
99    pub start_line: u32,
100    pub start_column: u32,
101    pub end_line: u32,
102    pub end_column: u32,
103}
104
105impl SourceLocation {
106    pub fn single_line(file: impl Into<PathBuf>, line: u32, column: u32) -> Self {
107        Self {
108            file: file.into(),
109            start_line: line,
110            start_column: column,
111            end_line: line,
112            end_column: column.saturating_add(1),
113        }
114    }
115}
116
117/// Snippet / surrounding lines for a diagnostic.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
119pub struct CodeContext {
120    pub before: Vec<String>,
121    pub highlight: String,
122    pub after: Vec<String>,
123}
124
125/// Structured diagnostic message attached to a finding.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct Diagnostic {
128    pub message: String,
129    pub location: SourceLocation,
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub context: Option<CodeContext>,
132}
133
134/// Evidence that led to a finding (language-agnostic).
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct DetectionEvidence {
137    pub kind: String,
138    pub detail: String,
139}
140
141/// One analysis finding.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct Finding {
144    pub id: String,
145    pub rule_id: String,
146    pub severity: Severity,
147    pub confidence: Confidence,
148    pub language: LanguageId,
149    pub location: SourceLocation,
150    pub explanation: String,
151    pub failure_scenario: String,
152    pub remediation: String,
153    pub evidence: Vec<DetectionEvidence>,
154    /// True when the tool treats this as a definite bug; false = review-required practice.
155    pub definite_bug: bool,
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub context: Option<CodeContext>,
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub fix_hint: Option<String>,
160}
161
162impl Finding {
163    pub fn builder(rule_id: impl Into<String>, language: LanguageId) -> FindingBuilder {
164        FindingBuilder {
165            rule_id: rule_id.into(),
166            language,
167            severity: Severity::Medium,
168            confidence: Confidence::Medium,
169            location: None,
170            explanation: String::new(),
171            failure_scenario: String::new(),
172            remediation: String::new(),
173            evidence: Vec::new(),
174            context: None,
175            fix_hint: None,
176            id: None,
177        }
178    }
179}
180
181pub struct FindingBuilder {
182    rule_id: String,
183    language: LanguageId,
184    severity: Severity,
185    confidence: Confidence,
186    location: Option<SourceLocation>,
187    explanation: String,
188    failure_scenario: String,
189    remediation: String,
190    evidence: Vec<DetectionEvidence>,
191    context: Option<CodeContext>,
192    fix_hint: Option<String>,
193    id: Option<String>,
194}
195
196impl FindingBuilder {
197    pub fn severity(mut self, s: Severity) -> Self {
198        self.severity = s;
199        self
200    }
201    pub fn confidence(mut self, c: Confidence) -> Self {
202        self.confidence = c;
203        self
204    }
205    pub fn location(mut self, loc: SourceLocation) -> Self {
206        self.location = Some(loc);
207        self
208    }
209    pub fn explanation(mut self, e: impl Into<String>) -> Self {
210        self.explanation = e.into();
211        self
212    }
213    pub fn failure_scenario(mut self, e: impl Into<String>) -> Self {
214        self.failure_scenario = e.into();
215        self
216    }
217    pub fn remediation(mut self, e: impl Into<String>) -> Self {
218        self.remediation = e.into();
219        self
220    }
221    pub fn evidence(mut self, kind: impl Into<String>, detail: impl Into<String>) -> Self {
222        self.evidence.push(DetectionEvidence {
223            kind: kind.into(),
224            detail: detail.into(),
225        });
226        self
227    }
228    pub fn context(mut self, c: CodeContext) -> Self {
229        self.context = Some(c);
230        self
231    }
232    pub fn fix_hint(mut self, h: impl Into<String>) -> Self {
233        self.fix_hint = Some(h.into());
234        self
235    }
236    pub fn id(mut self, id: impl Into<String>) -> Self {
237        self.id = Some(id.into());
238        self
239    }
240    pub fn build(self) -> Finding {
241        let location = self.location.expect("Finding requires location");
242        let definite_bug = self.confidence.is_definite_bug();
243        let id = self.id.unwrap_or_else(|| {
244            format!(
245                "{}:{}:{}:{}",
246                self.rule_id,
247                location.file.display(),
248                location.start_line,
249                location.start_column
250            )
251        });
252        Finding {
253            id,
254            rule_id: self.rule_id,
255            severity: self.severity,
256            confidence: self.confidence,
257            language: self.language,
258            location,
259            explanation: self.explanation,
260            failure_scenario: self.failure_scenario,
261            remediation: self.remediation,
262            evidence: self.evidence,
263            definite_bug,
264            context: self.context,
265            fix_hint: self.fix_hint,
266        }
267    }
268}
269
270/// Suppression entry (inline comment or config).
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272pub struct Suppression {
273    pub rule_id: Option<String>,
274    pub file: Option<PathBuf>,
275    pub line: Option<u32>,
276    pub reason: Option<String>,
277    pub pattern: Option<String>,
278}
279
280/// Approved exceptional pattern (config allowlist).
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282pub struct ApprovedPattern {
283    pub rule_id: Option<String>,
284    pub path_glob: Option<String>,
285    pub code_contains: Option<String>,
286    pub reason: String,
287}
288
289/// Source file payload (path + UTF-8 text). Domain-level; loading is an outbound port.
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct SourceFile {
292    pub path: PathBuf,
293    pub content: String,
294    pub language: LanguageId,
295}
296
297/// Control-flow sketch for error paths (language-agnostic).
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
299pub struct ControlFlow {
300    pub nodes: Vec<String>,
301    pub edges: Vec<(usize, usize)>,
302}
303
304/// Ordered path from an error signal to a sink (ignore, log, terminate, recover).
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
306pub struct ErrorPath {
307    pub steps: Vec<String>,
308    pub terminates: bool,
309    pub recovers: bool,
310    pub context_lost: bool,
311}