cargo_sonar/
codeclimate.rs

1use crate::Issue as _;
2
3#[derive(Debug, serde::Serialize, serde::Deserialize)]
4#[serde(rename_all = "lowercase")]
5// ALLOW: Unused fields are part of the serialized schema for 'sonar-scanner'
6#[allow(dead_code)]
7#[non_exhaustive]
8pub enum Severity {
9    Blocker,
10    Critical,
11    Major,
12    Minor,
13    Info,
14}
15
16impl From<crate::Severity> for Severity {
17    #[inline]
18    fn from(severity: crate::Severity) -> Self {
19        match severity {
20            crate::Severity::Blocker => Self::Blocker,
21            crate::Severity::Critical => Self::Critical,
22            crate::Severity::Major => Self::Major,
23            crate::Severity::Minor => Self::Minor,
24            crate::Severity::Info => Self::Info,
25        }
26    }
27}
28
29#[derive(Debug, serde::Serialize, serde::Deserialize)]
30#[serde(rename_all = "lowercase")]
31// ALLOW: Unused fields are part of the serialized schema for 'sonar-scanner'
32#[allow(dead_code)]
33#[non_exhaustive]
34pub enum Category {
35    #[serde(rename = "Bug Risk")] // Yes, with a space 🤷
36    BugRisk,
37    Clarity,
38    Compatibility,
39    Complexity,
40    Duplication,
41    Performance,
42    Security,
43    Style,
44}
45
46impl From<crate::Category> for Category {
47    #[inline]
48    fn from(category: crate::Category) -> Self {
49        match category {
50            crate::Category::Bug => Self::BugRisk,
51            crate::Category::Complexity => Self::Complexity,
52            crate::Category::Duplication => Self::Duplication,
53            crate::Category::Performance => Self::Performance,
54            crate::Category::Security => Self::Security,
55            crate::Category::Style => Self::Style,
56        }
57    }
58}
59
60#[derive(Debug, serde::Serialize, serde::Deserialize)]
61#[serde(rename_all = "lowercase")]
62pub struct LineColumn {
63    pub line: usize,
64    pub column: usize,
65}
66
67#[derive(Debug, serde::Serialize, serde::Deserialize)]
68#[serde(rename_all = "lowercase")]
69#[non_exhaustive]
70pub enum Position {
71    Lines { begin: usize, end: usize },
72    Positions { begin: LineColumn, end: LineColumn },
73}
74
75#[derive(Debug, serde::Serialize, serde::Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct Location {
78    pub path: String,
79    #[serde(flatten)]
80    pub position: Position,
81}
82
83impl From<crate::Location> for Location {
84    #[inline]
85    fn from(location: crate::Location) -> Self {
86        Self {
87            path: location.path.to_string_lossy().to_string(),
88            position: Position::Positions {
89                begin: LineColumn {
90                    line: location.range.start.line,
91                    column: location.range.start.column,
92                },
93                end: LineColumn {
94                    line: location.range.end.line,
95                    column: location.range.end.column,
96                },
97            },
98        }
99    }
100}
101
102#[derive(Debug, serde::Serialize, serde::Deserialize)]
103#[serde(rename_all = "snake_case")]
104pub struct Issue {
105    pub description: String,
106    pub check_name: String,
107    pub fingerprint: String,
108    pub severity: Severity,
109    pub location: Location,
110}
111
112#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
113pub struct Issues(Vec<Issue>);
114
115impl Issues {
116    /// Give the number of generated issues
117    #[must_use]
118    #[inline]
119    pub fn len(&self) -> usize {
120        self.0.len()
121    }
122
123    /// Returns if there is no issues
124    #[must_use]
125    #[inline]
126    pub fn is_empty(&self) -> bool {
127        self.0.is_empty()
128    }
129}
130
131impl FromIterator<Issue> for Issues {
132    #[inline]
133    fn from_iter<T: IntoIterator<Item = Issue>>(issues: T) -> Self {
134        Self(issues.into_iter().collect())
135    }
136}
137
138#[derive(Debug)]
139pub struct CodeClimate;
140
141impl<'c> crate::FromIssues<'c> for CodeClimate {
142    type Report = Issues;
143
144    fn from_issues(issues: impl IntoIterator<Item = crate::IssueType<'c>>) -> Self::Report {
145        issues
146            .into_iter()
147            .filter_map(|issue| {
148                issue.location().map(|location| Issue {
149                    description: location.message.clone(),
150                    check_name: issue.issue_uid(),
151                    fingerprint: format!("{:x}", issue.fingerprint()),
152                    severity: Severity::from(issue.severity()),
153                    location: Location::from(location),
154                })
155            })
156            .collect()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::{CodeClimate, Position, Severity};
163    use crate::{test::Issue, FromIssues as _, IssueType};
164    use test_log::test;
165
166    #[test]
167    fn simple_issue() {
168        let issues = CodeClimate::from_issues(vec![IssueType::Test(Issue)]);
169        let issue = &issues.0.first().unwrap();
170        assert_eq!(issue.check_name, "fake::fake_issue");
171        assert!(matches!(issue.severity, Severity::Info));
172        assert_eq!(issue.fingerprint, "1d623b89683f9ce4e074de1676d12416");
173        assert_eq!(issue.description, "this issue is bad!");
174        assert_eq!(issue.location.path, "Cargo.lock");
175        let Position::Positions { ref begin, ref end } = issue.location.position else {
176            panic!("not matching a Position::Positions variant");
177        };
178        assert_eq!(begin.line, 1);
179        assert_eq!(end.line, 2);
180        assert_eq!(begin.column, 1);
181        assert_eq!(end.column, 42);
182    }
183}