#![cfg_attr(coverage_nightly, coverage(off))]
use anyhow::Result;
use std::path::Path;
use crate::models::error::TemplateError;
use crate::services::accurate_complexity_analyzer::AccurateComplexityAnalyzer;
use crate::services::complexity::{ComplexityMetrics, FileComplexityMetrics, FunctionComplexity};
use crate::services::context::FileContext;
use crate::services::file_classifier::FileClassifier;
use crate::services::source_line_index::LineSpan;
use crate::services::enhanced_ast_visitor::EnhancedAstVisitor;
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn analyze_rust_file_with_complexity(
path: &Path,
) -> Result<FileComplexityMetrics, TemplateError> {
analyze_rust_file_with_complexity_and_classifier(path, None).await
}
#[allow(clippy::cast_possible_truncation)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn analyze_rust_file_with_complexity_and_classifier(
path: &Path,
_classifier: Option<&FileClassifier>,
) -> Result<FileComplexityMetrics, TemplateError> {
let analyzer = AccurateComplexityAnalyzer::new();
let accurate_result = analyzer
.analyze_file(path)
.await
.map_err(|e| TemplateError::InvalidUtf8(e.to_string()))?;
let mut function_metrics = Vec::new();
let mut total_cyclomatic = 0u32;
let mut total_cognitive = 0u32;
let mut max_nesting = 0u32;
for func in &accurate_result.functions {
total_cyclomatic += func.cyclomatic_complexity;
total_cognitive += func.cognitive_complexity;
max_nesting = max_nesting.max(func.max_nesting);
let span = LineSpan {
start: func.line_start,
end: func.line_end,
};
function_metrics.push(FunctionComplexity {
name: func.name.clone(),
line_start: func.line_start,
line_end: func.line_end,
metrics: ComplexityMetrics {
cyclomatic: clamp_u16(func.cyclomatic_complexity),
cognitive: clamp_u16(func.cognitive_complexity),
nesting_max: clamp_u8(func.max_nesting),
lines: clamp_u16(span.line_count()),
halstead: None,
},
});
}
let avg_cyclomatic = if function_metrics.is_empty() {
1
} else {
total_cyclomatic / function_metrics.len() as u32
};
let avg_cognitive = if function_metrics.is_empty() {
0
} else {
total_cognitive / function_metrics.len() as u32
};
Ok(FileComplexityMetrics {
path: path.display().to_string(),
total_complexity: ComplexityMetrics {
cyclomatic: clamp_u16(avg_cyclomatic),
cognitive: clamp_u16(avg_cognitive),
nesting_max: clamp_u8(max_nesting),
lines: clamp_u16(accurate_result.total_lines),
halstead: None,
},
functions: function_metrics,
classes: Vec::new(), })
}
fn clamp_u16(value: u32) -> u16 {
u16::try_from(value).unwrap_or(u16::MAX)
}
fn clamp_u8(value: u32) -> u8 {
u8::try_from(value).unwrap_or(u8::MAX)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn analyze_rust_file(path: &Path) -> Result<FileContext, TemplateError> {
analyze_rust_file_with_classifier(path, None).await
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn analyze_rust_file_with_classifier(
path: &Path,
_classifier: Option<&FileClassifier>,
) -> Result<FileContext, TemplateError> {
let content = tokio::fs::read_to_string(path)
.await
.map_err(TemplateError::Io)?;
let syntax_tree =
syn::parse_file(&content).map_err(|e| TemplateError::InvalidUtf8(e.to_string()))?;
let visitor = EnhancedAstVisitor::new(path, &content);
let items = visitor.extract_items(&syntax_tree);
Ok(FileContext {
path: path.display().to_string(),
language: "rust".to_string(),
items,
complexity_metrics: None,
})
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod fabrication_tests {
use super::*;
async fn metrics_for(source: &str) -> FileComplexityMetrics {
let dir = tempfile::TempDir::new().unwrap();
let file = dir.path().join("lib.rs");
std::fs::write(&file, source).unwrap();
analyze_rust_file_with_complexity(&file).await.unwrap()
}
#[tokio::test]
async fn test_extents_match_the_source_not_a_50_line_guess() {
let source = concat!(
"pub fn cc_six(a: i32) -> i32 {\n",
" let mut t = 0;\n",
" if a > 1 { t += 1; }\n",
" if a > 2 { t += 1; }\n",
" if a > 3 { t += 1; }\n",
" if a > 4 { t += 1; }\n",
" if a > 5 { t += 1; }\n",
" t\n",
"}\n",
"\n",
"pub fn cc_one(a: i32) -> i32 {\n",
" a + 1\n",
"}\n",
);
let metrics = metrics_for(source).await;
assert_eq!(
metrics.total_complexity.lines, 13,
"file is 13 lines; used to be reported as 61"
);
let cc_six = &metrics.functions[0];
assert_eq!((cc_six.line_start, cc_six.line_end), (1, 9));
assert_eq!(cc_six.metrics.lines, 9);
let cc_one = &metrics.functions[1];
assert_eq!(
(cc_one.line_start, cc_one.line_end),
(11, 13),
"last function used to be reported as 11-61"
);
assert_eq!(cc_one.metrics.lines, 3, "used to be the constant 50");
}
#[tokio::test]
async fn test_line_end_never_exceeds_the_file() {
let metrics = metrics_for("fn only_one() -> i32 { 42 }\n").await;
assert_eq!(metrics.total_complexity.lines, 1);
let only = &metrics.functions[0];
assert_eq!(only.line_start, 1);
assert_eq!(only.line_end, 1, "used to be 51, i.e. 50 lines past EOF");
assert_eq!(only.metrics.lines, 1);
}
#[tokio::test]
async fn test_all_extents_stay_within_the_file() {
let source = concat!(
"fn a() {}\n",
"\n",
"fn b(x: i32) -> i32 {\n",
" if x > 0 { 1 } else { 0 }\n",
"}\n",
"\n",
"fn another_orphan() -> u8 {\n",
" 3\n",
"}\n",
);
let metrics = metrics_for(source).await;
let total = metrics.total_complexity.lines;
assert_eq!(total, 9);
for func in &metrics.functions {
assert!(
func.line_start >= 1 && func.line_end <= u32::from(total),
"{} spans {}-{} in a {}-line file",
func.name,
func.line_start,
func.line_end,
total
);
assert!(func.line_end >= func.line_start);
}
}
#[tokio::test]
async fn test_empty_file_reports_zero_lines_and_no_functions() {
let metrics = metrics_for("").await;
assert!(metrics.functions.is_empty());
assert_eq!(metrics.total_complexity.lines, 0);
}
}