#![cfg_attr(coverage_nightly, coverage(off))]
use super::{Analyzer, AnalyzerInfo, ProjectAnalyzer, ProjectConfig, ProjectInput};
use crate::services::satd_detector::{SATDAnalysisResult, SATDDetector};
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::Path;
pub struct SATDAnalyzer {
inner: SATDDetector,
}
impl SATDAnalyzer {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new() -> Self {
Self {
inner: SATDDetector::new(),
}
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn new_with_strict_mode(strict: bool) -> Self {
if strict {
Self {
inner: SATDDetector::new_strict(),
}
} else {
Self::new()
}
}
}
impl Default for SATDAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SATDConfig {
pub base: ProjectConfig,
pub strict_mode: bool,
pub critical_only: bool,
pub include_context: bool,
}
impl Default for SATDConfig {
fn default() -> Self {
Self {
base: ProjectConfig::default(),
strict_mode: false,
critical_only: false,
include_context: true,
}
}
}
pub type SATDOutput = SATDAnalysisResult;
#[async_trait]
impl Analyzer for SATDAnalyzer {
type Input = ProjectInput;
type Output = SATDOutput;
type Config = ProjectConfig;
async fn analyze(&self, input: Self::Input, config: Self::Config) -> Result<Self::Output> {
self.inner
.analyze_project(&input.project_path, config.include_tests)
.await
.map_err(|e| anyhow::anyhow!("SATD analysis failed: {e}"))
}
fn name(&self) -> &'static str {
"satd"
}
}
#[async_trait]
impl ProjectAnalyzer for SATDAnalyzer {
async fn analyze_project(&self, project_path: &Path) -> Result<Self::Output> {
let input = ProjectInput {
project_path: project_path.to_path_buf(),
};
let config = ProjectConfig::default();
self.analyze(input, config).await
}
}
impl AnalyzerInfo for SATDAnalyzer {
fn name(&self) -> &'static str {
"satd"
}
fn version(&self) -> &'static str {
env!("CARGO_PKG_VERSION")
}
fn description(&self) -> &'static str {
"Analyzes code for Self-Admitted Technical Debt (SATD) in comments and documentation"
}
}
pub struct SATDAnalyzerFactory;
impl SATDAnalyzerFactory {
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn create() -> SATDAnalyzer {
SATDAnalyzer::new()
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn create_strict() -> SATDAnalyzer {
SATDAnalyzer::new_with_strict_mode(true)
}
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn create_critical_only() -> SATDAnalyzer {
SATDAnalyzer::new_with_strict_mode(true)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_satd_analyzer_creation() {
let analyzer = SATDAnalyzer::new();
assert_eq!(Analyzer::name(&analyzer), "satd");
assert_eq!(Analyzer::version(&analyzer), env!("CARGO_PKG_VERSION"));
}
#[test]
fn test_satd_analyzer_new() {
let analyzer = SATDAnalyzer::new();
assert_eq!(AnalyzerInfo::name(&analyzer), "satd");
}
#[test]
fn test_satd_analyzer_default() {
let analyzer = SATDAnalyzer::default();
assert_eq!(AnalyzerInfo::name(&analyzer), "satd");
}
#[test]
fn test_satd_analyzer_with_strict_mode_true() {
let analyzer = SATDAnalyzer::new_with_strict_mode(true);
assert_eq!(AnalyzerInfo::name(&analyzer), "satd");
}
#[test]
fn test_satd_analyzer_with_strict_mode_false() {
let analyzer = SATDAnalyzer::new_with_strict_mode(false);
assert_eq!(AnalyzerInfo::name(&analyzer), "satd");
}
#[test]
fn test_satd_analyzer_name_trait() {
let analyzer = SATDAnalyzer::new();
assert_eq!(Analyzer::name(&analyzer), "satd");
}
#[tokio::test]
async fn test_satd_config_default() {
let config = SATDConfig::default();
assert!(!config.strict_mode);
assert!(!config.critical_only);
assert!(config.include_context);
}
#[test]
fn test_satd_config_custom() {
let config = SATDConfig {
base: ProjectConfig::default(),
strict_mode: true,
critical_only: true,
include_context: false,
};
assert!(config.strict_mode);
assert!(config.critical_only);
assert!(!config.include_context);
}
#[test]
fn test_satd_config_serialization() {
let config = SATDConfig::default();
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("strict_mode"));
assert!(json.contains("critical_only"));
assert!(json.contains("include_context"));
}
#[test]
fn test_satd_config_deserialization() {
let json = r#"{
"base": {"include_tests": true, "max_depth": null, "parallel": false},
"strict_mode": true,
"critical_only": false,
"include_context": true
}"#;
let config: SATDConfig = serde_json::from_str(json).unwrap();
assert!(config.strict_mode);
assert!(!config.critical_only);
assert!(config.include_context);
}
#[test]
fn test_satd_config_clone() {
let config = SATDConfig::default();
let cloned = config.clone();
assert_eq!(config.strict_mode, cloned.strict_mode);
assert_eq!(config.critical_only, cloned.critical_only);
assert_eq!(config.include_context, cloned.include_context);
}
#[test]
fn test_satd_config_debug() {
let config = SATDConfig::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("SATDConfig"));
assert!(debug_str.contains("strict_mode"));
}
#[tokio::test]
async fn test_analyzer_info() {
let analyzer = SATDAnalyzer::new();
assert_eq!(Analyzer::name(&analyzer), "satd");
assert!(AnalyzerInfo::description(&analyzer).contains("Technical Debt"));
}
#[test]
fn test_analyzer_info_name() {
let analyzer = SATDAnalyzer::new();
assert_eq!(AnalyzerInfo::name(&analyzer), "satd");
}
#[test]
fn test_analyzer_info_version() {
let analyzer = SATDAnalyzer::new();
let version = AnalyzerInfo::version(&analyzer);
assert!(!version.is_empty());
assert!(version.contains('.'));
}
#[test]
fn test_analyzer_info_description() {
let analyzer = SATDAnalyzer::new();
let description = AnalyzerInfo::description(&analyzer);
assert!(description.contains("SATD"));
assert!(description.contains("Technical Debt"));
}
#[tokio::test]
async fn test_factory_creation() {
let analyzer = SATDAnalyzerFactory::create();
assert_eq!(Analyzer::name(&analyzer), "satd");
let strict_analyzer = SATDAnalyzerFactory::create_strict();
assert_eq!(Analyzer::name(&strict_analyzer), "satd");
let critical_analyzer = SATDAnalyzerFactory::create_critical_only();
assert_eq!(Analyzer::name(&critical_analyzer), "satd");
}
#[test]
fn test_factory_create() {
let analyzer = SATDAnalyzerFactory::create();
assert_eq!(AnalyzerInfo::name(&analyzer), "satd");
}
#[test]
fn test_factory_create_strict() {
let analyzer = SATDAnalyzerFactory::create_strict();
assert_eq!(AnalyzerInfo::name(&analyzer), "satd");
}
#[test]
fn test_factory_create_critical_only() {
let analyzer = SATDAnalyzerFactory::create_critical_only();
assert_eq!(AnalyzerInfo::name(&analyzer), "satd");
}
#[tokio::test]
async fn test_project_analysis() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(
&test_file,
r#"
fn good_function() -> i32 {
42
}
fn bad_function() -> i32 {
// TODO: This needs to be fixed eventually
// FIXME: Memory leak possible here
// HACK: Quick workaround for now
0
}
"#,
)
.unwrap();
let analyzer = SATDAnalyzer::new();
let result = analyzer.analyze_project(temp_dir.path()).await.unwrap();
if !result.items.is_empty() {
assert!(result.total_files_analyzed > 0);
assert!(result.files_with_debt > 0);
let debt_texts: Vec<&str> =
result.items.iter().map(|item| item.text.as_str()).collect();
assert!(debt_texts.iter().any(|text| text.contains("TODO")
|| text.contains("FIXME")
|| text.contains("HACK")));
}
}
#[tokio::test]
async fn test_strict_mode_analyzer() {
let analyzer = SATDAnalyzer::new_with_strict_mode(true);
assert_eq!(Analyzer::name(&analyzer), "satd");
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(
&test_file,
"// NOTE: This might be considered debt in strict mode",
)
.unwrap();
let result = analyzer.analyze_project(temp_dir.path()).await.unwrap();
assert!(result.total_files_analyzed <= 1);
}
#[tokio::test]
async fn test_analyze_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let analyzer = SATDAnalyzer::new();
let result = analyzer.analyze_project(temp_dir.path()).await.unwrap();
assert!(result.items.is_empty());
}
#[tokio::test]
async fn test_analyze_with_config() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(&test_file, "fn main() {}").unwrap();
let analyzer = SATDAnalyzer::new();
let input = ProjectInput {
project_path: temp_dir.path().to_path_buf(),
};
let config = ProjectConfig {
include_tests: true,
max_depth: None,
parallel: false,
};
let result = analyzer.analyze(input, config).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_analyze_multiple_files() {
let temp_dir = TempDir::new().unwrap();
fs::write(
temp_dir.path().join("file1.rs"),
"// TODO: Implement this\nfn stub1() {}",
)
.unwrap();
fs::write(
temp_dir.path().join("file2.rs"),
"// FIXME: Bug here\nfn stub2() {}",
)
.unwrap();
fs::write(temp_dir.path().join("file3.rs"), "fn clean_code() {}").unwrap();
let analyzer = SATDAnalyzer::new();
let result = analyzer.analyze_project(temp_dir.path()).await.unwrap();
let _ = result.total_files_analyzed;
}
#[tokio::test]
async fn test_analyze_nested_directory() {
let temp_dir = TempDir::new().unwrap();
let nested_dir = temp_dir.path().join("src").join("utils");
fs::create_dir_all(&nested_dir).unwrap();
fs::write(
nested_dir.join("helper.rs"),
"// HACK: Temporary solution\nfn helper() {}",
)
.unwrap();
let analyzer = SATDAnalyzer::new();
let result = analyzer.analyze_project(temp_dir.path()).await.unwrap();
let _ = result.total_files_analyzed;
}
#[tokio::test]
async fn test_project_analyzer_trait() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("test.rs"), "fn main() {}").unwrap();
let analyzer = SATDAnalyzer::new();
let result = ProjectAnalyzer::analyze_project(&analyzer, temp_dir.path()).await;
assert!(result.is_ok());
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}