use std::path::{Path, PathBuf};
use crate::services::defect_detector::{LuaDefectDetector, RustDefectDetector, Severity};
use crate::tdg::language_simple::Language;
use crate::tdg::score::TdgScore;
pub(crate) const NEW_FILE_WAIVER: &str =
"file is not tracked by git; critical-defect auto-fail is not applied to code \
with no history (#279)";
pub(crate) fn apply(score: &mut TdgScore, source: &str, language: Language) {
let count = count_critical_defects(source, language);
score.critical_defects_count = count;
score.has_critical_defects = count > 0;
if score.has_critical_defects
&& score
.file_path
.as_deref()
.is_some_and(is_exempt_as_new_file)
{
score.critical_defects_suppressed = Some(NEW_FILE_WAIVER.to_string());
}
}
pub(crate) fn count_critical_defects(source: &str, language: Language) -> usize {
let label = Path::new("<source>");
let detected: usize = match language {
Language::Rust => critical_instances(&RustDefectDetector::new().detect(source, label)),
Language::Lua => critical_instances(&LuaDefectDetector::new().detect(source, label)),
_ => 0,
};
let lean_sorry = if language == Language::Lean {
count_lean_sorry(source)
} else {
0
};
detected + lean_sorry
}
fn critical_instances(defects: &[crate::services::defect_detector::DefectPattern]) -> usize {
defects
.iter()
.filter(|d| d.severity == Severity::Critical)
.map(|d| d.instances.len())
.sum()
}
pub(crate) fn is_exempt_as_new_file(path: &Path) -> bool {
matches!(git_tracking_status(path), GitTracking::UntrackedInRepo)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GitTracking {
Tracked,
UntrackedInRepo,
NotVersioned,
}
pub(crate) fn git_tracking_status(path: &Path) -> GitTracking {
let Some(repo_anchor) = git_anchor_for(path) else {
return GitTracking::NotVersioned;
};
let absolute = absolute_path(path);
let run = |args: &[&str], file: Option<&Path>| {
let mut cmd = std::process::Command::new("git");
cmd.arg("-C").arg(&repo_anchor).args(args);
if let Some(f) = file {
cmd.arg("--").arg(f);
}
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
};
let inside_work_tree = run(&["rev-parse", "--is-inside-work-tree"], None)
.map(|o| o.status.success() && String::from_utf8_lossy(&o.stdout).trim() == "true")
.unwrap_or(false);
if !inside_work_tree {
return GitTracking::NotVersioned;
}
let has_history = run(&["log", "--oneline", "-1"], Some(&absolute))
.map(|o| o.status.success() && !o.stdout.is_empty())
.unwrap_or(false);
if has_history {
GitTracking::Tracked
} else {
GitTracking::UntrackedInRepo
}
}
fn absolute_path(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| {
std::env::current_dir().map_or_else(|_| path.to_path_buf(), |cwd| cwd.join(path))
})
}
fn git_anchor_for(path: &Path) -> Option<PathBuf> {
let absolute = absolute_path(path);
if absolute.is_dir() {
return Some(absolute);
}
match absolute.parent() {
Some(parent) if !parent.as_os_str().is_empty() => Some(parent.to_path_buf()),
_ => Some(absolute),
}
}
#[cfg(test)]
pub(crate) fn is_file_git_tracked(path: &Path) -> bool {
matches!(git_tracking_status(path), GitTracking::Tracked)
}
pub(crate) fn count_lean_sorry(source: &str) -> usize {
let mut count = 0;
let mut in_block_comment: i32 = 0;
for line in source.lines() {
let trimmed = line.trim();
if trimmed.starts_with("--") {
continue;
}
let cleaned = strip_lean_block_comments(trimmed, &mut in_block_comment);
if in_block_comment > 0 {
continue;
}
if contains_lean_sorry_word(&cleaned) {
count += 1;
}
}
count
}
fn strip_lean_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_lean_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
|| (!bytes[abs_idx - 1].is_ascii_alphanumeric() && bytes[abs_idx - 1] != b'_');
let after_ok = abs_idx + sorry.len() >= bytes.len()
|| (!bytes[abs_idx + sorry.len()].is_ascii_alphanumeric()
&& bytes[abs_idx + sorry.len()] != b'_');
if before_ok && after_ok {
return true;
}
pos = abs_idx + 1;
} else {
break;
}
}
false
}