semantic/analysis/
analysis_imports.rs1use std::collections::{HashMap, HashSet};
5
6use objects::object::SemanticChange;
7
8use crate::parser::{Language, ParsedFile};
9
10pub fn detect_import_changes(
15 old_path: &std::path::Path,
16 new_path: &std::path::Path,
17 old_content: &str,
18 new_content: &str,
19) -> Vec<SemanticChange> {
20 detect_import_changes_with_manifest(old_path, new_path, old_content, new_content, None)
21}
22
23pub fn detect_import_changes_with_manifest(
25 old_path: &std::path::Path,
26 new_path: &std::path::Path,
27 old_content: &str,
28 new_content: &str,
29 manifest_content: Option<&str>,
30) -> Vec<SemanticChange> {
31 let old_parsed = ParsedFile::parse(old_content, Language::from_path(old_path));
32 let new_parsed = ParsedFile::parse(new_content, Language::from_path(new_path));
33
34 detect_import_changes_with_parsed(
35 old_path,
36 new_path,
37 old_parsed.as_ref(),
38 new_parsed.as_ref(),
39 manifest_content,
40 )
41}
42
43pub(crate) fn detect_import_changes_with_parsed(
44 _old_path: &std::path::Path,
45 new_path: &std::path::Path,
46 old_parsed: Option<&ParsedFile>,
47 new_parsed: Option<&ParsedFile>,
48 manifest_content: Option<&str>,
49) -> Vec<SemanticChange> {
50 let mut changes = Vec::new();
51
52 let old_imports: HashSet<String> = old_parsed
53 .map(|p| p.extract_imports().into_iter().map(|i| i.raw).collect())
54 .unwrap_or_default();
55
56 let new_imports: HashSet<String> = new_parsed
57 .map(|p| p.extract_imports().into_iter().map(|i| i.raw).collect())
58 .unwrap_or_default();
59
60 let versions = manifest_content
61 .map(|m| parse_manifest_versions(m, Language::from_path(new_path)))
62 .unwrap_or_default();
63
64 let old_deps = dependency_names(&old_imports);
65 let new_deps = dependency_names(&new_imports);
66
67 for dep_name in new_deps.difference(&old_deps) {
68 let version = versions
69 .get(dep_name)
70 .cloned()
71 .unwrap_or_else(|| "unknown".to_string());
72 changes.push(SemanticChange::DependencyAdded {
73 name: dep_name.clone(),
74 version,
75 });
76 }
77
78 for dep_name in old_deps.difference(&new_deps) {
79 changes.push(SemanticChange::DependencyRemoved {
80 name: dep_name.clone(),
81 });
82 }
83
84 changes
85}
86
87fn dependency_names(imports: &HashSet<String>) -> HashSet<String> {
88 imports
89 .iter()
90 .filter_map(|import| extract_dependency_from_import(import))
91 .filter(|name| !is_stdlib_dependency(name))
92 .collect()
93}
94
95fn parse_manifest_versions(content: &str, language: Language) -> HashMap<String, String> {
97 match language {
98 Language::Rust => parse_cargo_toml_versions(content),
99 Language::JavaScript | Language::TypeScript => parse_package_json_versions(content),
100 _ => HashMap::new(),
101 }
102}
103
104fn parse_cargo_toml_versions(content: &str) -> HashMap<String, String> {
106 let mut versions = HashMap::new();
107 let mut in_deps = false;
109 for line in content.lines() {
110 let trimmed = line.trim();
111 if trimmed.starts_with('[') {
112 in_deps = trimmed == "[dependencies]"
113 || trimmed == "[dev-dependencies]"
114 || trimmed == "[build-dependencies]";
115 continue;
116 }
117 if !in_deps {
118 continue;
119 }
120 if let Some((name, rest)) = trimmed.split_once('=') {
122 let name = name.trim().trim_matches('"');
123 let rest = rest.trim();
124 if let Some(version) = rest.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
125 versions.insert(name.to_string(), version.to_string());
126 }
127 else if rest.starts_with('{')
129 && let Some(start) = rest.find("version")
130 {
131 let after = &rest[start..];
132 if let Some(eq) = after.find('=') {
133 let val = after[eq + 1..].trim().trim_start_matches('"');
134 if let Some(end) = val.find('"') {
135 versions.insert(name.to_string(), val[..end].to_string());
136 }
137 }
138 }
139 }
140 }
141 versions
142}
143
144fn parse_package_json_versions(content: &str) -> HashMap<String, String> {
147 let mut versions = HashMap::new();
148 let mut in_deps = false;
149 let mut brace_depth: i32 = 0;
150 for line in content.lines() {
151 let trimmed = line.trim();
152 if (trimmed.contains("\"dependencies\"")
154 || trimmed.contains("\"devDependencies\"")
155 || trimmed.contains("\"peerDependencies\""))
156 && trimmed.contains(':')
157 {
158 in_deps = true;
159 if trimmed.contains('{') {
160 brace_depth = 1;
161 }
162 continue;
163 }
164 if in_deps {
165 brace_depth += trimmed.matches('{').count() as i32;
166 brace_depth -= trimmed.matches('}').count() as i32;
167 if brace_depth <= 0 {
168 in_deps = false;
169 continue;
170 }
171 if let Some((name_part, version_part)) = trimmed.split_once(':') {
173 let name = name_part.trim().trim_matches(|c| c == '"' || c == ',');
174 let version = version_part
175 .trim()
176 .trim_matches(|c| c == '"' || c == ',' || c == ' ');
177 if !name.is_empty() && !version.is_empty() && !version.starts_with('{') {
178 versions.insert(name.to_string(), version.to_string());
179 }
180 }
181 }
182 }
183 versions
184}
185
186fn extract_dependency_from_import(import: &str) -> Option<String> {
187 let trimmed = import.trim();
188
189 if let Some(stripped) = trimmed.strip_prefix("use ") {
190 let path = stripped.trim_end_matches(';');
191 let first = path.split("::").next()?;
192 return Some(first.to_string());
193 }
194
195 if trimmed.starts_with("extern crate ") {
196 let parts: Vec<&str> = trimmed.split_whitespace().collect();
197 if parts.len() >= 3 {
198 return Some(parts[2].trim_end_matches(';').to_string());
199 }
200 }
201
202 None
203}
204
205fn is_stdlib_dependency(name: &str) -> bool {
206 matches!(name, "std" | "core" | "alloc" | "crate" | "super" | "self")
207}