cargo-sonar 1.6.0

Helper to transform reports from Rust tooling for code quality, into valid Sonar report
Documentation
use crate::Issue as _;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
// ALLOW: Unused fields are part of the serialized schema for 'sonar-scanner'
#[allow(dead_code)]
#[non_exhaustive]
pub enum Severity {
    Blocker,
    Critical,
    Major,
    Minor,
    Info,
}

impl From<crate::Severity> for Severity {
    #[inline]
    fn from(severity: crate::Severity) -> Self {
        match severity {
            crate::Severity::Blocker => Self::Blocker,
            crate::Severity::Critical => Self::Critical,
            crate::Severity::Major => Self::Major,
            crate::Severity::Minor => Self::Minor,
            crate::Severity::Info => Self::Info,
        }
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
// ALLOW: Unused fields are part of the serialized schema for 'sonar-scanner'
#[allow(dead_code)]
#[non_exhaustive]
pub enum Category {
    #[serde(rename = "Bug Risk")] // Yes, with a space 🤷
    BugRisk,
    Clarity,
    Compatibility,
    Complexity,
    Duplication,
    Performance,
    Security,
    Style,
}

impl From<crate::Category> for Category {
    #[inline]
    fn from(category: crate::Category) -> Self {
        match category {
            crate::Category::Bug => Self::BugRisk,
            crate::Category::Complexity => Self::Complexity,
            crate::Category::Duplication => Self::Duplication,
            crate::Category::Performance => Self::Performance,
            crate::Category::Security => Self::Security,
            crate::Category::Style => Self::Style,
        }
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub struct LineColumn {
    pub line: usize,
    pub column: usize,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Position {
    Lines { begin: usize, end: usize },
    Positions { begin: LineColumn, end: LineColumn },
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Location {
    pub path: String,
    #[serde(flatten)]
    pub position: Position,
}

impl From<crate::Location> for Location {
    #[inline]
    fn from(location: crate::Location) -> Self {
        Self {
            path: location.path.to_string_lossy().to_string(),
            position: Position::Positions {
                begin: LineColumn {
                    line: location.range.start.line,
                    column: location.range.start.column,
                },
                end: LineColumn {
                    line: location.range.end.line,
                    column: location.range.end.column,
                },
            },
        }
    }
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Issue {
    pub description: String,
    pub check_name: String,
    pub fingerprint: String,
    pub severity: Severity,
    pub location: Location,
}

#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Issues(Vec<Issue>);

impl Issues {
    /// Give the number of generated issues
    #[must_use]
    #[inline]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Returns if there is no issues
    #[must_use]
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

impl FromIterator<Issue> for Issues {
    #[inline]
    fn from_iter<T: IntoIterator<Item = Issue>>(issues: T) -> Self {
        Self(issues.into_iter().collect())
    }
}

#[derive(Debug)]
pub struct CodeClimate;

impl<'c> crate::FromIssues<'c> for CodeClimate {
    type Report = Issues;

    fn from_issues(issues: impl IntoIterator<Item = crate::IssueType<'c>>) -> Self::Report {
        issues
            .into_iter()
            .filter_map(|issue| {
                issue.location().map(|location| Issue {
                    description: location.message.clone(),
                    check_name: issue.issue_uid(),
                    fingerprint: format!("{:x}", issue.fingerprint()),
                    severity: Severity::from(issue.severity()),
                    location: Location::from(location),
                })
            })
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::{CodeClimate, Position, Severity};
    use crate::{test::Issue, FromIssues as _, IssueType};
    use test_log::test;

    #[test]
    fn simple_issue() {
        let issues = CodeClimate::from_issues(vec![IssueType::Test(Issue)]);
        let issue = &issues.0.first().unwrap();
        assert_eq!(issue.check_name, "fake::fake_issue");
        assert!(matches!(issue.severity, Severity::Info));
        assert_eq!(issue.fingerprint, "1d623b89683f9ce4e074de1676d12416");
        assert_eq!(issue.description, "this issue is bad!");
        assert_eq!(issue.location.path, "Cargo.lock");
        let Position::Positions { ref begin, ref end } = issue.location.position else {
            panic!("not matching a Position::Positions variant");
        };
        assert_eq!(begin.line, 1);
        assert_eq!(end.line, 2);
        assert_eq!(begin.column, 1);
        assert_eq!(end.column, 42);
    }
}