1use crate::Issue as _;
2use std::collections::HashMap;
3
4#[derive(Debug, serde::Serialize, serde::Deserialize)]
5#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
6#[allow(dead_code)]
8#[non_exhaustive]
9pub enum Severity {
10 High,
11 Medium,
12 Low,
13}
14
15impl From<crate::Severity> for Severity {
16 #[inline]
17 fn from(severity: crate::Severity) -> Self {
18 match severity {
19 crate::Severity::Blocker | crate::Severity::Critical => Self::High,
20 crate::Severity::Major => Self::Medium,
21 crate::Severity::Minor | crate::Severity::Info => Self::Low,
22 }
23 }
24}
25
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct TextRange {
29 pub start_line: usize,
30 pub end_line: usize,
31 pub start_column: usize,
32 pub end_column: usize,
33}
34
35impl TextRange {
36 #[inline]
37 #[must_use]
38 pub fn new(
39 (start_line, start_column): (usize, usize),
40 (end_line, end_column): (usize, usize),
41 ) -> Self {
42 Self {
43 start_line,
44 end_line,
45 start_column,
46 end_column: end_column.saturating_add(usize::from(
48 start_line == end_line && start_column == end_column,
49 )),
50 }
51 }
52}
53
54impl From<crate::TextRange> for TextRange {
55 #[inline]
56 fn from(range: crate::TextRange) -> Self {
57 Self::new(
58 (range.start.line, range.start.column),
59 (range.end.line, range.end.column),
60 )
61 }
62}
63
64#[derive(Debug, serde::Serialize, serde::Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct Location {
67 pub message: String,
68 pub file_path: String,
69 pub text_range: TextRange,
70}
71
72impl From<crate::Location> for Location {
73 #[inline]
74 fn from(location: crate::Location) -> Self {
75 Self {
76 message: location.message,
77 file_path: location.path.to_string_lossy().into_owned(),
78 text_range: TextRange::from(location.range),
79 }
80 }
81}
82
83#[derive(Debug, serde::Serialize, serde::Deserialize)]
85#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
86#[allow(dead_code)]
88#[non_exhaustive]
89pub enum CleanCodeAttribute {
90 Formatted,
92 Conventional,
93 Identifiable,
94 Clear,
96 Logical,
97 Complete,
98 Efficient,
99 Focused,
101 Distinct,
102 Modular,
103 Tested,
104 Lawful,
106 Trustworthy,
107 Respectful,
108}
109
110#[derive(Debug, serde::Serialize, serde::Deserialize)]
112#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
113#[allow(dead_code)]
115#[non_exhaustive]
116pub enum SoftwareQuality {
117 Maintainability,
118 Reliability,
119 Security,
120}
121
122impl From<crate::Category> for SoftwareQuality {
123 #[inline]
124 fn from(category: crate::Category) -> Self {
125 match category {
126 crate::Category::Bug | crate::Category::Performance => SoftwareQuality::Reliability,
127 crate::Category::Complexity | crate::Category::Duplication | crate::Category::Style => {
128 SoftwareQuality::Maintainability
129 }
130 crate::Category::Security => SoftwareQuality::Security,
131 }
132 }
133}
134
135#[derive(Debug, serde::Serialize, serde::Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct Impact {
138 pub software_quality: SoftwareQuality,
139 pub severity: Severity,
140}
141
142#[derive(Debug, serde::Serialize, serde::Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct Rule {
145 pub id: String,
146 pub name: String,
147 pub description: String,
148 pub engine_id: String,
149 pub clean_code_attribute: CleanCodeAttribute,
150 pub impacts: Vec<Impact>,
151}
152
153#[derive(Debug, serde::Serialize, serde::Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct Issue {
156 pub rule_id: String,
157 pub primary_location: Location,
158 pub secondary_locations: Vec<Location>,
159}
160
161#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
162pub struct Issues {
163 rules: Vec<Rule>,
164 issues: Vec<Issue>,
165}
166
167impl Issues {
168 #[must_use]
170 #[inline]
171 pub fn len(&self) -> usize {
172 self.issues.len()
173 }
174
175 #[must_use]
177 #[inline]
178 pub fn is_empty(&self) -> bool {
179 self.issues.is_empty()
180 }
181}
182
183#[derive(Debug)]
184pub struct Sonar;
185
186impl<'c> crate::FromIssues<'c> for Sonar {
187 type Report = Issues;
188
189 fn from_issues(issues: impl IntoIterator<Item = crate::IssueType<'c>>) -> Self::Report {
190 let (rules, issues) = issues.into_iter().fold(
191 (HashMap::new(), Vec::new()),
192 |(mut rules, mut issues), issue| {
193 let Some(location) = issue.location() else {
194 return (rules, issues);
195 };
196 rules.entry(issue.issue_uid()).or_insert_with(|| Rule {
197 id: issue.issue_uid(),
198 name: issue.issue_id(),
199 description: location.message.clone(),
200 engine_id: issue.analyzer_id(),
201 clean_code_attribute: CleanCodeAttribute::Clear,
202 impacts: vec![Impact {
203 software_quality: SoftwareQuality::from(issue.category()),
204 severity: Severity::from(issue.severity()),
205 }],
206 });
207 issues.push(Issue {
208 rule_id: issue.issue_uid(),
209 primary_location: Location::from(location),
210 secondary_locations: issue
211 .other_locations()
212 .into_iter()
213 .map(Location::from)
214 .collect(),
215 });
216 (rules, issues)
217 },
218 );
219 Self::Report {
220 rules: rules.into_values().collect(),
221 issues,
222 }
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::{Severity, Sonar};
229 use crate::{
230 sonar::{CleanCodeAttribute, SoftwareQuality},
231 test::Issue,
232 FromIssues as _, IssueType,
233 };
234 use test_log::test;
235
236 #[test]
237 fn simple_issue() {
238 let issues = Sonar::from_issues(vec![IssueType::Test(Issue)]);
239 assert_eq!(issues.rules.len(), 1);
240 let rule = &issues.rules.first().unwrap();
241 assert_eq!(rule.id, "fake::fake_issue");
242 assert_eq!(rule.engine_id, "fake");
243 assert_eq!(rule.name, "fake_issue");
244 assert_eq!(rule.description, "this issue is bad!");
245 assert!(matches!(
246 rule.clean_code_attribute,
247 CleanCodeAttribute::Clear
248 ));
249 assert_eq!(rule.impacts.len(), 1);
250 let impact = &rule.impacts.first().unwrap();
251 assert!(matches!(
252 impact.software_quality,
253 SoftwareQuality::Maintainability
254 ));
255 assert!(matches!(impact.severity, Severity::Low));
256 assert_eq!(issues.issues.len(), 1);
257 let issue = &issues.issues.first().unwrap();
258 assert_eq!(issue.rule_id, "fake::fake_issue");
259 assert_eq!(issue.primary_location.message, "this issue is bad!");
260 assert_eq!(issue.primary_location.file_path, "Cargo.lock");
261 assert_eq!(issue.primary_location.text_range.start_line, 1);
262 assert_eq!(issue.primary_location.text_range.end_line, 2);
263 assert_eq!(issue.primary_location.text_range.start_column, 1);
264 assert_eq!(issue.primary_location.text_range.end_column, 42);
265 assert!(issue.secondary_locations.is_empty());
266 }
267}