cargo-sonar 1.6.0

Helper to transform reports from Rust tooling for code quality, into valid Sonar report
Documentation
use crate::{Category, Location, Severity, TextRange};
use cargo_metadata::{
    diagnostic::{DiagnosticLevel, DiagnosticSpan},
    CompilerMessage, Message,
};
use dyn_iter::IntoDynIterator as _;
use std::{
    io::{BufRead as _, BufReader},
    path::PathBuf,
};

const CLIPPY_ENGINE: &str = "clippy";

#[derive(Debug)]
pub struct Clippy {
    compiler_messages: dyn_iter::DynIter<'static, CompilerMessage>,
}

impl Clippy {
    /// Create a Clippy parser for issues
    ///
    /// # Errors
    /// May fail reading and parsing the file (IO errors).
    #[inline]
    pub fn try_new<R>(json_read: R) -> eyre::Result<Self>
    where
        R: std::io::Read + 'static,
    {
        let reader = BufReader::new(json_read);
        let compiler_messages = reader
            .lines()
            .map_while(Result::ok)
            .flat_map(|line| serde_json::from_str::<cargo_metadata::Message>(&line))
            .filter_map(|message| {
                if let Message::CompilerMessage(compiler_message) = message {
                    Some(compiler_message)
                } else {
                    None
                }
            })
            .into_dyn_iter();
        let clippy = Self { compiler_messages };
        Ok(clippy)
    }
}

impl Iterator for Clippy {
    type Item = CompilerMessage;

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

impl From<&DiagnosticLevel> for Severity {
    #[inline]
    fn from(level: &DiagnosticLevel) -> Self {
        match *level {
            DiagnosticLevel::Ice => Severity::Blocker,
            DiagnosticLevel::Error => Severity::Critical,
            DiagnosticLevel::Warning => Severity::Major,
            DiagnosticLevel::FailureNote => Severity::Minor,
            _ => Severity::Info,
        }
    }
}

impl From<&DiagnosticSpan> for TextRange {
    #[inline]
    fn from(span: &DiagnosticSpan) -> Self {
        TextRange::new(
            (span.line_start, span.column_start.saturating_sub(1)),
            (span.line_end, span.column_end.saturating_sub(1)),
        )
    }
}

pub type Issue = CompilerMessage;
impl crate::Issue for Issue {
    #[inline]
    fn analyzer_id(&self) -> String {
        CLIPPY_ENGINE.to_owned()
    }

    #[inline]
    fn issue_id(&self) -> String {
        self.message.code.as_ref().map_or_else(
            || String::from("unknown"),
            |diagnostic_code| {
                diagnostic_code
                    .code
                    .trim_start_matches("clippy::")
                    .to_owned()
            },
        )
    }

    #[inline]
    fn fingerprint(&self) -> md5::Digest {
        md5::compute(&self.message.message)
    }

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

    #[inline]
    fn severity(&self) -> Severity {
        Severity::from(&self.message.level)
    }

    #[inline]
    fn location(&self) -> Option<Location> {
        self.message.spans.first().map(|span| Location {
            path: PathBuf::from(&span.file_name),
            range: TextRange::from(span),
            message: self.message.message.clone(),
        })
    }

