1use regex::Regex;
2use std::collections::HashSet;
3use std::sync::OnceLock;
4
5static IMPORT_RE: OnceLock<Regex> = OnceLock::new();
6static REQUIRE_RE: OnceLock<Regex> = OnceLock::new();
7static RUST_USE_RE: OnceLock<Regex> = OnceLock::new();
8static PY_IMPORT_RE: OnceLock<Regex> = OnceLock::new();
9static GO_IMPORT_RE: OnceLock<Regex> = OnceLock::new();
10
11fn import_re() -> &'static Regex {
12 IMPORT_RE.get_or_init(|| {
13 Regex::new(r#"import\s+(?:\{[^}]*\}\s+from\s+|.*from\s+)['"]([^'"]+)['"]"#).unwrap()
14 })
15}
16fn require_re() -> &'static Regex {
17 REQUIRE_RE.get_or_init(|| Regex::new(r#"require\(['"]([^'"]+)['"]\)"#).unwrap())
18}
19fn rust_use_re() -> &'static Regex {
20 RUST_USE_RE.get_or_init(|| Regex::new(r"^use\s+([\w:]+)").unwrap())
21}
22fn py_import_re() -> &'static Regex {
23 PY_IMPORT_RE.get_or_init(|| Regex::new(r"^(?:from\s+(\S+)\s+import|import\s+(\S+))").unwrap())
24}
25fn go_import_re() -> &'static Regex {
26 GO_IMPORT_RE.get_or_init(|| Regex::new(r#""([^"]+)""#).unwrap())
27}
28
29#[derive(Debug, Clone)]
30pub struct DepInfo {
31 pub imports: Vec<String>,
32 pub exports: Vec<String>,
33}
34
35pub fn extract_deps(content: &str, ext: &str) -> DepInfo {
36 match ext {
37 "ts" | "tsx" | "js" | "jsx" | "svelte" | "vue" => extract_ts_deps(content),
38 "rs" => extract_rust_deps(content),
39 "py" => extract_python_deps(content),
40 "go" => extract_go_deps(content),
41 _ => DepInfo {
42 imports: Vec::new(),
43 exports: Vec::new(),
44 },
45 }
46}
47
48fn extract_ts_deps(content: &str) -> DepInfo {
49 let mut imports = HashSet::new();
50 let mut exports = Vec::new();
51
52 for line in content.lines() {
53 let trimmed = line.trim();
54
55 if let Some(caps) = import_re().captures(trimmed) {
56 let path = &caps[1];
57 if path.starts_with('.') || path.starts_with('/') {
58 imports.insert(clean_import_path(path));
59 }
60 }
61 if let Some(caps) = require_re().captures(trimmed) {
62 let path = &caps[1];
63 if path.starts_with('.') || path.starts_with('/') {
64 imports.insert(clean_import_path(path));
65 }
66 }
67
68 if trimmed.starts_with("export ") {
69 if let Some(name) = extract_export_name(trimmed) {
70 exports.push(name);
71 }
72 }
73 }
74
75 DepInfo {
76 imports: imports.into_iter().collect(),
77 exports,
78 }
79}
80
81fn extract_rust_deps(content: &str) -> DepInfo {
82 let mut imports = HashSet::new();
83 let mut exports = Vec::new();
84
85 for line in content.lines() {
86 let trimmed = line.trim();
87
88 if let Some(caps) = rust_use_re().captures(trimmed) {
89 let path = &caps[1];
90 if !path.starts_with("std::") && !path.starts_with("core::") {
91 imports.insert(path.to_string());
92 }
93 }
94
95 if trimmed.starts_with("pub fn ") || trimmed.starts_with("pub async fn ") {
96 if let Some(name) = trimmed
97 .split('(')
98 .next()
99 .and_then(|s| s.split_whitespace().last())
100 {
101 exports.push(name.to_string());
102 }
103 } else if trimmed.starts_with("pub struct ")
104 || trimmed.starts_with("pub enum ")
105 || trimmed.starts_with("pub trait ")
106 {
107 if let Some(name) = trimmed.split_whitespace().nth(2) {
108 let clean = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
109 exports.push(clean.to_string());
110 }
111 }
112 }
113
114 DepInfo {
115 imports: imports.into_iter().collect(),
116 exports,
117 }
118}
119
120fn extract_python_deps(content: &str) -> DepInfo {
121 let mut imports = HashSet::new();
122 let mut exports = Vec::new();
123
124 for line in content.lines() {
125 let trimmed = line.trim();
126
127 if let Some(caps) = py_import_re().captures(trimmed) {
128 if let Some(m) = caps.get(1).or(caps.get(2)) {
129 let module = m.as_str();
130 if !module.starts_with("os")
131 && !module.starts_with("sys")
132 && !module.starts_with("json")
133 {
134 imports.insert(module.to_string());
135 }
136 }
137 }
138
139 if trimmed.starts_with("def ") && !trimmed.contains("_") {
140 if let Some(name) = trimmed
141 .strip_prefix("def ")
142 .and_then(|s| s.split('(').next())
143 {
144 exports.push(name.to_string());
145 }
146 } else if trimmed.starts_with("class ") {
147 if let Some(name) = trimmed
148 .strip_prefix("class ")
149 .and_then(|s| s.split(['(', ':']).next())
150 {
151 exports.push(name.to_string());
152 }
153 }
154 }
155
156 DepInfo {
157 imports: imports.into_iter().collect(),
158 exports,
159 }
160}
161
162fn extract_go_deps(content: &str) -> DepInfo {
163 let mut imports = HashSet::new();
164 let mut exports = Vec::new();
165
166 let mut in_import_block = false;
167 for line in content.lines() {
168 let trimmed = line.trim();
169
170 if trimmed.starts_with("import (") {
171 in_import_block = true;
172 continue;
173 }
174 if in_import_block {
175 if trimmed == ")" {
176 in_import_block = false;
177 continue;
178 }
179 if let Some(caps) = go_import_re().captures(trimmed) {
180 imports.insert(caps[1].to_string());
181 }
182 }
183
184 if trimmed.starts_with("func ") {
185 let name_part = trimmed.strip_prefix("func ").unwrap_or("");
186 if let Some(name) = name_part.split('(').next() {
187 let name = name.trim();
188 if !name.is_empty() && name.starts_with(char::is_uppercase) {
189 exports.push(name.to_string());
190 }
191 }
192 }
193 }
194
195 DepInfo {
196 imports: imports.into_iter().collect(),
197 exports,
198 }
199}
200
201fn clean_import_path(path: &str) -> String {
202 path.trim_start_matches("./")
203 .trim_end_matches(".js")
204 .trim_end_matches(".ts")
205 .trim_end_matches(".tsx")
206 .trim_end_matches(".jsx")
207 .to_string()
208}
209
210fn extract_export_name(line: &str) -> Option<String> {
211 let without_export = line.strip_prefix("export ")?;
212 let without_default = without_export
213 .strip_prefix("default ")
214 .unwrap_or(without_export);
215
216 for keyword in &[
217 "function ",
218 "async function ",
219 "class ",
220 "const ",
221 "let ",
222 "type ",
223 "interface ",
224 "enum ",
225 ] {
226 if let Some(rest) = without_default.strip_prefix(keyword) {
227 let name = rest
228 .split(|c: char| !c.is_alphanumeric() && c != '_')
229 .next()?;
230 if !name.is_empty() {
231 return Some(name.to_string());
232 }
233 }
234 }
235
236 None
237}