use crate::sonar;
use cargo_metadata::{
diagnostic::{DiagnosticLevel, DiagnosticSpan},
Message,
};
use eyre::Context as _;
use skip_error::SkipError as _;
use std::{
fs::File,
io::{BufRead as _, BufReader},
path::PathBuf,
};
use tracing::{error, info};
const CLIPPY_ENGINE: &str = "clippy";
#[derive(Debug)]
pub struct Clippy {
json: PathBuf,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("no span defined to report an error")]
NoSpan,
#[error("the message of type '{0}' are not handled")]
InvalidMessage(&'static str),
}
impl From<&DiagnosticLevel> for sonar::Severity {
fn from(level: &DiagnosticLevel) -> Self {
match *level {
DiagnosticLevel::Ice => sonar::Severity::Blocker,
DiagnosticLevel::Error => sonar::Severity::Critical,
DiagnosticLevel::Warning => sonar::Severity::Major,
DiagnosticLevel::FailureNote => sonar::Severity::Minor,
_ => sonar::Severity::Info,
}
}
}
impl From<&DiagnosticSpan> for sonar::TextRange {
fn from(span: &DiagnosticSpan) -> Self {
Self {
start_line: span.line_start,
end_line: span.line_end,
start_column: span.column_start.saturating_sub(1),
end_column: span.column_end.saturating_sub(1),
}
}
}
#[allow(clippy::panic_in_result_fn)]
impl TryFrom<Message> for sonar::Issue {
type Error = Error;
fn try_from(message: Message) -> Result<Self, Self::Error> {
if let Message::CompilerMessage(message) = message {
let rule_id = message.message.code.as_ref().map_or_else(
|| String::from("clippy"),
|diagnostic_code| diagnostic_code.code.clone(),
);
let severity = sonar::Severity::from(&message.message.level);
let r#type = sonar::Type::CodeSmell;
let (primary_location, secondary_locations) = match message.message.spans.len() {
0 => return Err(Error::NoSpan),
n => {
let span = message.message.spans.get(0).ok_or(Error::NoSpan)?;
let primary_location = sonar::Location {
message: message.message.message.clone(),
file_path: span.file_name.clone(),
text_range: sonar::TextRange::from(span),
};
let secondary_locations = (1..n)
.filter_map(|idx| message.message.spans.get(idx))
.map(|span| sonar::Location {
message: message.message.message.clone(),
file_path: span.file_name.clone(),
text_range: sonar::TextRange::from(span),
})
.collect();
(primary_location, secondary_locations)
}
};
let issue = Self {
engine_id: CLIPPY_ENGINE.to_owned(),
rule_id,
severity,
r#type,
primary_location,
secondary_locations,
};
Ok(issue)
} else {
let kind = match message {
Message::CompilerArtifact(_) => "compiler-artifact",
Message::CompilerMessage(_) => {
unreachable!("'CompilerMessage' has already been parsed above")
}
Message::BuildScriptExecuted(_) => "build-script-executed",
Message::BuildFinished(_) => "build-finished",
Message::TextLine(_) => "text-line",
_ => "unknown",
};
Err(Error::InvalidMessage(kind))
}
}
}
impl Clippy {
pub fn new<P>(json: P) -> Self
where
P: Into<PathBuf>,
{
Self { json: json.into() }
}
}
impl std::convert::TryInto<sonar::Issues> for Clippy {
type Error = eyre::Error;
fn try_into(self) -> Result<sonar::Issues, Self::Error> {
let file = File::open(&self.json).with_context(|| {
format!(
"failed to open 'cargo-clippy' report from '{:?}' file",
self.json
)
})?;
let reader = BufReader::new(file);
let issues: sonar::Issues = reader
.lines()
.flatten()
.flat_map(|line| serde_json::from_str::<cargo_metadata::Message>(&line))
.map(sonar::Issue::try_from)
.skip_error_and_debug()
.collect();
info!("{} sonar issues created", issues.len());
Ok(issues)
}
}