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);
}
}