use crate::Language;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Error => write!(f, "error"),
Severity::Warning => write!(f, "warning"),
Severity::Info => write!(f, "info"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LintIssue {
pub file_path: PathBuf,
pub line: usize,
pub column: Option<usize>,
pub severity: Severity,
pub code: Option<String>,
pub message: String,
pub suggestion: Option<String>,
pub source: Option<String>,
pub language: Option<Language>,
pub code_line: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context_before: Vec<(usize, String)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context_after: Vec<(usize, String)>,
}
impl LintIssue {
pub fn new(file_path: PathBuf, line: usize, message: String, severity: Severity) -> Self {
Self {
file_path,
line,
column: None,
severity,
code: None,
message,
suggestion: None,
source: None,
language: None,
code_line: None,
context_before: Vec::new(),
context_after: Vec::new(),
}
}
pub fn with_column(mut self, column: usize) -> Self {
self.column = Some(column);
self
}
pub fn with_code(mut self, code: String) -> Self {
self.code = Some(code);
self
}
pub fn with_suggestion(mut self, suggestion: String) -> Self {
self.suggestion = Some(suggestion);
self
}
pub fn with_source(mut self, source: String) -> Self {
self.source = Some(source);
self
}
pub fn with_language(mut self, language: Language) -> Self {
self.language = Some(language);
self
}
pub fn with_code_line(mut self, code_line: String) -> Self {
self.code_line = Some(code_line);
self
}
pub fn with_context_before(mut self, context: Vec<(usize, String)>) -> Self {
self.context_before = context;
self
}
pub fn with_context_after(mut self, context: Vec<(usize, String)>) -> Self {
self.context_after = context;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnavailableTool {
pub tool: String,
pub language: String,
pub tool_type: String,
pub install_hint: String,
#[serde(default)]
pub auto_install_failed: bool,
}
impl UnavailableTool {
pub fn new(tool: &str, language: &str, tool_type: &str, install_hint: &str) -> Self {
Self {
tool: tool.to_string(),
language: language.to_string(),
tool_type: tool_type.to_string(),
install_hint: install_hint.to_string(),
auto_install_failed: false,
}
}
pub fn with_auto_install_failed(mut self) -> Self {
self.auto_install_failed = true;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormatResult {
pub file_path: PathBuf,
pub changed: bool,
pub diff: Option<String>,
pub error: Option<String>,
}
impl FormatResult {
pub fn unchanged(file_path: PathBuf) -> Self {
Self {
file_path,
changed: false,
diff: None,
error: None,
}
}
pub fn changed(file_path: PathBuf) -> Self {
Self {
file_path,
changed: true,
diff: None,
error: None,
}
}
pub fn with_diff(mut self, diff: String) -> Self {
self.diff = Some(diff);
self
}
pub fn error(file_path: PathBuf, error: String) -> Self {
Self {
file_path,
changed: false,
diff: None,
error: Some(error),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RunModeKind {
#[default]
Both,
CheckOnly,
FormatOnly,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RunResult {
pub total_files: usize,
pub files_with_issues: usize,
pub files_formatted: usize,
pub issues: Vec<LintIssue>,
pub issues_before_format: usize,
pub issues_fixed: usize,
pub format_results: Vec<FormatResult>,
pub duration_ms: u64,
pub exit_code: i32,
pub run_mode: RunModeKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unavailable_tools: Vec<UnavailableTool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub security: Option<crate::security::sast::SastResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub complexity: Option<crate::complexity::AnalysisResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checks_run: Vec<String>,
}
impl RunResult {
pub fn new() -> Self {
Self::default()
}
pub fn add_issue(&mut self, issue: LintIssue) {
self.issues.push(issue);
}
pub fn add_format_result(&mut self, result: FormatResult) {
if result.changed {
self.files_formatted += 1;
}
self.format_results.push(result);
}
pub fn calculate_exit_code(&mut self) {
self.calculate_exit_code_with_fail_on(&crate::config::FailOn::Warning);
}
pub fn calculate_exit_code_with_fail_on(&mut self, fail_on: &crate::config::FailOn) {
let has_format_errors = self.format_results.iter().any(|r| r.error.is_some());
if has_format_errors {
self.exit_code = 4;
return;
}
let error_count = self
.issues
.iter()
.filter(|i| i.severity == Severity::Error)
.count();
let warning_count = self
.issues
.iter()
.filter(|i| i.severity == Severity::Warning)
.count();
let info_count = self
.issues
.iter()
.filter(|i| i.severity == Severity::Info)
.count();
self.exit_code = fail_on.exit_code(error_count, warning_count, info_count);
}
pub fn count_files_with_issues(&mut self) {
use std::collections::HashSet;
let unique_files: HashSet<_> = self.issues.iter().map(|i| &i.file_path).collect();
self.files_with_issues = unique_files.len();
}
pub fn merge_all_check_issues(&mut self) {
if let Some(ref sec) = self.security {
for finding in &sec.findings {
let severity = match finding.severity {
crate::security::Severity::Critical | crate::security::Severity::High => {
Severity::Error
}
crate::security::Severity::Medium => Severity::Warning,
_ => Severity::Info,
};
let mut issue = LintIssue::new(
finding.file_path.clone(),
finding.line,
format!("[security] {}", finding.message),
severity,
);
issue = issue.with_source(format!("security/{}", finding.source));
issue = issue.with_code(finding.rule_id.clone());
if let Some(ref fix) = finding.fix_suggestion {
issue = issue.with_suggestion(fix.clone());
}
self.issues.push(issue);
}
}
if let Some(ref cx) = self.complexity {
let threshold = cx.thresholds.cyclomatic.good;
let warning_threshold = cx.thresholds.cyclomatic.warning;
let high_threshold = cx.thresholds.cyclomatic.high;
for file in &cx.files {
for func in &file.functions {
if func.metrics.cyclomatic > threshold {
let severity = if func.metrics.cyclomatic > high_threshold {
Severity::Error
} else if func.metrics.cyclomatic > warning_threshold {
Severity::Warning
} else {
Severity::Info
};
let exceeded_threshold = match severity {
Severity::Error => high_threshold,
Severity::Warning => warning_threshold,
_ => threshold,
};
let mut issue = LintIssue::new(
file.path.clone(),
func.start_line as usize,
format!(
"[complexity] function `{}` cyclomatic complexity {} exceeds threshold {}",
func.name, func.metrics.cyclomatic, exceeded_threshold,
),
severity,
);
issue = issue.with_source("linthis-complexity".to_string());
issue = issue.with_suggestion(
"Consider refactoring into smaller functions".to_string(),
);
self.issues.push(issue);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_severity_display_error() {
assert_eq!(format!("{}", Severity::Error), "error");
}
#[test]
fn test_severity_display_warning() {
assert_eq!(format!("{}", Severity::Warning), "warning");
}
#[test]
fn test_severity_display_info() {
assert_eq!(format!("{}", Severity::Info), "info");
}
#[test]
fn test_severity_equality() {
assert_eq!(Severity::Error, Severity::Error);
assert_ne!(Severity::Error, Severity::Warning);
}
#[test]
fn test_lint_issue_new() {
let issue = LintIssue::new(
PathBuf::from("test.cpp"),
10,
"Test message".to_string(),
Severity::Warning,
);
assert_eq!(issue.file_path, PathBuf::from("test.cpp"));
assert_eq!(issue.line, 10);
assert_eq!(issue.message, "Test message");
assert_eq!(issue.severity, Severity::Warning);
assert!(issue.column.is_none());
assert!(issue.code.is_none());
assert!(issue.suggestion.is_none());
assert!(issue.source.is_none());
assert!(issue.language.is_none());
}
#[test]
fn test_lint_issue_with_column() {
let issue = LintIssue::new(
PathBuf::from("test.cpp"),
10,
"msg".to_string(),
Severity::Error,
)
.with_column(5);
assert_eq!(issue.column, Some(5));
}
#[test]
fn test_lint_issue_with_code() {
let issue = LintIssue::new(
PathBuf::from("test.cpp"),
10,
"msg".to_string(),
Severity::Warning,
)
.with_code("E001".to_string());
assert_eq!(issue.code, Some("E001".to_string()));
}
#[test]
fn test_lint_issue_with_suggestion() {
let issue = LintIssue::new(
PathBuf::from("test.cpp"),
10,
"msg".to_string(),
Severity::Info,
)
.with_suggestion("Use nullptr instead".to_string());
assert_eq!(issue.suggestion, Some("Use nullptr instead".to_string()));
}
#[test]
fn test_lint_issue_with_source() {
let issue = LintIssue::new(
PathBuf::from("test.cpp"),
10,
"msg".to_string(),
Severity::Warning,
)
.with_source("cpplint".to_string());
assert_eq!(issue.source, Some("cpplint".to_string()));
}
#[test]
fn test_lint_issue_with_language() {
let issue = LintIssue::new(
PathBuf::from("test.cpp"),
10,
"msg".to_string(),
Severity::Warning,
)
.with_language(Language::Cpp);
assert_eq!(issue.language, Some(Language::Cpp));
}
#[test]
fn test_lint_issue_builder_chaining() {
let issue = LintIssue::new(
PathBuf::from("test.cpp"),
10,
"Test error".to_string(),
Severity::Error,
)
.with_column(5)
.with_code("E001".to_string())
.with_source("clang-tidy".to_string())
.with_suggestion("Fix it".to_string())
.with_language(Language::Cpp);
assert_eq!(issue.column, Some(5));
assert_eq!(issue.code, Some("E001".to_string()));
assert_eq!(issue.source, Some("clang-tidy".to_string()));
assert_eq!(issue.suggestion, Some("Fix it".to_string()));
assert_eq!(issue.language, Some(Language::Cpp));
}
#[test]
fn test_format_result_unchanged() {
let result = FormatResult::unchanged(PathBuf::from("test.cpp"));
assert_eq!(result.file_path, PathBuf::from("test.cpp"));
assert!(!result.changed);
assert!(result.diff.is_none());
assert!(result.error.is_none());
}
#[test]
fn test_format_result_changed() {
let result = FormatResult::changed(PathBuf::from("test.cpp"));
assert_eq!(result.file_path, PathBuf::from("test.cpp"));
assert!(result.changed);
assert!(result.diff.is_none());
assert!(result.error.is_none());
}
#[test]
fn test_format_result_with_diff() {
let result =
FormatResult::changed(PathBuf::from("test.cpp")).with_diff("- old\n+ new".to_string());
assert!(result.changed);
assert_eq!(result.diff, Some("- old\n+ new".to_string()));
}
#[test]
fn test_format_result_error() {
let result = FormatResult::error(PathBuf::from("test.cpp"), "Format failed".to_string());
assert_eq!(result.file_path, PathBuf::from("test.cpp"));
assert!(!result.changed);
assert!(result.diff.is_none());
assert_eq!(result.error, Some("Format failed".to_string()));
}
#[test]
fn test_run_mode_kind_default() {
let mode = RunModeKind::default();
assert_eq!(mode, RunModeKind::Both);
}
#[test]
fn test_run_result_new() {
let result = RunResult::new();
assert_eq!(result.total_files, 0);
assert_eq!(result.files_with_issues, 0);
assert_eq!(result.files_formatted, 0);
assert!(result.issues.is_empty());
assert_eq!(result.issues_before_format, 0);
assert_eq!(result.issues_fixed, 0);
assert!(result.format_results.is_empty());
assert_eq!(result.duration_ms, 0);
assert_eq!(result.exit_code, 0);
assert_eq!(result.run_mode, RunModeKind::Both);
}
#[test]
fn test_run_result_add_issue() {
let mut result = RunResult::new();
let issue = LintIssue::new(
PathBuf::from("test.cpp"),
10,
"Test".to_string(),
Severity::Warning,
);
result.add_issue(issue);
assert_eq!(result.issues.len(), 1);
assert_eq!(result.issues[0].file_path, PathBuf::from("test.cpp"));
}
#[test]
fn test_run_result_add_format_result_changed() {
let mut result = RunResult::new();
let format_result = FormatResult::changed(PathBuf::from("test.cpp"));
result.add_format_result(format_result);
assert_eq!(result.files_formatted, 1);
assert_eq!(result.format_results.len(), 1);
}
#[test]
fn test_run_result_add_format_result_unchanged() {
let mut result = RunResult::new();
let format_result = FormatResult::unchanged(PathBuf::from("test.cpp"));
result.add_format_result(format_result);
assert_eq!(result.files_formatted, 0);
assert_eq!(result.format_results.len(), 1);
}
#[test]
fn test_run_result_calculate_exit_code_success() {
let mut result = RunResult::new();
result.calculate_exit_code();
assert_eq!(result.exit_code, 0);
}
#[test]
fn test_run_result_calculate_exit_code_with_error() {
let mut result = RunResult::new();
result.add_issue(LintIssue::new(
PathBuf::from("test.cpp"),
10,
"Error".to_string(),
Severity::Error,
));
result.calculate_exit_code();
assert_eq!(result.exit_code, 1);
}
#[test]
fn test_run_result_calculate_exit_code_with_warning() {
let mut result = RunResult::new();
result.add_issue(LintIssue::new(
PathBuf::from("test.cpp"),
10,
"Warning".to_string(),
Severity::Warning,
));
result.calculate_exit_code();
assert_eq!(result.exit_code, 2); }
#[test]
fn test_run_result_calculate_exit_code_format_error() {
let mut result = RunResult::new();
result.add_format_result(FormatResult::error(
PathBuf::from("test.cpp"),
"Format failed".to_string(),
));
result.calculate_exit_code();
assert_eq!(result.exit_code, 4); }
#[test]
fn test_run_result_count_files_with_issues() {
let mut result = RunResult::new();
result.add_issue(LintIssue::new(
PathBuf::from("test1.cpp"),
10,
"Issue 1".to_string(),
Severity::Warning,
));
result.add_issue(LintIssue::new(
PathBuf::from("test1.cpp"),
20,
"Issue 2".to_string(),
Severity::Warning,
));
result.add_issue(LintIssue::new(
PathBuf::from("test2.cpp"),
5,
"Issue 3".to_string(),
Severity::Error,
));
result.count_files_with_issues();
assert_eq!(result.files_with_issues, 2);
}
#[test]
fn test_run_result_count_files_with_issues_empty() {
let mut result = RunResult::new();
result.count_files_with_issues();
assert_eq!(result.files_with_issues, 0);
}
}