use std::collections::{HashMap, HashSet};
use objects::object::SemanticChange;
use crate::parser::{Language, ParsedFile};
pub fn detect_import_changes(
old_path: &std::path::Path,
new_path: &std::path::Path,
old_content: &str,
new_content: &str,
) -> Vec<SemanticChange> {
detect_import_changes_with_manifest(old_path, new_path, old_content, new_content, None)
}
pub fn detect_import_changes_with_manifest(
old_path: &std::path::Path,
new_path: &std::path::Path,
old_content: &str,
new_content: &str,
manifest_content: Option<&str>,
) -> Vec<SemanticChange> {
let old_parsed = ParsedFile::parse(old_content, Language::from_path(old_path));
let new_parsed = ParsedFile::parse(new_content, Language::from_path(new_path));
detect_import_changes_with_parsed(
old_path,
new_path,
old_parsed.as_ref(),
new_parsed.as_ref(),
manifest_content,
)
}
pub(crate) fn detect_import_changes_with_parsed(
_old_path: &std::path::Path,
new_path: &std::path::Path,
old_parsed: Option<&ParsedFile>,
new_parsed: Option<&ParsedFile>,
manifest_content: Option<&str>,
) -> Vec<SemanticChange> {
let mut changes = Vec::new();
let old_imports: HashSet<String> = old_parsed
.map(|p| p.extract_imports().into_iter().map(|i| i.raw).collect())
.unwrap_or_default();
let new_imports: HashSet<String> = new_parsed
.map(|p| p.extract_imports().into_iter().map(|i| i.raw).collect())
.unwrap_or_default();
let versions = manifest_content
.map(|m| parse_manifest_versions(m, Language::from_path(new_path)))
.unwrap_or_default();
let old_deps = dependency_names(&old_imports);
let new_deps = dependency_names(&new_imports);
for dep_name in new_deps.difference(&old_deps) {
let version = versions
.get(dep_name)
.cloned()
.unwrap_or_else(|| "unknown".to_string());
changes.push(SemanticChange::DependencyAdded {
name: dep_name.clone(),
version,
});
}
for dep_name in old_deps.difference(&new_deps) {
changes.push(SemanticChange::DependencyRemoved {
name: dep_name.clone(),
});
}
changes
}
fn dependency_names(imports: &HashSet<String>) -> HashSet<String> {
imports
.iter()
.filter_map(|import| extract_dependency_from_import(import))
.filter(|name| !is_stdlib_dependency(name))
.collect()
}
fn parse_manifest_versions(content: &str, language: Language) -> HashMap<String, String> {
match language {
Language::Rust => parse_cargo_toml_versions(content),
Language::JavaScript | Language::TypeScript => parse_package_json_versions(content),
_ => HashMap::new(),
}
}
fn parse_cargo_toml_versions(content: &str) -> HashMap<String, String> {
let mut versions = HashMap::new();
let mut in_deps = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_deps = trimmed == "[dependencies]"
|| trimmed == "[dev-dependencies]"
|| trimmed == "[build-dependencies]";
continue;
}
if !in_deps {
continue;
}
if let Some((name, rest)) = trimmed.split_once('=') {
let name = name.trim().trim_matches('"');
let rest = rest.trim();
if let Some(version) = rest.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
versions.insert(name.to_string(), version.to_string());
}
else if rest.starts_with('{')
&& let Some(start) = rest.find("version")
{
let after = &rest[start..];
if let Some(eq) = after.find('=') {
let val = after[eq + 1..].trim().trim_start_matches('"');
if let Some(end) = val.find('"') {
versions.insert(name.to_string(), val[..end].to_string());
}
}
}
}
}
versions
}
fn parse_package_json_versions(content: &str) -> HashMap<String, String> {
let mut versions = HashMap::new();
let mut in_deps = false;
let mut brace_depth: i32 = 0;
for line in content.lines() {
let trimmed = line.trim();
if (trimmed.contains("\"dependencies\"")
|| trimmed.contains("\"devDependencies\"")
|| trimmed.contains("\"peerDependencies\""))
&& trimmed.contains(':')
{
in_deps = true;
if trimmed.contains('{') {
brace_depth = 1;
}
continue;
}
if in_deps {
brace_depth += trimmed.matches('{').count() as i32;
brace_depth -= trimmed.matches('}').count() as i32;
if brace_depth <= 0 {
in_deps = false;
continue;
}
if let Some((name_part, version_part)) = trimmed.split_once(':') {
let name = name_part.trim().trim_matches(|c| c == '"' || c == ',');
let version = version_part
.trim()
.trim_matches(|c| c == '"' || c == ',' || c == ' ');
if !name.is_empty() && !version.is_empty() && !version.starts_with('{') {
versions.insert(name.to_string(), version.to_string());
}
}
}
}
versions
}
fn extract_dependency_from_import(import: &str) -> Option<String> {
let trimmed = import.trim();
if let Some(stripped) = trimmed.strip_prefix("use ") {
let path = stripped.trim_end_matches(';');
let first = path.split("::").next()?;
return Some(first.to_string());
}
if trimmed.starts_with("extern crate ") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 3 {
return Some(parts[2].trim_end_matches(';').to_string());
}
}
None
}
fn is_stdlib_dependency(name: &str) -> bool {
matches!(name, "std" | "core" | "alloc" | "crate" | "super" | "self")
}