pub fn count_sorry(source: &str) -> usize {
let mut count = 0;
let mut in_block_comment = 0i32;
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("--") {
continue;
}
let cleaned = strip_block_comments(trimmed, &mut in_block_comment);
if in_block_comment > 0 {
continue;
}
if contains_sorry_word(&cleaned) {
count += 1;
}
}
count
}
fn strip_block_comments(line: &str, depth: &mut i32) -> String {
let bytes = line.as_bytes();
let mut result = String::with_capacity(line.len());
let mut i = 0;
while i < bytes.len() {
if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'-' {
*depth += 1;
i += 2;
continue;
}
if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'/' && *depth > 0 {
*depth -= 1;
i += 2;
continue;
}
if *depth == 0 {
result.push(bytes[i] as char);
}
i += 1;
}
result
}
fn contains_sorry_word(line: &str) -> bool {
let bytes = line.as_bytes();
let sorry = b"sorry";
let mut pos = 0;
while pos + sorry.len() <= bytes.len() {
if let Some(idx) = line[pos..].find("sorry") {
let abs_idx = pos + idx;
let before_ok = abs_idx == 0 || !is_ident_char(bytes[abs_idx - 1]);
let after_ok = abs_idx + sorry.len() >= bytes.len()
|| !is_ident_char(bytes[abs_idx + sorry.len()]);
if before_ok && after_ok {
return true;
}
pos = abs_idx + 1;
} else {
break;
}
}
false
}
fn is_ident_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
pub fn count_theorems(source: &str) -> usize {
source
.lines()
.filter(|line| {
let trimmed = line.trim();
trimmed.starts_with("theorem ")
|| trimmed.starts_with("lemma ")
|| trimmed.starts_with("private theorem ")
|| trimmed.starts_with("private lemma ")
})
.count()
}
impl Default for LeanComplexityAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl LeanComplexityAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
cyclomatic_complexity: 0,
cognitive_complexity: 0,
}
}
pub fn analyze_complexity(&mut self, source: &str) -> Result<(u32, u32), String> {
self.cyclomatic_complexity = 1;
self.cognitive_complexity = 1;
for line in source.lines() {
let trimmed = line.trim();
if trimmed.contains("if ")
|| trimmed.contains("match ")
|| trimmed.contains("| ")
|| trimmed.starts_with("by ")
|| trimmed.contains("sorry")
{
self.cyclomatic_complexity += 1;
self.cognitive_complexity += 1;
}
}
Ok((self.cyclomatic_complexity, self.cognitive_complexity))
}
}
pub async fn analyze_lean_file(
path: &Path,
) -> Result<crate::services::context::FileContext, crate::models::error::TemplateError> {
use crate::models::error::TemplateError;
use crate::services::context::FileContext;
let content = tokio::fs::read_to_string(path)
.await
.map_err(TemplateError::Io)?;
let visitor = LeanAstVisitor::new(path);
let items = visitor
.analyze_lean_source(&content)
.map_err(TemplateError::InvalidUtf8)?;
Ok(FileContext {
path: path.display().to_string(),
language: "lean".to_string(),
items,
complexity_metrics: None,
})
}