use crate::Issue as _;
use std::collections::HashMap;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[allow(dead_code)]
#[non_exhaustive]
pub enum Severity {
High,
Medium,
Low,
}
impl From<crate::Severity> for Severity {
#[inline]
fn from(severity: crate::Severity) -> Self {
match severity {
crate::Severity::Blocker | crate::Severity::Critical => Self::High,
crate::Severity::Major => Self::Medium,
crate::Severity::Minor | crate::Severity::Info => Self::Low,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextRange {
pub start_line: usize,
pub end_line: usize,
pub start_column: usize,
pub end_column: usize,
}
impl TextRange {
#[inline]
#[must_use]
pub fn new(
(start_line, start_column): (usize, usize),
(end_line, end_column): (usize, usize),
) -> Self {
Self {
start_line,
end_line,
start_column,
end_column: end_column.saturating_add(usize::from(
start_line == end_line && start_column == end_column,
)),
}
}
}
impl From<crate::TextRange> for TextRange {
#[inline]
fn from(range: crate::TextRange) -> Self {
Self::new(
(range.start.line, range.start.column),
(range.end.line, range.end.column),
)
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Location {
pub message: String,
pub file_path: String,
pub text_range: TextRange,
}
impl From<crate::Location> for Location {
#[inline]
fn from(location: crate::Location) -> Self {
Self {
message: location.message,
file_path: location.path.to_string_lossy().into_owned(),
text_range: TextRange::from(location.range),
}
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[allow(dead_code)]
#[non_exhaustive]
pub enum CleanCodeAttribute {
Formatted,
Conventional,
Identifiable,
Clear,
Logical,
Complete,
Efficient,
Focused,
Distinct,
Modular,
Tested,
Lawful,
Trustworthy,
Respectful,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[allow(dead_code)]
#[non_exhaustive]
pub enum SoftwareQuality {
Maintainability,
Reliability,
Security,
}
impl From<crate::Category> for SoftwareQuality {
#[inline]
fn from(category: crate::Category) -> Self {
match category {
crate::Category::Bug | crate::Category::Performance => SoftwareQuality::Reliability,
crate::Category::Complexity | crate::Category::Duplication | crate::Category::Style => {
SoftwareQuality::Maintainability
}
crate::Category::Security => SoftwareQuality::Security,
}
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Impact {
pub software_quality: SoftwareQuality,
pub severity: Severity,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
pub id: String,
pub name: String,
pub description: String,
pub engine_id: String,
pub clean_code_attribute: CleanCodeAttribute,
pub impacts: Vec<Impact>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Issue {
pub rule_id: String,
pub primary_location: Location,
pub secondary_locations: Vec<Location>,
}
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Issues {
rules: Vec<Rule>,
issues: Vec<Issue>,
}
impl Issues {
#[must_use]
#[inline]
pub fn len(&self) -> usize {
self.issues.len()
}
#[must_use]
#[inline]
pub fn is_empty(&self) -> bool {
self.issues.is_empty()
}
}
#[derive(Debug)]
pub struct Sonar;
impl<'c> crate::FromIssues<'c> for Sonar {
type Report = Issues;
fn from_issues(issues: impl IntoIterator<Item = crate::IssueType<'c>>) -> Self::Report {
let (rules, issues) = issues.into_iter().fold(
(HashMap::new(), Vec::new()),
|(mut rules, mut issues), issue| {
let Some(location) = issue.location() else {
return (rules, issues);
};
rules.entry(issue.issue_uid()).or_insert_with(|| Rule {
id: issue.issue_uid(),
name: issue.issue_id(),
description: location.message.clone(),
engine_id: issue.analyzer_id(),
clean_code_attribute: CleanCodeAttribute::Clear,
impacts: vec![Impact {
software_quality: SoftwareQuality::from(issue.category()),
severity: Severity::from(issue.severity()),
}],
});
issues.push(Issue {
rule_id: issue.issue_uid(),
primary_location: Location::from(location),
secondary_locations: issue
.other_locations()
.into_iter()
.map(Location::from)
.collect(),
});
(rules, issues)
},
);
Self::Report {
rules: rules.into_values().collect(),
issues,
}
}
}
#[cfg(test)]
mod tests {
use super::{Severity, Sonar};
use crate::{
sonar::{CleanCodeAttribute, SoftwareQuality},
test::Issue,
FromIssues as _, IssueType,
};
use test_log::test;
#[test]
fn simple_issue() {
let issues = Sonar::from_issues(vec![IssueType::Test(Issue)]);
assert_eq!(issues.rules.len(), 1);
let rule = &issues.rules.first().unwrap();
assert_eq!(rule.id, "fake::fake_issue");
assert_eq!(rule.engine_id, "fake");
assert_eq!(rule.name, "fake_issue");
assert_eq!(rule.description, "this issue is bad!");
assert!(matches!(
rule.clean_code_attribute,
CleanCodeAttribute::Clear
));
assert_eq!(rule.impacts.len(), 1);
let impact = &rule.impacts.first().unwrap();
assert!(matches!(
impact.software_quality,
SoftwareQuality::Maintainability
));
assert!(matches!(impact.severity, Severity::Low));
assert_eq!(issues.issues.len(), 1);
let issue = &issues.issues.first().unwrap();
assert_eq!(issue.rule_id, "fake::fake_issue");
assert_eq!(issue.primary_location.message, "this issue is bad!");
assert_eq!(issue.primary_location.file_path, "Cargo.lock");
assert_eq!(issue.primary_location.text_range.start_line, 1);
assert_eq!(issue.primary_location.text_range.end_line, 2);
assert_eq!(issue.primary_location.text_range.start_column, 1);
assert_eq!(issue.primary_location.text_range.end_column, 42);
assert!(issue.secondary_locations.is_empty());
}
}