#![cfg_attr(coverage_nightly, coverage(off))]
use anyhow::Result;
use std::path::{Path, PathBuf};
#[cfg(test)]
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::services::complexity::{ComplexityMetrics, FileComplexityMetrics, FunctionComplexity};
use crate::services::context::AstItem;
use crate::services::languages::go::GoAstVisitor;
pub struct UnifiedGoAnalyzer {
file_path: PathBuf,
#[cfg(test)]
parse_count: AtomicUsize,
}
#[derive(Debug)]
pub struct UnifiedAnalysis {
pub ast_items: Vec<AstItem>,
pub file_metrics: FileComplexityMetrics,
pub parsed_at: std::time::Instant,
}
#[derive(Debug, thiserror::Error)]
pub enum AnalysisError {
#[error("Failed to read file: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse Go syntax: {0}")]
Parse(String),
#[error("Analysis error: {0}")]
Analysis(String),
}
impl UnifiedGoAnalyzer {
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn new(file_path: PathBuf) -> Self {
Self {
file_path,
#[cfg(test)]
parse_count: AtomicUsize::new(0),
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn file_path(&self) -> &Path {
&self.file_path
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn analyze(&self) -> Result<UnifiedAnalysis, AnalysisError> {
#[cfg(test)]
{
self.parse_count.fetch_add(1, Ordering::SeqCst);
}
let content = tokio::fs::read_to_string(&self.file_path)
.await
.map_err(AnalysisError::Io)?;
let visitor = GoAstVisitor::new(&self.file_path);
let ast_items = visitor
.analyze_go_source(&content)
.map_err(AnalysisError::Analysis)?;
let file_metrics = self.extract_complexity_metrics(&content);
Ok(UnifiedAnalysis {
ast_items,
file_metrics,
parsed_at: std::time::Instant::now(),
})
}
#[cfg(test)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn parse_count(&self) -> usize {
self.parse_count.load(Ordering::SeqCst)
}
fn extract_complexity_metrics(&self, content: &str) -> FileComplexityMetrics {
let mut functions = Vec::new();
let lines = content.lines().count();
let function_pattern = regex::Regex::new(r"(?m)^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(")
.expect("valid static Go function regex");
for cap in function_pattern.captures_iter(content) {
let name = cap
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_else(|| "anonymous".to_string());
let cyclomatic = self.estimate_complexity(content);
functions.push(FunctionComplexity {
name,
line_start: 0, line_end: 0,
metrics: ComplexityMetrics {
cyclomatic: cyclomatic as u16,
cognitive: cyclomatic as u16, nesting_max: 0,
lines: 10, halstead: None,
},
});
}
let total_cyclomatic: u32 = functions.iter().map(|f| f.metrics.cyclomatic as u32).sum();
let avg_cyclomatic = if functions.is_empty() {
1
} else {
total_cyclomatic / functions.len() as u32
};
FileComplexityMetrics {
path: self.file_path.display().to_string(),
total_complexity: ComplexityMetrics {
cyclomatic: avg_cyclomatic as u16,
cognitive: avg_cyclomatic as u16,
nesting_max: 0,
lines: lines as u16,
halstead: None,
},
functions,
classes: Vec::new(), }
}
fn estimate_complexity(&self, content: &str) -> u32 {
let mut complexity = 1;
let keywords = [
"if ", "else if", "for ", "switch ", "case ", "&&", "||", "select ", ];
for keyword in &keywords {
complexity += content.matches(keyword).count() as u32;
}
complexity
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyzer_creation() {
let path = PathBuf::from("test.go");
let analyzer = UnifiedGoAnalyzer::new(path.clone());
assert_eq!(analyzer.file_path(), path.as_path());
}
#[tokio::test]
async fn test_parse_count_increments() {
let temp_file = tempfile::NamedTempFile::with_suffix(".go").unwrap();
std::fs::write(temp_file.path(), "package main\n\nfunc main() {}").unwrap();
let analyzer = UnifiedGoAnalyzer::new(temp_file.path().to_path_buf());
assert_eq!(analyzer.parse_count(), 0);
let _ = analyzer.analyze().await;
assert_eq!(analyzer.parse_count(), 1);
}
}