1use regex::Regex;
2use std::collections::HashSet;
3use std::sync::OnceLock;
4
5#[cfg(feature = "tree-sitter")]
6use super::deep_queries::{self, ImportKind};
7
8static IMPORT_RE: OnceLock<Regex> = OnceLock::new();
9static REQUIRE_RE: OnceLock<Regex> = OnceLock::new();
10static RUST_USE_RE: OnceLock<Regex> = OnceLock::new();
11static PY_IMPORT_RE: OnceLock<Regex> = OnceLock::new();
12static GO_IMPORT_RE: OnceLock<Regex> = OnceLock::new();
13static C_INCLUDE_RE: OnceLock<Regex> = OnceLock::new();
14static RUBY_REQUIRE_RE: OnceLock<Regex> = OnceLock::new();
15static PHP_INCLUDE_RE: OnceLock<Regex> = OnceLock::new();
16static BASH_SOURCE_RE: OnceLock<Regex> = OnceLock::new();
17static DART_IMPORT_RE: OnceLock<Regex> = OnceLock::new();
18static ZIG_IMPORT_RE: OnceLock<Regex> = OnceLock::new();
19
20fn import_re() -> &'static Regex {
21 IMPORT_RE.get_or_init(|| {
22 Regex::new(r#"import\s+(?:\{[^}]*\}\s+from\s+|.*from\s+)['"]([^'"]+)['"]"#).unwrap()
23 })
24}
25fn require_re() -> &'static Regex {
26 REQUIRE_RE.get_or_init(|| Regex::new(r#"require\(['"]([^'"]+)['"]\)"#).unwrap())
27}
28fn rust_use_re() -> &'static Regex {
29 RUST_USE_RE.get_or_init(|| Regex::new(r"^use\s+([\w:]+)").unwrap())
30}
31fn py_import_re() -> &'static Regex {
32 PY_IMPORT_RE.get_or_init(|| Regex::new(r"^(?:from\s+(\S+)\s+import|import\s+(\S+))").unwrap())
33}
34fn go_import_re() -> &'static Regex {
35 GO_IMPORT_RE.get_or_init(|| Regex::new(r#""([^"]+)""#).unwrap())
36}
37
38#[derive(Debug, Clone)]
39pub struct DepInfo {
40 pub imports: Vec<String>,
41 pub exports: Vec<String>,
42}
43
44pub fn extract_deps(content: &str, ext: &str) -> DepInfo {
45 let lang = crate::core::language_capabilities::language_for_ext(ext);
46 match lang {
47 Some(crate::core::language_capabilities::LanguageId::TypeScript)
48 | Some(crate::core::language_capabilities::LanguageId::JavaScript)
49 | Some(crate::core::language_capabilities::LanguageId::Vue)
50 | Some(crate::core::language_capabilities::LanguageId::Svelte) => extract_ts_deps(content),
51 Some(crate::core::language_capabilities::LanguageId::Rust) => extract_rust_deps(content),
52 Some(crate::core::language_capabilities::LanguageId::Python) => {
53 extract_python_deps(content)
54 }
55 Some(crate::core::language_capabilities::LanguageId::Go) => extract_go_deps(content),
56 Some(crate::core::language_capabilities::LanguageId::C)
57 | Some(crate::core::language_capabilities::LanguageId::Cpp) => extract_c_like_deps(content),
58 Some(crate::core::language_capabilities::LanguageId::Ruby) => extract_ruby_deps(content),
59 Some(crate::core::language_capabilities::LanguageId::Php) => extract_php_deps(content),
60 Some(crate::core::language_capabilities::LanguageId::Bash) => extract_bash_deps(content),
61 Some(crate::core::language_capabilities::LanguageId::Kotlin) => {
62 extract_kotlin_deps(content)
63 }
64 Some(crate::core::language_capabilities::LanguageId::Dart) => {
65 let mut imports = HashSet::new();
66 let re = DART_IMPORT_RE.get_or_init(|| {
67 Regex::new(r#"^\s*(?:import|export|part)\s+['"]([^'"]+)['"]"#).unwrap()
68 });
69 for line in content.lines() {
70 let trimmed = line.trim();
71 if let Some(caps) = re.captures(trimmed) {
72 let p = caps[1].trim();
73 if p.starts_with('.') || p.starts_with('/') {
74 imports.insert(clean_path_like(p));
75 }
76 }
77 }
78 DepInfo {
79 imports: imports.into_iter().collect(),
80 exports: Vec::new(),
81 }
82 }
83 Some(crate::core::language_capabilities::LanguageId::Zig) => {
84 let mut imports = HashSet::new();
85 let re =
86 ZIG_IMPORT_RE.get_or_init(|| Regex::new(r#"@import\(\s*"([^"]+)"\s*\)"#).unwrap());
87 for line in content.lines() {
88 let trimmed = line.trim();
89 if let Some(caps) = re.captures(trimmed) {
90 let p = caps[1].trim();
91 if p.starts_with('.') || p.contains('/') || p.ends_with(".zig") {
92 imports.insert(clean_path_like(p));
93 }
94 }
95 }
96 DepInfo {
97 imports: imports.into_iter().collect(),
98 exports: Vec::new(),
99 }
100 }
101 _ => DepInfo {
102 imports: Vec::new(),
103 exports: Vec::new(),
104 },
105 }
106}
107
108fn extract_ts_deps(content: &str) -> DepInfo {
109 let mut imports = HashSet::new();
110 let mut exports = Vec::new();
111
112 for line in content.lines() {
113 let trimmed = line.trim();
114
115 if let Some(caps) = import_re().captures(trimmed) {
116 let path = &caps[1];
117 if path.starts_with('.') || path.starts_with('/') {
118 imports.insert(clean_import_path(path));
119 }
120 }
121 if let Some(caps) = require_re().captures(trimmed) {
122 let path = &caps[1];
123 if path.starts_with('.') || path.starts_with('/') {
124 imports.insert(clean_import_path(path));
125 }
126 }
127
128 if trimmed.starts_with("export ") {
129 if let Some(name) = extract_export_name(trimmed) {
130 exports.push(name);
131 }
132 }
133 }
134
135 DepInfo {
136 imports: imports.into_iter().collect(),
137 exports,
138 }
139}
140
141fn extract_rust_deps(content: &str) -> DepInfo {
142 let mut imports = HashSet::new();
143 let mut exports = Vec::new();
144
145 for line in content.lines() {
146 let trimmed = line.trim();
147
148 if let Some(caps) = rust_use_re().captures(trimmed) {
149 let path = &caps[1];
150 if !path.starts_with("std::") && !path.starts_with("core::") {
151 imports.insert(path.to_string());
152 }
153 }
154
155 if trimmed.starts_with("pub fn ") || trimmed.starts_with("pub async fn ") {
156 if let Some(name) = trimmed
157 .split('(')
158 .next()
159 .and_then(|s| s.split_whitespace().last())
160 {
161 exports.push(name.to_string());
162 }
163 } else if trimmed.starts_with("pub struct ")
164 || trimmed.starts_with("pub enum ")
165 || trimmed.starts_with("pub trait ")
166 {
167 if let Some(name) = trimmed.split_whitespace().nth(2) {
168 let clean = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
169 exports.push(clean.to_string());
170 }
171 }
172 }
173
174 DepInfo {
175 imports: imports.into_iter().collect(),
176 exports,
177 }
178}
179
180fn extract_python_deps(content: &str) -> DepInfo {
181 let mut imports = HashSet::new();
182 let mut exports = Vec::new();
183
184 for line in content.lines() {
185 let trimmed = line.trim();
186
187 if let Some(caps) = py_import_re().captures(trimmed) {
188 if let Some(m) = caps.get(1).or(caps.get(2)) {
189 let module = m.as_str();
190 if !module.starts_with("os")
191 && !module.starts_with("sys")
192 && !module.starts_with("json")
193 {
194 imports.insert(module.to_string());
195 }
196 }
197 }
198
199 if trimmed.starts_with("def ") && !trimmed.contains("_") {
200 if let Some(name) = trimmed
201 .strip_prefix("def ")
202 .and_then(|s| s.split('(').next())
203 {
204 exports.push(name.to_string());
205 }
206 } else if trimmed.starts_with("class ") {
207 if let Some(name) = trimmed
208 .strip_prefix("class ")
209 .and_then(|s| s.split(['(', ':']).next())
210 {
211 exports.push(name.to_string());
212 }
213 }
214 }
215
216 DepInfo {
217 imports: imports.into_iter().collect(),
218 exports,
219 }
220}
221
222fn extract_go_deps(content: &str) -> DepInfo {
223 let mut imports = HashSet::new();
224 let mut exports = Vec::new();
225
226 let mut in_import_block = false;
227 for line in content.lines() {
228 let trimmed = line.trim();
229
230 if trimmed.starts_with("import (") {
231 in_import_block = true;
232 continue;
233 }
234 if in_import_block {
235 if trimmed == ")" {
236 in_import_block = false;
237 continue;
238 }
239 if let Some(caps) = go_import_re().captures(trimmed) {
240 imports.insert(caps[1].to_string());
241 }
242 }
243
244 if trimmed.starts_with("func ") {
245 let name_part = trimmed.strip_prefix("func ").unwrap_or("");
246 if let Some(name) = name_part.split('(').next() {
247 let name = name.trim();
248 if !name.is_empty() && name.starts_with(char::is_uppercase) {
249 exports.push(name.to_string());
250 }
251 }
252 }
253 }
254
255 DepInfo {
256 imports: imports.into_iter().collect(),
257 exports,
258 }
259}
260
261#[cfg(feature = "tree-sitter")]
262fn extract_kotlin_deps(content: &str) -> DepInfo {
263 let analysis = deep_queries::analyze(content, "kt");
264 let imports = analysis
265 .imports
266 .into_iter()
267 .map(|import| match import.kind {
268 ImportKind::Star => format!("{}.*", import.source),
269 _ => import.source,
270 })
271 .collect();
272
273 DepInfo {
274 imports,
275 exports: analysis.exports,
276 }
277}
278
279#[cfg(not(feature = "tree-sitter"))]
280fn extract_kotlin_deps(_content: &str) -> DepInfo {
281 DepInfo {
282 imports: Vec::new(),
283 exports: Vec::new(),
284 }
285}
286
287fn clean_import_path(path: &str) -> String {
288 path.trim_start_matches("./")
289 .trim_end_matches(".js")
290 .trim_end_matches(".ts")
291 .trim_end_matches(".tsx")
292 .trim_end_matches(".jsx")
293 .to_string()
294}
295
296fn clean_path_like(path: &str) -> String {
297 path.trim()
298 .trim_start_matches("./")
299 .trim_end_matches(".js")
300 .trim_end_matches(".ts")
301 .trim_end_matches(".tsx")
302 .trim_end_matches(".jsx")
303 .trim_end_matches(".py")
304 .trim_end_matches(".go")
305 .trim_end_matches(".rs")
306 .trim_end_matches(".c")
307 .trim_end_matches(".cpp")
308 .trim_end_matches(".h")
309 .trim_end_matches(".hpp")
310 .trim_end_matches(".php")
311 .trim_end_matches(".dart")
312 .trim_end_matches(".zig")
313 .trim_end_matches(".sh")
314 .trim_end_matches(".bash")
315 .to_string()
316}
317
318fn extract_c_like_deps(content: &str) -> DepInfo {
319 let mut imports = HashSet::new();
320 let re =
321 C_INCLUDE_RE.get_or_init(|| Regex::new(r#"^\s*#\s*include\s*[<"]([^">]+)[">]"#).unwrap());
322 for line in content.lines() {
323 let trimmed = line.trim();
324 if let Some(caps) = re.captures(trimmed) {
325 let inc = caps[1].trim();
326 if inc.starts_with('.') || inc.contains('/') {
327 imports.insert(clean_path_like(inc));
328 }
329 }
330 }
331 DepInfo {
332 imports: imports.into_iter().collect(),
333 exports: Vec::new(),
334 }
335}
336
337fn extract_ruby_deps(content: &str) -> DepInfo {
338 let mut imports = HashSet::new();
339 let re = RUBY_REQUIRE_RE
340 .get_or_init(|| Regex::new(r#"^\s*require(?:_relative)?\s+['"]([^'"]+)['"]"#).unwrap());
341 for line in content.lines() {
342 let trimmed = line.trim();
343 if let Some(caps) = re.captures(trimmed) {
344 let req = caps[1].trim();
345 if req.starts_with('.') || req.contains('/') {
346 imports.insert(clean_path_like(req));
347 }
348 }
349 }
350 DepInfo {
351 imports: imports.into_iter().collect(),
352 exports: Vec::new(),
353 }
354}
355
356fn extract_php_deps(content: &str) -> DepInfo {
357 let mut imports = HashSet::new();
358 let re = PHP_INCLUDE_RE.get_or_init(|| {
359 Regex::new(r#"\b(?:require|require_once|include|include_once)\s*\(?\s*['"]([^'"]+)['"]"#)
360 .unwrap()
361 });
362 for line in content.lines() {
363 let trimmed = line.trim();
364 if let Some(caps) = re.captures(trimmed) {
365 let p = caps[1].trim();
366 if p.starts_with('.') || p.starts_with('/') {
367 imports.insert(clean_path_like(p));
368 }
369 }
370 }
371 DepInfo {
372 imports: imports.into_iter().collect(),
373 exports: Vec::new(),
374 }
375}
376
377fn extract_bash_deps(content: &str) -> DepInfo {
378 let mut imports = HashSet::new();
379 let re = BASH_SOURCE_RE
380 .get_or_init(|| Regex::new(r#"^\s*(?:source|\.)\s+['"]?([^'"\s;]+)['"]?"#).unwrap());
381 for line in content.lines() {
382 let trimmed = line.trim();
383 if let Some(caps) = re.captures(trimmed) {
384 let p = caps[1].trim();
385 if p.starts_with('.') || p.starts_with('/') {
386 imports.insert(clean_path_like(p));
387 }
388 }
389 }
390 DepInfo {
391 imports: imports.into_iter().collect(),
392 exports: Vec::new(),
393 }
394}
395
396fn extract_export_name(line: &str) -> Option<String> {
397 let without_export = line.strip_prefix("export ")?;
398 let without_default = without_export
399 .strip_prefix("default ")
400 .unwrap_or(without_export);
401
402 for keyword in &[
403 "function ",
404 "async function ",
405 "class ",
406 "const ",
407 "let ",
408 "type ",
409 "interface ",
410 "enum ",
411 ] {
412 if let Some(rest) = without_default.strip_prefix(keyword) {
413 let name = rest
414 .split(|c: char| !c.is_alphanumeric() && c != '_')
415 .next()?;
416 if !name.is_empty() {
417 return Some(name.to_string());
418 }
419 }
420 }
421
422 None
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn c_include_relative_is_extracted() {
431 let src = r#"#include "foo/bar.h"
432#include <stdio.h>
433"#;
434 let deps = extract_deps(src, "c");
435 assert!(deps.imports.contains(&"foo/bar".to_string()));
436 assert!(
437 !deps.imports.iter().any(|i| i.contains("stdio")),
438 "system includes should not be treated as internal deps"
439 );
440 }
441
442 #[test]
443 fn ruby_require_relative_is_extracted() {
444 let src = r#"require_relative "./lib/utils"
445require "json"
446"#;
447 let deps = extract_deps(src, "rb");
448 assert!(deps.imports.contains(&"lib/utils".to_string()));
449 assert!(
450 !deps.imports.iter().any(|i| i == "json"),
451 "external requires should not be treated as internal deps"
452 );
453 }
454
455 #[test]
456 fn php_require_is_extracted() {
457 let src = r#"<?php
458require_once "./vendor/autoload.php";
459include "http://example.com/a.php";
460"#;
461 let deps = extract_deps(src, "php");
462 assert!(deps.imports.contains(&"vendor/autoload".to_string()));
463 assert!(
464 deps.imports.iter().all(|i| !i.starts_with("http")),
465 "remote includes should not be treated as internal deps"
466 );
467 }
468
469 #[test]
470 fn bash_source_is_extracted() {
471 let src = r#"#!/usr/bin/env bash
472source "./scripts/env.sh"
473. ../common.sh
474"#;
475 let deps = extract_deps(src, "sh");
476 assert!(deps.imports.contains(&"scripts/env".to_string()));
477 assert!(deps.imports.contains(&"../common".to_string()));
478 }
479
480 #[test]
481 fn dart_import_relative_is_extracted() {
482 let src = r#"import "./src/util.dart";
483import "package:foo/bar.dart";
484"#;
485 let deps = extract_deps(src, "dart");
486 assert!(deps.imports.contains(&"src/util".to_string()));
487 assert!(
488 deps.imports.iter().all(|i| !i.starts_with("package:")),
489 "package imports should not be treated as internal deps"
490 );
491 }
492
493 #[test]
494 fn zig_import_is_extracted() {
495 let src = r#"const m = @import("lib/math.zig");
496const std = @import("std");
497"#;
498 let deps = extract_deps(src, "zig");
499 assert!(deps.imports.contains(&"lib/math".to_string()));
500 assert!(!deps.imports.iter().any(|i| i == "std"), "std is external");
501 }
502
503 #[test]
504 fn kotlin_deps_are_extracted_from_ast() {
505 let content = r#"
506package com.example.app
507
508import com.example.services.UserService
509import com.example.shared.*
510
511class Feature
512fun build(): Feature = Feature()
513"#;
514 let deps = extract_deps(content, "kt");
515 assert!(deps
516 .imports
517 .contains(&"com.example.services.UserService".to_string()));
518 assert!(deps.imports.contains(&"com.example.shared.*".to_string()));
519 assert!(deps.exports.contains(&"Feature".to_string()));
520 assert!(deps.exports.contains(&"build".to_string()));
521 }
522}