#![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,
},
});
}
Ok(FileComplexityMetrics {
path: path.display().to_string(),
total_complexity: ComplexityMetrics {
cyclomatic: clamp_u16(total_cyclomatic),
cognitive: clamp_u16(total_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);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod total_complexity_is_a_total_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()
}
fn sum_of_functions(metrics: &FileComplexityMetrics) -> u16 {
metrics.functions.iter().map(|f| f.metrics.cyclomatic).sum()
}
#[tokio::test]
async fn test_file_total_is_the_sum_not_the_mean() {
let source = concat!(
"pub fn branchy(a: i32) -> i32 {\n",
" if a > 1 { return 1; }\n",
" if a > 2 { return 2; }\n",
" 0\n",
"}\n",
"\n",
"pub fn trivial() -> i32 {\n",
" 7\n",
"}\n",
);
let metrics = metrics_for(source).await;
assert_eq!(metrics.functions.len(), 2);
let sum = sum_of_functions(&metrics);
assert_eq!(
metrics.total_complexity.cyclomatic, sum,
"total_complexity must equal the sum of the file's functions; \
the integer mean used to be reported here"
);
assert!(
metrics.total_complexity.cyclomatic > 2,
"the mean floor of this fixture was 2 — a value that is neither \
the sum ({sum}) nor the maximum"
);
}
#[tokio::test]
async fn test_trivial_helpers_cannot_lower_a_file_total() {
let mut worst = String::from("pub fn beast(k: i64) -> i64 {\n let mut r = 0i64;\n");
for i in 0..60 {
worst.push_str(&format!(" if k > {i} {{ r += {i}; }}\n"));
}
worst.push_str(" r\n}\n");
let beast_only = metrics_for(&worst).await;
for i in 0..60 {
worst.push_str(&format!("pub fn t{i}() -> i64 {{ {i} }}\n"));
}
let with_helpers = metrics_for(&worst).await;
assert_eq!(
with_helpers.total_complexity.cyclomatic,
sum_of_functions(&with_helpers)
);
assert!(
with_helpers.total_complexity.cyclomatic >= beast_only.total_complexity.cyclomatic,
"adding 60 trivial helpers dropped the reported total from {} to {} \
(the mean collapsed a true sum of 122 to 2)",
beast_only.total_complexity.cyclomatic,
with_helpers.total_complexity.cyclomatic
);
let mild: String = (0..8)
.map(|i| {
format!(
"pub fn m{i}(a:i32,b:i32)->i32{{ if a>b {{ a }} else if a<b {{ b }} else {{ 0 }} }}\n"
)
})
.collect();
let mild = metrics_for(&mild).await;
assert!(
with_helpers.total_complexity.cyclomatic > mild.total_complexity.cyclomatic,
"the file holding a 61-branch function reported {} while a file whose \
worst function is a 3 reported {} — this is the inverted 'Top Files \
by Complexity' ranking",
with_helpers.total_complexity.cyclomatic,
mild.total_complexity.cyclomatic
);
}
#[tokio::test]
async fn test_ast_and_heuristic_producers_agree_on_the_meaning() {
let source = concat!(
"pub fn a(x: i32) -> i32 {\n",
" if x > 0 { 1 } else { 0 }\n",
"}\n",
"pub fn b(x: i32) -> i32 {\n",
" if x > 0 { if x > 1 { 2 } else { 1 } } else { 0 }\n",
"}\n",
"pub fn c() -> i32 { 3 }\n",
);
let ast = metrics_for(source).await;
assert_eq!(ast.total_complexity.cyclomatic, sum_of_functions(&ast));
let heuristic = crate::cli::language_analyzer::analyze_with_heuristics(
Path::new("fragment.rs"),
source,
crate::cli::language_analyzer::Language::Rust,
)
.unwrap();
assert_eq!(
heuristic.total_complexity.cyclomatic,
sum_of_functions(&heuristic),
"the include!()-fragment producer must also report a sum"
);
}
}