    #[inline]
    fn other_locations(&self) -> Vec<Location> {
        self.message
            .spans
            .iter()
            .skip(1)
            .map(|span| Location {
                path: PathBuf::from(&span.file_name),
                range: TextRange::from(span),
                message: self.message.message.clone(),
            })
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use crate::{clippy::Clippy, Category, Issue as _, Severity};
    use std::{io::Write as _, path::PathBuf};
    use test_log::test;

    #[test]
    fn single_issue() {
        let json = r##"{
          "reason": "compiler-message",
          "package_id": "cargo-sonar 0.15.0 (path+file:///home/woshilapin/projects/woshilapin/cargo-sonar)",
          "manifest_path": "/home/woshilapin/projects/woshilapin/cargo-sonar/Cargo.toml",
          "target": {
            "kind": [
              "bin"
            ],
            "crate_types": [
              "bin"
            ],
            "name": "cargo-sonar",
            "src_path": "/home/woshilapin/projects/woshilapin/cargo-sonar/src/main.rs",
            "edition": "2021",
            "doc": true,
            "doctest": false,
            "test": true
          },
          "message": {
            "rendered": "warning: used `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertion in a function that returns `Result`\n   --> src/clippy.rs:156:5\n    |\n156 |     #[test]\n    |     ^^^^^^^\n    |\n    = help: `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertions should not be used in a function that returns `Result` as `Result` is expected to return an error instead of crashing\nnote: return Err() instead of panicking\n   --> src/clippy.rs:206:9\n    |\n206 |         assert_eq!(sonar_issues.len(), 1);\n    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#panic_in_result_fn\nnote: the lint level is defined here\n   --> src/main.rs:26:5\n    |\n26  |     clippy::panic_in_result_fn,\n    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^\n    = note: this warning originates in the attribute macro `test` (in Nightly builds, run with -Z macro-backtrace for more info)\n\n",
            "children": [
              {
                "children": [],
                "code": null,
                "level": "help",
                "message": "`unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertions should not be used in a function that returns `Result` as `Result` is expected to return an error instead of crashing",
                "rendered": null,
                "spans": []
              },
              {
                "children": [],
                "code": null,
                "level": "note",
                "message": "return Err() instead of panicking",
                "rendered": null,
                "spans": [
                  {
                    "byte_end": 6887,
                    "byte_start": 6854,
                    "column_end": 42,
                    "column_start": 9,
                    "expansion": null,
                    "file_name": "src/clippy.rs",
                    "is_primary": true,
                    "label": null,
                    "line_end": 206,
                    "line_start": 206,
                    "suggested_replacement": null,
                    "suggestion_applicability": null,
                    "text": [
                      {
                        "highlight_end": 42,
                        "highlight_start": 9,
                        "text": "        assert_eq!(sonar_issues.len(), 1);"
                      }
                    ]
                  }
                ]
              },
              {
                "children": [],
                "code": null,
                "level": "help",
                "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#panic_in_result_fn",
                "rendered": null,
                "spans": []
              },
              {
                "children": [],
                "code": null,
                "level": "note",
                "message": "the lint level is defined here",
                "rendered": null,
                "spans": [
                  {
                    "byte_end": 757,
                    "byte_start": 731,
                    "column_end": 31,
                    "column_start": 5,
                    "expansion": null,
                    "file_name": "src/main.rs",
                    "is_primary": true,
                    "label": null,
                    "line_end": 26,
                    "line_start": 26,
                    "suggested_replacement": null,
                    "suggestion_applicability": null,
                    "text": [
                      {
                        "highlight_end": 31,
                        "highlight_start": 5,
                        "text": "    clippy::panic_in_result_fn,"
                      }
                    ]
                  }
                ]
              }
            ],
            "code": {
              "code": "clippy::panic_in_result_fn",
              "explanation": null
            },
            "level": "warning",
            "message": "used `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertion in a function that returns `Result`",
            "spans": [
              {
                "byte_end": 5238,
                "byte_start": 5231,
                "column_end": 12,
                "column_start": 5,
                "expansion": {
                  "def_site_span": {
                    "byte_end": 2153,
                    "byte_start": 2089,
                    "column_end": 65,
                    "column_start": 1,
                    "expansion": null,
                    "file_name": "/home/woshilapin/.cargo/registry/src/index.crates.io-6f17d22bba15001f/test-log-0.2.12/src/lib.rs",
                    "is_primary": false,
                    "label": null,
                    "line_end": 82,
                    "line_start": 82,
                    "suggested_replacement": null,
                    "suggestion_applicability": null,
                    "text": [
                      {
                        "highlight_end": 65,
                        "highlight_start": 1,
                        "text": "pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {"
                      }
                    ]
                  },
                  "macro_decl_name": "#[test]",
                  "span": {
                    "byte_end": 5238,
                    "byte_start": 5231,
                    "column_end": 12,
                    "column_start": 5,
                    "expansion": null,
                    "file_name": "src/clippy.rs",
                    "is_primary": false,
                    "label": null,
                    "line_end": 156,
                    "line_start": 156,
                    "suggested_replacement": null,
                    "suggestion_applicability": null,
                    "text": [
                      {
                        "highlight_end": 12,
                        "highlight_start": 5,
                        "text": "    #[test]"
                      }
                    ]
                  }
                },
                "file_name": "src/clippy.rs",
                "is_primary": true,
                "label": null,
                "line_end": 156,
                "line_start": 156,
                "suggested_replacement": null,
                "suggestion_applicability": null,
                "text": [
                  {
                    "highlight_end": 12,
                    "highlight_start": 5,
                    "text": "    #[test]"
                  }
                ]
              }
            ]
          }
        }"##;
        let json = json.to_owned().replace('\n', "");
        let mut clippy_json = tempfile::NamedTempFile::new().unwrap();
        write!(clippy_json, "{}", json).unwrap();
        let clippy_json = clippy_json.reopen().unwrap();

        let mut clippy = Clippy::try_new(clippy_json).unwrap();
        let issue = clippy.next().unwrap();
        assert_eq!(issue.analyzer_id(), "clippy");
        assert_eq!(issue.issue_uid(), "clippy::panic_in_result_fn");
        assert!(matches!(issue.severity(), Severity::Major));
        assert!(matches!(issue.category(), Category::Style));
        let location = issue.location().unwrap();
        assert_eq!(location.path, PathBuf::from("src/clippy.rs"));
        assert_eq!(location.message, "used `unimplemented!()`, `unreachable!()`, `todo!()`, `panic!()` or assertion in a function that returns `Result`");
        assert_eq!(location.range.start.line, 156);
        assert_eq!(location.range.end.line, 156);
        assert_eq!(location.range.start.column, 4);
        assert_eq!(location.range.end.column, 11);
    }
}