1use serde::{Deserialize, Serialize};
2
3use crate::config::Level;
4use crate::rules::{RuleBackend, RuleFamily};
5use crate::span::Span;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Severity {
10 Info,
11 Warn,
12 Deny,
13}
14
15impl Severity {
16 #[must_use]
17 pub fn exit_code(self) -> i32 {
18 match self {
19 Self::Info => 0,
20 Self::Warn => 1,
21 Self::Deny => 2,
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum FixSafety {
29 Safe,
30 Unsafe,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum FindingLabelKind {
36 Primary,
37 Secondary,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct FindingLabel {
42 pub kind: FindingLabelKind,
43 pub span: Span,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub message: Option<String>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum FindingNoteKind {
51 Note,
52 Help,
53 Info,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct FindingNote {
58 pub kind: FindingNoteKind,
59 pub message: String,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct TextEdit {
64 pub file: String,
65 pub byte_start: u32,
66 pub byte_end: u32,
67 pub replacement: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Fix {
72 pub id: String,
73 pub safety: FixSafety,
74 pub message: String,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 pub edits: Vec<TextEdit>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Finding {
81 pub rule_id: String,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub family: Option<RuleFamily>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub engine: Option<RuleBackend>,
86 pub severity: Severity,
87 pub message: String,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub primary: Option<Span>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub secondary: Vec<Span>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub help: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub evidence: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub confidence: Option<String>,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub tags: Vec<String>,
100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub labels: Vec<FindingLabel>,
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
103 pub notes: Vec<FindingNote>,
104 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub fixes: Vec<Fix>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct RuleCatalogEntry {
110 pub id: String,
111 pub family: RuleFamily,
112 pub backend: RuleBackend,
113 pub default_level: Level,
114 pub summary: String,
115 #[serde(default)]
116 pub fixable: bool,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct AdapterRun {
121 pub name: String,
122 pub enabled: bool,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub toolchain: Option<String>,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub status: Option<String>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ToolchainSummary {
131 pub requested: String,
132 pub resolved: String,
133 pub semantic: String,
134 #[serde(default)]
135 pub nightly_available: bool,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub reason: Option<String>,
138}
139
140#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141pub struct RunSummary {
142 #[serde(default)]
143 pub engine_used: Vec<RuleBackend>,
144 #[serde(default)]
145 pub semantic_backend_available: bool,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub semantic_backend_reason: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub toolchain: Option<ToolchainSummary>,
150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
151 pub skipped_rules: Vec<String>,
152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
153 pub adapter_runs: Vec<AdapterRun>,
154}
155
156#[derive(Debug, Clone, Default, Serialize, Deserialize)]
157pub struct Metrics {
158 #[serde(default)]
159 pub per_file: Vec<FileMetrics>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct FileMetrics {
164 pub path: String,
165 #[serde(default)]
166 pub cyclomatic_sum: u32,
167 #[serde(default)]
168 pub cyclomatic_max_fn: u32,
169}
170
171#[derive(Debug, Clone, Default, Serialize, Deserialize)]
172pub struct Report {
173 #[serde(default)]
174 pub findings: Vec<Finding>,
175 #[serde(default)]
176 pub metrics: Metrics,
177 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 pub rule_catalog: Vec<RuleCatalogEntry>,
179 #[serde(default)]
180 pub summary: RunSummary,
181}
182
183impl Report {
184 pub fn worst_severity(&self) -> Severity {
185 self.findings
186 .iter()
187 .map(|f| f.severity)
188 .max_by_key(|s| s.exit_code())
189 .unwrap_or(Severity::Info)
190 }
191}