Skip to main content

lean_ctx/core/
deps.rs

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}