cargo-sonar 1.6.0

Helper to transform reports from Rust tooling for code quality, into valid Sonar report
Documentation
use crate::{cargo::Lockfile, Category, Location, Severity};
use dyn_iter::IntoDynIterator as _;
use eyre::Result;
use std::io::{BufRead as _, BufReader};

const OUTDATED_ENGINE: &str = "outdated";

#[derive(Debug, serde::Deserialize)]
pub struct CrateMetadata {
    pub crate_name: String,
    pub dependencies: Vec<Metadata>,
}

#[derive(Debug, serde::Deserialize)]
pub struct Metadata {
    pub name: String,
    pub project: String,
    pub compat: String,
    pub latest: String,
    pub kind: Option<String>,
    pub platform: Option<String>,
}

#[derive(Debug)]
pub struct Outdated<'lock> {
    issues: dyn_iter::DynIter<'lock, Issue<'lock>>,
}

impl<'lock> Iterator for Outdated<'lock> {
    type Item = Issue<'lock>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        self.issues.next()
    }
}

impl<'lock> Outdated<'lock> {
    /// Create a Outdated parser for issues
    ///
    /// # Errors
    /// May fail reading and parsing the file (IO errors).
    #[inline]
    pub fn try_new<R>(json_read: R, lockfile: &'lock Lockfile) -> Result<Self>
    where
        R: std::io::Read + 'static,
    {
        let reader = BufReader::new(json_read);
        let issues = reader
            .lines()
            .map_while(Result::ok)
            .flat_map(|line| serde_json::from_str::<CrateMetadata>(&line))
            .flat_map(|crate_metadata| {
                crate_metadata
                    .dependencies
                    .into_iter()
                    .map(move |dependency| (crate_metadata.crate_name.clone(), dependency))
            })
            .map(move |(crate_name, dependency)| (lockfile, crate_name, dependency))
            .into_dyn_iter();
        let outdated = Self { issues };
        Ok(outdated)
    }
}

pub type CrateName = String;
pub type Issue<'lock> = (&'lock Lockfile, CrateName, Metadata);

impl crate::Issue for Issue<'_> {
    #[inline]
    fn analyzer_id(&self) -> String {
        OUTDATED_ENGINE.to_owned()
    }

    #[inline]
    fn issue_id(&self) -> String {
        self.2.name.clone()
    }

    #[inline]
    fn fingerprint(&self) -> md5::Digest {
        md5::compute(format!("{}:{}", self.1, self.2.name))
    }

    #[inline]
    fn category(&self) -> Category {
        Category::Security
    }

    #[inline]
    fn severity(&self) -> Severity {
        Severity::Minor
    }

    #[inline]
    fn location(&self) -> Option<Location> {
        let message = format!(
            "'{}' in crate '{}' is outdated and can be updated up to '{}'",
            &self.2.name, &self.1, &self.2.latest
        );
        let path = self.0.lockfile_path.clone();
        let range = self.0.dependency_range(self.2.name.as_str());
        let location = Location {
            path,
            range,
            message,
        };
        Some(location)
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        Category, Issue as _, Severity, TextRange,
        {cargo::PackageRange, outdated::Outdated, Lockfile},
    };
    use std::{io::Write as _, path::PathBuf};
    use test_log::test;

    #[test]
    fn single_issue() {
        let json = r#"{
          "crate_name": "cargo-sonar",
          "dependencies": [
            {
              "name": "clap",
              "project": "4.3.8",
              "compat": "4.3.16",
              "latest": "4.3.16",
              "kind": "Normal",
              "platform": null
            }
          ]
        }
        <NEW_LINE>
        {
          "crate_name": "cargo-codeclimate",
          "dependencies": [
            {
              "name": "clap",
              "project": "4.3.8",
              "compat": "4.3.16",
              "latest": "4.3.16",
              "kind": "Normal",
              "platform": null
            }
          ]
        }"#;
        let json = json
            .to_owned()
            .replace('\n', "")
            .replace("<NEW_LINE>", "\n");
        let mut outdated_json = tempfile::NamedTempFile::new().unwrap();
        write!(outdated_json, "{}", json).unwrap();
        let outdated_json = outdated_json.reopen().unwrap();

        let lockfile = Lockfile {
            lockfile_path: PathBuf::from("Cargo.lock"),
            dependencies: [(
                "clap".to_owned(),
                PackageRange {
                    range: TextRange::new((175, 1), (184, 2)),
                    name_range: TextRange::new((176, 9), (176, 12)),
                    version_range: TextRange::new((177, 12), (177, 16)),
                },
            )]
            .into_iter()
            .collect(),
        };

        let mut outdated = Outdated::try_new(outdated_json, &lockfile).unwrap();
        let issue = outdated.next().unwrap();
        assert_eq!(issue.analyzer_id(), "outdated");
        assert_eq!(issue.issue_uid(), "outdated::clap");
        assert!(matches!(issue.severity(), Severity::Minor));
        assert!(matches!(issue.category(), Category::Security));
        let location = issue.location().unwrap();
        assert_eq!(location.path, PathBuf::from("Cargo.lock"));
        assert_eq!(
            location.message,
            "'clap' in crate 'cargo-sonar' is outdated and can be updated up to '4.3.16'"
        );
        assert_eq!(location.range.start.line, 175);
        assert_eq!(location.range.end.line, 184);
        assert_eq!(location.range.start.column, 1);
        assert_eq!(location.range.end.column, 2);

        let issue = outdated.next().unwrap();
        assert_eq!(issue.analyzer_id(), "outdated");
        assert_eq!(issue.issue_uid(), "outdated::clap");
        assert!(matches!(issue.severity(), Severity::Minor));
        assert!(matches!(issue.category(), Category::Security));
        let location = issue.location().unwrap();
        assert_eq!(location.path, PathBuf::from("Cargo.lock"));
        assert_eq!(
            location.message,
            "'clap' in crate 'cargo-codeclimate' is outdated and can be updated up to '4.3.16'"
        );
        assert_eq!(location.range.start.line, 175);
        assert_eq!(location.range.end.line, 184);
        assert_eq!(location.range.start.column, 1);
        assert_eq!(location.range.end.column, 2);
    }
}