#![allow(unused)]
#![cfg_attr(coverage_nightly, coverage(off))]
use crate::services::service_registry::ServiceRegistry;
use anyhow::Result;
use serde::Serialize;
use std::path::Path;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct SatdAnalysisRequest {
pub path: std::path::PathBuf,
pub strict_mode: bool,
pub include_tests: bool,
pub extended: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct SatdAnalysisResult {
pub total_files: usize,
pub violations: Vec<SatdViolation>,
pub summary: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct SatdViolation {
pub file_path: String,
pub line_number: usize,
pub violation_type: String,
pub message: String,
pub severity: SatdSeverity,
}
#[derive(Debug, Clone, Serialize)]
pub enum SatdSeverity {
Critical,
High,
Medium,
Low,
}
#[derive(Clone)]
pub struct SatdFacade {
registry: Arc<ServiceRegistry>,
}
impl SatdFacade {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new(registry: Arc<ServiceRegistry>) -> Self {
Self { registry }
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn analyze_project(
&self,
request: SatdAnalysisRequest,
) -> Result<SatdAnalysisResult> {
use crate::services::satd_detector::SATDDetector;
let detector = if request.strict_mode {
SATDDetector::new_strict()
} else if request.extended {
SATDDetector::new_extended()
} else {
SATDDetector::new()
};
if request.path.is_file() {
let content = tokio::fs::read_to_string(&request.path).await?;
let satd_items = detector
.extract_from_content(&content, &request.path)
.map_err(|e| anyhow::anyhow!("SATD analysis of {:?} failed: {e}", request.path))?;
return Ok(Self::build_result(&satd_items));
}
let satd_items = if request.include_tests {
detector
.analyze_directory_with_tests(&request.path, true)
.await?
} else {
detector.analyze_directory(&request.path).await?
};
Ok(Self::build_result(&satd_items))
}
fn build_result(
satd_items: &[crate::services::satd_detector::TechnicalDebt],
) -> SatdAnalysisResult {
let violations: Vec<SatdViolation> = satd_items
.iter()
.map(|item| {
let severity = match item.severity {
crate::services::satd_detector::Severity::Critical => SatdSeverity::Critical,
crate::services::satd_detector::Severity::High => SatdSeverity::High,
crate::services::satd_detector::Severity::Medium => SatdSeverity::Medium,
crate::services::satd_detector::Severity::Low => SatdSeverity::Low,
};
SatdViolation {
file_path: item.file.display().to_string(),
line_number: item.line as usize,
violation_type: format!("{:?}", item.category),
message: item.text.clone(),
severity,
}
})
.collect();
let total_files = violations
.iter()
.map(|v| &v.file_path)
.collect::<std::collections::HashSet<_>>()
.len();
let summary = format!(
"Found {} SATD violations in {} files",
violations.len(),
total_files
);
SatdAnalysisResult {
total_files,
violations,
summary,
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn analyze_file<P: AsRef<Path>>(&self, path: P) -> Result<SatdAnalysisResult> {
let request = SatdAnalysisRequest {
path: path.as_ref().to_path_buf(),
strict_mode: false,
include_tests: true,
extended: false,
};
self.analyze_project(request).await
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::services::service_registry::ServiceRegistry;
#[tokio::test]
async fn test_satd_facade_creation() {
let registry = Arc::new(ServiceRegistry::new());
let _facade = SatdFacade::new(registry);
}
fn facade() -> SatdFacade {
SatdFacade::new(Arc::new(ServiceRegistry::new()))
}
fn crate_with_test_debt() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join("src")).expect("src");
std::fs::create_dir_all(dir.path().join("tests")).expect("tests");
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"st\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.expect("manifest");
std::fs::write(
dir.path().join("src/lib.rs"),
"// TODO: prod debt\npub fn a() {}\n",
)
.expect("lib");
std::fs::write(
dir.path().join("tests/it.rs"),
"// TODO: fix this test\npub fn t() {}\n",
)
.expect("test file");
dir
}
fn request(path: std::path::PathBuf, include_tests: bool) -> SatdAnalysisRequest {
SatdAnalysisRequest {
path,
strict_mode: false,
include_tests,
extended: false,
}
}
#[tokio::test]
async fn test_include_tests_actually_includes_test_files() {
let dir = crate_with_test_debt();
let facade = facade();
let without = facade
.analyze_project(request(dir.path().to_path_buf(), false))
.await
.expect("analysis without tests");
let with = facade
.analyze_project(request(dir.path().to_path_buf(), true))
.await
.expect("analysis with tests");
assert_eq!(
without.violations.len(),
1,
"only src/lib.rs debt without the flag: {:?}",
without.violations
);
assert!(
with.violations.len() > without.violations.len(),
"--include-tests must add the TODO in tests/: {:?}",
with.violations
);
assert!(
with.violations
.iter()
.any(|v| v.file_path.contains("it.rs")),
"the test file's debt must be listed: {:?}",
with.violations
);
}
#[tokio::test]
async fn test_a_single_file_path_is_analyzed_not_walked() {
let dir = crate_with_test_debt();
let file = dir.path().join("src/lib.rs");
let result = facade()
.analyze_project(request(file.clone(), false))
.await
.expect("single-file analysis");
assert_eq!(
result.violations.len(),
1,
"the file's own TODO must be reported: {:?}",
result.violations
);
assert_eq!(result.total_files, 1);
assert!(result.violations[0].file_path.contains("lib.rs"));
}
}