impl DependencyCache {
fn is_valid(&self, cargo_lock_path: &Path) -> bool {
if let Ok(metadata) = fs::metadata(cargo_lock_path) {
if let Ok(modified) = metadata.modified() {
let mtime = modified
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
return mtime == self.cargo_lock_mtime;
}
}
false
}
fn load(project_path: &Path) -> Option<Self> {
let cache_path = project_path.join(".pmat/deps-cache.json");
fs::read_to_string(&cache_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
}
fn save(&self, project_path: &Path) {
let cache_path = project_path.join(".pmat/deps-cache.json");
if let Some(parent) = cache_path.parent() {
let _ = fs::create_dir_all(parent);
}
if let Ok(json) = serde_json::to_string_pretty(self) {
let _ = fs::write(&cache_path, json);
}
}
}
pub(super) fn parse_cargo_lock(cargo_lock_path: &Path) -> (usize, Vec<DuplicateCrate>) {
let content = match fs::read_to_string(cargo_lock_path) {
Ok(c) => c,
Err(_) => return (0, Vec::new()),
};
let mut crate_versions: HashMap<String, Vec<String>> = HashMap::new();
let mut current_name: Option<String> = None;
let mut current_version: Option<String> = None;
let mut package_count = 0;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[[package]]" {
package_count += 1;
if let (Some(name), Some(version)) = (current_name.take(), current_version.take()) {
crate_versions.entry(name).or_default().push(version);
}
} else if let Some(name) = trimmed.strip_prefix("name = \"") {
current_name = name.strip_suffix('"').map(|s| s.to_string());
} else if let Some(version) = trimmed.strip_prefix("version = \"") {
current_version = version.strip_suffix('"').map(|s| s.to_string());
}
}
if let (Some(name), Some(version)) = (current_name, current_version) {
crate_versions.entry(name).or_default().push(version);
}
let duplicates: Vec<DuplicateCrate> = crate_versions
.into_iter()
.filter(|(_, versions)| versions.len() > 1)
.map(|(name, mut versions)| {
versions.sort();
versions.dedup();
DuplicateCrate { name, versions }
})
.filter(|d| d.versions.len() > 1)
.collect();
(package_count, duplicates)
}
pub(super) fn count_production_transitive(project_path: &Path) -> Option<usize> {
let output = std::process::Command::new("cargo")
.args(["tree", "-e", "no-dev", "--prefix=none"])
.current_dir(project_path)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut unique_packages: HashSet<String> = HashSet::new();
for line in stdout.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
if let Some(name) = trimmed.split_whitespace().next() {
unique_packages.insert(name.to_string());
}
}
}
Some(unique_packages.len())
}
pub(super) fn get_cached_dependency_analysis(
project_path: &Path,
cargo_lock_path: &Path,
) -> (usize, Option<usize>, Vec<DuplicateCrate>) {
if let Some(cache) = DependencyCache::load(project_path) {
if cache.is_valid(cargo_lock_path) {
return (
cache.transitive_count,
cache.prod_transitive_count,
cache.duplicate_crates,
);
}
}
let (transitive_count, duplicate_crates) = parse_cargo_lock(cargo_lock_path);
let prod_transitive_count = count_production_transitive(project_path);
let mtime = fs::metadata(cargo_lock_path)
.and_then(|m| m.modified())
.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
})
.unwrap_or(0);
let cache = DependencyCache {
cargo_lock_mtime: mtime,
transitive_count,
prod_transitive_count,
duplicate_crates: duplicate_crates.clone(),
};
cache.save(project_path);
(transitive_count, prod_transitive_count, duplicate_crates)
}