#![cfg_attr(coverage_nightly, coverage(off))]
use super::output::{print_json_report, print_junit_report, print_text_report};
use super::types::{DefectReport, DefectSummary, OutputFormat, SeverityCount};
use crate::services::defect_detector::{
detect_defects, exclusion_reason, is_supported, unmeasured, DefectPattern, Severity,
SUPPORTED_EXTENSIONS,
};
use anyhow::Result;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use walkdir::WalkDir;
pub(crate) const EXIT_NOTHING_MEASURED: i32 = 5;
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_analyze_defects(
path: Option<&Path>,
file: Option<&Path>,
severity_filter: Option<Severity>,
format: OutputFormat,
) -> Result<i32> {
let target_path = path.unwrap_or_else(|| Path::new("."));
crate::cli::ensure_analysis_path_exists(target_path)?;
if let Some(specific_file) = file {
crate::cli::ensure_analysis_path_exists(specific_file)?;
}
let files_to_scan = if let Some(specific_file) = file {
vec![specific_file.to_path_buf()]
} else {
collect_source_files(target_path)?
};
let (mut all_defects, scan) = scan_files(&files_to_scan);
if scan.analysed == 0 {
eprintln!(
"Error: {}",
unmeasured::refusal(
"defect",
file.unwrap_or(target_path),
files_to_scan.len(),
&scan.describe_skips(),
&scan.remedy(),
)
);
return Ok(EXIT_NOTHING_MEASURED);
}
if let Some(filter_severity) = severity_filter {
all_defects.retain(|d| d.severity == filter_severity);
}
let summary = calculate_summary(scan.analysed, &all_defects);
let has_critical = all_defects
.iter()
.any(|d| matches!(d.severity, Severity::Critical));
let exit_code = if has_critical { 1 } else { 0 };
let report = DefectReport {
summary,
defects: all_defects,
exit_code,
has_critical_defects: has_critical,
};
match format {
OutputFormat::Text | OutputFormat::Plain => print_text_report(&report),
OutputFormat::Json => print_json_report(&report)?,
OutputFormat::Junit => print_junit_report(&report)?,
_ => print_text_report(&report),
}
Ok(exit_code)
}
pub(crate) fn scan_files(files: &[std::path::PathBuf]) -> (Vec<DefectPattern>, ScanTally) {
let mut defects = Vec::new();
let mut scan = ScanTally::default();
for file_path in files {
if let Some(reason) = exclusion_reason(file_path) {
*scan.skipped.entry(reason).or_insert(0) += 1;
continue;
}
match fs::read_to_string(file_path) {
Ok(content) => {
scan.analysed += 1;
defects.extend(detect_defects(&content, file_path));
}
Err(_) => {
*scan
.skipped
.entry(unmeasured::Reason::Unreadable)
.or_insert(0) += 1
}
}
}
(defects, scan)
}
#[derive(Debug, Default)]
pub(crate) struct ScanTally {
pub(crate) analysed: usize,
pub(crate) skipped: BTreeMap<unmeasured::Reason, usize>,
}
impl ScanTally {
pub(crate) fn describe_skips(&self) -> String {
let parts: Vec<String> = self
.skipped
.iter()
.map(|(reason, count)| format!("{count} {}", reason.as_str()))
.collect();
if parts.is_empty() {
return "no reason recorded".to_string();
}
parts.join(", ")
}
pub(crate) fn remedy(&self) -> String {
let only_unsupported = !self.skipped.is_empty()
&& self
.skipped
.keys()
.all(|reason| *reason == unmeasured::Reason::NoRuleSet);
if only_unsupported {
return format!(
"pmat's Known-Defects database has rule sets for {} files only, so there is \
nothing it can say about these; run `pmat analyze complexity` or `pmat analyze \
satd`, which are language-agnostic.",
SUPPORTED_EXTENSIONS.join(", ")
);
}
NON_PRODUCTION_REMEDY.to_string()
}
}
const NON_PRODUCTION_REMEDY: &str =
"point the analysis at the project root, where the production code this \
command measures lives (a package's tests/, benches/, examples/ and fuzz/ \
trees are never measured, and there is no flag to opt them in).";
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub(crate) fn collect_source_files(path: &Path) -> Result<Vec<std::path::PathBuf>> {
let mut files = Vec::new();
for entry in WalkDir::new(path)
.into_iter()
.filter_entry(|e| !is_hidden(e))
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.is_file() && is_supported(path) {
files.push(path.to_path_buf());
}
}
Ok(files)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub(crate) fn is_hidden(entry: &walkdir::DirEntry) -> bool {
if entry.depth() == 0 {
return false;
}
entry
.file_name()
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false)
|| entry.file_name() == "target"
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub(crate) fn calculate_summary(files_analysed: usize, defects: &[DefectPattern]) -> DefectSummary {
let files_with_defects = defects
.iter()
.flat_map(|d| d.instances.iter().map(|i| i.file.as_str()))
.collect::<std::collections::BTreeSet<_>>()
.len();
let mut critical = 0;
let mut high = 0;
let mut medium = 0;
let mut low = 0;
for defect in defects {
match defect.severity {
Severity::Critical => critical += defect.instances.len(),
Severity::High => high += defect.instances.len(),
Severity::Medium => medium += defect.instances.len(),
Severity::Low => low += defect.instances.len(),
}
}
DefectSummary {
total_files_scanned: files_analysed,
files_with_defects,
total_defects: critical + high + medium + low,
by_severity: SeverityCount {
critical,
high,
medium,
low,
},
}
}