use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
pub(crate) const STRONG_PENALTY: f32 = 0.3;
pub(crate) const STALE_PENALTY: f32 = 0.7;
pub(crate) const MIN_MULTIPLIER: f32 = 0.1;
const STALE_THRESHOLD: Duration = Duration::from_secs(60 * 60 * 24 * 365);
const ARCHIVE_PATH_KEYWORDS: &[&str] = &[
"archive",
"deprecated",
"legacy",
"/old/",
"backup",
"obsolete",
"unused",
];
const ARCHIVE_ANNOTATIONS: &[&str] = &[
"#[deprecated]",
"@deprecated",
"// TODO: remove",
"// FIXME: obsolete",
"// DEPRECATED",
"#[allow(deprecated)]",
];
pub(crate) fn path_keyword_reason(path: &str) -> Option<String> {
let lower = path.to_ascii_lowercase();
for kw in ARCHIVE_PATH_KEYWORDS {
if lower.contains(kw) {
let label = kw.trim_matches('/');
return Some(format!("path:{label}"));
}
}
None
}
pub(crate) fn annotation_reason(content: &str) -> Option<String> {
for pat in ARCHIVE_ANNOTATIONS {
if content.contains(pat) {
return Some(format!("annotation:{pat}"));
}
}
None
}
const MARKER_FILES: &[&str] = &[".archived", "DEPRECATED"];
#[derive(Default)]
pub(crate) struct MarkerCache {
inner: HashMap<PathBuf, Option<String>>,
}
impl MarkerCache {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn reason_for(&mut self, root_path: &Path, chunk_file: &str) -> Option<String> {
let abs = root_path.join(chunk_file);
let dir = abs.parent()?.to_path_buf();
if let Some(cached) = self.inner.get(&dir) {
return cached.clone();
}
let mut found: Option<String> = None;
for marker in MARKER_FILES {
if dir.join(marker).exists() {
found = Some(format!("marker:{marker}"));
break;
}
}
self.inner.insert(dir, found.clone());
found
}
}
pub(crate) fn stale_reason(root_path: &Path, chunk_file: &str) -> Option<String> {
let abs = root_path.join(chunk_file);
let meta = std::fs::metadata(&abs).ok()?;
let mtime = meta.modified().ok()?;
let age = SystemTime::now().duration_since(mtime).ok()?;
if age > STALE_THRESHOLD {
Some("stale:git_mtime".to_string())
} else {
None
}
}
pub(crate) fn classify(
root_path: &Path,
chunk_file: &str,
chunk_content: &str,
markers: &mut MarkerCache,
) -> (f32, Option<String>) {
let mut multiplier = 1.0_f32;
let mut first_reason: Option<String> = None;
let mut strong_fired = false;
if let Some(reason) = path_keyword_reason(chunk_file) {
multiplier *= STRONG_PENALTY;
first_reason.get_or_insert(reason);
strong_fired = true;
}
if let Some(reason) = annotation_reason(chunk_content) {
multiplier *= STRONG_PENALTY;
first_reason.get_or_insert(reason);
strong_fired = true;
}
if let Some(reason) = markers.reason_for(root_path, chunk_file) {
multiplier *= STRONG_PENALTY;
first_reason.get_or_insert(reason);
strong_fired = true;
}
if !strong_fired {
if let Some(reason) = stale_reason(root_path, chunk_file) {
multiplier *= STALE_PENALTY;
first_reason.get_or_insert(reason);
}
}
if multiplier < MIN_MULTIPLIER {
multiplier = MIN_MULTIPLIER;
}
(multiplier, first_reason)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_keyword_matches_lowercase_substring() {
assert_eq!(
path_keyword_reason("src/Legacy/foo.rs"),
Some("path:legacy".to_string())
);
assert_eq!(
path_keyword_reason("crates/deprecated/bar.rs"),
Some("path:deprecated".to_string())
);
assert_eq!(
path_keyword_reason("src/old/foo.rs"),
Some("path:old".to_string())
);
assert_eq!(path_keyword_reason("src/main.rs"), None);
}
#[test]
fn test_annotation_matches_deprecated_macro() {
assert_eq!(
annotation_reason("#[deprecated]\nfn old() {}"),
Some("annotation:#[deprecated]".to_string())
);
assert_eq!(
annotation_reason("/** @deprecated use new_fn */"),
Some("annotation:@deprecated".to_string())
);
assert_eq!(annotation_reason("fn alive() {}"), None);
}
#[test]
fn test_marker_file_check_is_cached_per_directory() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("legacy_module");
std::fs::create_dir(&dir).unwrap();
std::fs::write(dir.join(".archived"), "").unwrap();
std::fs::write(dir.join("a.rs"), "fn a() {}").unwrap();
std::fs::write(dir.join("b.rs"), "fn b() {}").unwrap();
let mut cache = MarkerCache::new();
let r1 = cache.reason_for(tmp.path(), "legacy_module/a.rs");
let r2 = cache.reason_for(tmp.path(), "legacy_module/b.rs");
assert_eq!(r1, Some("marker:.archived".to_string()));
assert_eq!(r2, Some("marker:.archived".to_string()));
assert_eq!(cache.inner.len(), 1);
}
#[test]
fn test_marker_file_detected_in_parent_dir() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("retiring");
std::fs::create_dir(&dir).unwrap();
std::fs::write(dir.join("DEPRECATED"), "use new_module instead").unwrap();
std::fs::write(dir.join("foo.rs"), "fn foo() {}").unwrap();
let mut cache = MarkerCache::new();
let r = cache.reason_for(tmp.path(), "retiring/foo.rs");
assert_eq!(r, Some("marker:DEPRECATED".to_string()));
}
#[test]
fn test_penalties_stack_with_floor() {
let tmp = tempfile::tempdir().unwrap();
let mut cache = MarkerCache::new();
let (mult, reason) = classify(
tmp.path(),
"src/legacy/foo.rs",
"#[deprecated]\nfn old() {}",
&mut cache,
);
assert!((mult - MIN_MULTIPLIER).abs() < f32::EPSILON);
assert_eq!(reason, Some("path:legacy".to_string()));
}
#[test]
fn test_clean_chunk_passes_through() {
let tmp = tempfile::tempdir().unwrap();
let mut cache = MarkerCache::new();
let (mult, reason) = classify(tmp.path(), "src/main.rs", "fn main() {}", &mut cache);
assert!((mult - 1.0).abs() < f32::EPSILON);
assert_eq!(reason, None);
}
}