1use anyhow::{Context, Result};
17use streaming_iterator::StreamingIterator;
18use tree_sitter::{Parser, Query, QueryCursor};
19use crate::models::{Language, SearchResult, Span, SymbolKind};
20
21pub fn parse(path: &str, source: &str, language: Language) -> Result<Vec<SearchResult>> {
23 let mut parser = Parser::new();
24
25 let ts_language_fn = match language {
28 Language::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
29 Language::JavaScript => tree_sitter_typescript::LANGUAGE_TSX, _ => return Err(anyhow::anyhow!("Unsupported language: {:?}", language)),
31 };
32
33 let ts_language: tree_sitter::Language = ts_language_fn.into();
35
36 parser
37 .set_language(&ts_language)
38 .context("Failed to set TypeScript/JavaScript language")?;
39
40 let tree = parser
41 .parse(source, None)
42 .context("Failed to parse TypeScript/JavaScript source")?;
43
44 let root_node = tree.root_node();
45
46 let mut symbols = Vec::new();
47
48 symbols.extend(extract_functions(source, &root_node, &ts_language)?);
50 symbols.extend(extract_arrow_functions(source, &root_node, &ts_language)?);
51 symbols.extend(extract_classes(source, &root_node, &ts_language)?);
52 symbols.extend(extract_interfaces(source, &root_node, &ts_language)?);
53 symbols.extend(extract_type_aliases(source, &root_node, &ts_language)?);
54 symbols.extend(extract_enums(source, &root_node, &ts_language)?);
55 symbols.extend(extract_variables(source, &root_node, &ts_language)?);
56 symbols.extend(extract_methods(source, &root_node, &ts_language)?);
57
58 for symbol in &mut symbols {
60 symbol.path = path.to_string();
61 symbol.lang = language.clone();
62 }
63
64 Ok(symbols)
65}
66
67fn extract_functions(
69 source: &str,
70 root: &tree_sitter::Node,
71 language: &tree_sitter::Language,
72) -> Result<Vec<SearchResult>> {
73 let query_str = r#"
74 (function_declaration
75 name: (identifier) @name) @function
76
77 (generator_function_declaration
78 name: (identifier) @name) @function
79 "#;
80
81 let query = Query::new(language, query_str)
82 .context("Failed to create function query")?;
83
84 extract_symbols(source, root, &query, SymbolKind::Function, None)
85}
86
87fn extract_arrow_functions(
89 source: &str,
90 root: &tree_sitter::Node,
91 language: &tree_sitter::Language,
92) -> Result<Vec<SearchResult>> {
93 let query_str = r#"
94 (lexical_declaration
95 (variable_declarator
96 name: (identifier) @name
97 value: (arrow_function))) @arrow_fn
98
99 (variable_declaration
100 (variable_declarator
101 name: (identifier) @name
102 value: (arrow_function))) @arrow_fn
103 "#;
104
105 let query = Query::new(language, query_str)
106 .context("Failed to create arrow function query")?;
107
108 extract_symbols(source, root, &query, SymbolKind::Function, None)
109}
110
111fn extract_classes(
113 source: &str,
114 root: &tree_sitter::Node,
115 language: &tree_sitter::Language,
116) -> Result<Vec<SearchResult>> {
117 let query_str = r#"
118 (class_declaration
119 name: (type_identifier) @name) @class
120
121 (abstract_class_declaration
122 name: (type_identifier) @name) @class
123 "#;
124
125 let query = Query::new(language, query_str)
126 .context("Failed to create class query")?;
127
128 extract_symbols(source, root, &query, SymbolKind::Class, None)
129}
130
131fn extract_interfaces(
133 source: &str,
134 root: &tree_sitter::Node,
135 language: &tree_sitter::Language,
136) -> Result<Vec<SearchResult>> {
137 let query_str = r#"
138 (interface_declaration
139 name: (type_identifier) @name) @interface
140 "#;
141
142 let query = Query::new(language, query_str)
143 .context("Failed to create interface query")?;
144
145 extract_symbols(source, root, &query, SymbolKind::Interface, None)
146}
147
148fn extract_type_aliases(
150 source: &str,
151 root: &tree_sitter::Node,
152 language: &tree_sitter::Language,
153) -> Result<Vec<SearchResult>> {
154 let query_str = r#"
155 (type_alias_declaration
156 name: (type_identifier) @name) @type
157 "#;
158
159 let query = Query::new(language, query_str)
160 .context("Failed to create type alias query")?;
161
162 extract_symbols(source, root, &query, SymbolKind::Type, None)
163}
164
165fn extract_enums(
167 source: &str,
168 root: &tree_sitter::Node,
169 language: &tree_sitter::Language,
170) -> Result<Vec<SearchResult>> {
171 let query_str = r#"
172 (enum_declaration
173 name: (identifier) @name) @enum
174 "#;
175
176 let query = Query::new(language, query_str)
177 .context("Failed to create enum query")?;
178
179 extract_symbols(source, root, &query, SymbolKind::Enum, None)
180}
181
182fn extract_variables(
184 source: &str,
185 root: &tree_sitter::Node,
186 language: &tree_sitter::Language,
187) -> Result<Vec<SearchResult>> {
188 let query_str = r#"
191 (lexical_declaration
192 (variable_declarator
193 name: (identifier) @name)) @decl
194
195 (variable_declaration
196 (variable_declarator
197 name: (identifier) @name)) @decl
198 "#;
199
200 let query = Query::new(language, query_str)
201 .context("Failed to create variable query")?;
202
203 let mut cursor = QueryCursor::new();
204 let mut matches = cursor.matches(&query, *root, source.as_bytes());
205
206 let mut symbols = Vec::new();
207
208 while let Some(match_) = matches.next() {
209 let mut name = None;
210 let mut declarator_node = None;
211
212 for capture in match_.captures {
213 let capture_name: &str = &query.capture_names()[capture.index as usize];
214 if capture_name == "name" {
215 name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
216 if let Some(parent) = capture.node.parent() {
218 if parent.kind() == "variable_declarator" {
219 declarator_node = Some(parent);
220 }
221 }
222 }
223 }
224
225 if let (Some(name), Some(declarator)) = (name, declarator_node) {
226 let mut is_arrow_function = false;
228 for i in 0..declarator.child_count() {
229 if let Some(child) = declarator.child(i) {
230 if child.kind() == "arrow_function" {
231 is_arrow_function = true;
232 break;
233 }
234 }
235 }
236
237 if !is_arrow_function {
239 if let Some(decl_node) = declarator.parent() {
240 let span = node_to_span(&decl_node);
241 let preview = extract_preview(source, &span);
242
243 let decl_text = decl_node.utf8_text(source.as_bytes()).unwrap_or("");
245 let kind = if decl_text.trim_start().starts_with("const") {
246 SymbolKind::Constant
247 } else {
248 SymbolKind::Variable
249 };
250
251 symbols.push(SearchResult::new(
252 String::new(),
253 Language::TypeScript,
254 kind,
255 Some(name),
256 span,
257 None,
258 preview,
259 ));
260 }
261 }
262 }
263 }
264
265 Ok(symbols)
266}
267
268fn extract_methods(
270 source: &str,
271 root: &tree_sitter::Node,
272 language: &tree_sitter::Language,
273) -> Result<Vec<SearchResult>> {
274 let query_str = r#"
275 (class_declaration
276 name: (type_identifier) @class_name
277 body: (class_body
278 (method_definition
279 name: (_) @method_name))) @class
280
281 (abstract_class_declaration
282 name: (type_identifier) @class_name
283 body: (class_body
284 (method_definition
285 name: (_) @method_name))) @class
286 "#;
287
288 let query = Query::new(language, query_str)
289 .context("Failed to create method query")?;
290
291 let mut cursor = QueryCursor::new();
292 let mut matches = cursor.matches(&query, *root, source.as_bytes());
293
294 let mut symbols = Vec::new();
295
296 while let Some(match_) = matches.next() {
297 let mut class_name = None;
298 let mut method_name = None;
299 let mut method_node = None;
300
301 for capture in match_.captures {
302 let capture_name: &str = &query.capture_names()[capture.index as usize];
303 match capture_name {
304 "class_name" => {
305 class_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
306 }
307 "method_name" => {
308 method_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
309 let mut current = capture.node;
311 while let Some(parent) = current.parent() {
312 if parent.kind() == "method_definition" {
313 method_node = Some(parent);
314 break;
315 }
316 current = parent;
317 }
318 }
319 _ => {}
320 }
321 }
322
323 if let (Some(class_name), Some(method_name), Some(node)) = (class_name, method_name, method_node) {
324 let scope = format!("class {}", class_name);
325 let span = node_to_span(&node);
326 let preview = extract_preview(source, &span);
327
328 symbols.push(SearchResult::new(
329 String::new(),
330 Language::TypeScript,
331 SymbolKind::Method,
332 Some(method_name),
333 span,
334 Some(scope),
335 preview,
336 ));
337 }
338 }
339
340 Ok(symbols)
341}
342
343fn extract_symbols(
345 source: &str,
346 root: &tree_sitter::Node,
347 query: &Query,
348 kind: SymbolKind,
349 scope: Option<String>,
350) -> Result<Vec<SearchResult>> {
351 let mut cursor = QueryCursor::new();
352 let mut matches = cursor.matches(query, *root, source.as_bytes());
353
354 let mut symbols = Vec::new();
355
356 while let Some(match_) = matches.next() {
357 let mut name = None;
359 let mut full_node = None;
360
361 for capture in match_.captures {
362 let capture_name: &str = &query.capture_names()[capture.index as usize];
363 if capture_name == "name" {
364 name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
365 } else {
366 full_node = Some(capture.node);
368 }
369 }
370
371 if let (Some(name), Some(node)) = (name, full_node) {
372 let span = node_to_span(&node);
373 let preview = extract_preview(source, &span);
374
375 symbols.push(SearchResult::new(
376 String::new(),
377 Language::TypeScript,
378 kind.clone(),
379 Some(name),
380 span,
381 scope.clone(),
382 preview,
383 ));
384 }
385 }
386
387 Ok(symbols)
388}
389
390fn node_to_span(node: &tree_sitter::Node) -> Span {
392 let start = node.start_position();
393 let end = node.end_position();
394
395 Span::new(
396 start.row + 1, start.column,
398 end.row + 1,
399 end.column,
400 )
401}
402
403fn extract_preview(source: &str, span: &Span) -> String {
405 let lines: Vec<&str> = source.lines().collect();
406
407 let start_idx = (span.start_line - 1) as usize; let end_idx = (start_idx + 7).min(lines.len());
410
411 lines[start_idx..end_idx].join("\n")
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn test_parse_function() {
420 let source = r#"
421 function greet(name: string): string {
422 return `Hello, ${name}!`;
423 }
424 "#;
425
426 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
427 assert_eq!(symbols.len(), 1);
428 assert_eq!(symbols[0].symbol.as_deref(), Some("greet"));
429 assert!(matches!(symbols[0].kind, SymbolKind::Function));
430 }
431
432 #[test]
433 fn test_parse_arrow_function() {
434 let source = r#"
435 const add = (a: number, b: number): number => {
436 return a + b;
437 };
438 "#;
439
440 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
441 assert_eq!(symbols.len(), 1);
442 assert_eq!(symbols[0].symbol.as_deref(), Some("add"));
443 assert!(matches!(symbols[0].kind, SymbolKind::Function));
444 }
445
446 #[test]
447 fn test_parse_async_function() {
448 let source = r#"
449 async function fetchData(url: string): Promise<Response> {
450 return await fetch(url);
451 }
452 "#;
453
454 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
455 assert_eq!(symbols.len(), 1);
456 assert_eq!(symbols[0].symbol.as_deref(), Some("fetchData"));
457 assert!(matches!(symbols[0].kind, SymbolKind::Function));
458 }
459
460 #[test]
461 fn test_parse_class() {
462 let source = r#"
463 class User {
464 name: string;
465 age: number;
466
467 constructor(name: string, age: number) {
468 this.name = name;
469 this.age = age;
470 }
471 }
472 "#;
473
474 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
475
476 let class_symbols: Vec<_> = symbols.iter()
478 .filter(|s| matches!(s.kind, SymbolKind::Class))
479 .collect();
480
481 assert_eq!(class_symbols.len(), 1);
482 assert_eq!(class_symbols[0].symbol.as_deref(), Some("User"));
483 }
484
485 #[test]
486 fn test_parse_class_with_methods() {
487 let source = r#"
488 class Calculator {
489 add(a: number, b: number): number {
490 return a + b;
491 }
492
493 subtract(a: number, b: number): number {
494 return a - b;
495 }
496 }
497 "#;
498
499 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
500
501 assert!(symbols.len() >= 3);
503
504 let method_symbols: Vec<_> = symbols.iter()
505 .filter(|s| matches!(s.kind, SymbolKind::Method))
506 .collect();
507
508 assert_eq!(method_symbols.len(), 2);
509 assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("add")));
510 assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("subtract")));
511
512 for method in method_symbols {
514 }
516 }
517
518 #[test]
519 fn test_parse_interface() {
520 let source = r#"
521 interface User {
522 name: string;
523 age: number;
524 email?: string;
525 }
526 "#;
527
528 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
529 assert_eq!(symbols.len(), 1);
530 assert_eq!(symbols[0].symbol.as_deref(), Some("User"));
531 assert!(matches!(symbols[0].kind, SymbolKind::Interface));
532 }
533
534 #[test]
535 fn test_parse_type_alias() {
536 let source = r#"
537 type UserId = string | number;
538 type UserRole = 'admin' | 'user' | 'guest';
539 "#;
540
541 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
542 assert_eq!(symbols.len(), 2);
543
544 let type_symbols: Vec<_> = symbols.iter()
545 .filter(|s| matches!(s.kind, SymbolKind::Type))
546 .collect();
547
548 assert_eq!(type_symbols.len(), 2);
549 assert!(type_symbols.iter().any(|s| s.symbol.as_deref() == Some("UserId")));
550 assert!(type_symbols.iter().any(|s| s.symbol.as_deref() == Some("UserRole")));
551 }
552
553 #[test]
554 fn test_parse_enum() {
555 let source = r#"
556 enum Status {
557 Active,
558 Inactive,
559 Pending
560 }
561 "#;
562
563 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
564 assert_eq!(symbols.len(), 1);
565 assert_eq!(symbols[0].symbol.as_deref(), Some("Status"));
566 assert!(matches!(symbols[0].kind, SymbolKind::Enum));
567 }
568
569 #[test]
570 fn test_parse_const() {
571 let source = r#"
572 const MAX_SIZE = 100;
573 const DEFAULT_USER = {
574 name: "Anonymous",
575 age: 0
576 };
577 "#;
578
579 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
580 assert_eq!(symbols.len(), 2);
581
582 let const_symbols: Vec<_> = symbols.iter()
583 .filter(|s| matches!(s.kind, SymbolKind::Constant))
584 .collect();
585
586 assert_eq!(const_symbols.len(), 2);
587 assert!(const_symbols.iter().any(|s| s.symbol.as_deref() == Some("MAX_SIZE")));
588 assert!(const_symbols.iter().any(|s| s.symbol.as_deref() == Some("DEFAULT_USER")));
589 }
590
591 #[test]
592 fn test_parse_react_component() {
593 let source = r#"
594 import React, { useState } from 'react';
595
596 interface ButtonProps {
597 label: string;
598 onClick: () => void;
599 }
600
601 const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
602 return (
603 <button onClick={onClick}>
604 {label}
605 </button>
606 );
607 };
608
609 function useCounter(initial: number) {
610 const [count, setCount] = React.useState(initial);
611 return { count, setCount };
612 }
613
614 export default Button;
615 "#;
616
617 let symbols = parse("Button.tsx", source, Language::TypeScript).unwrap();
618
619 assert!(symbols.iter().any(|s| s.symbol.as_deref() == Some("ButtonProps") && matches!(s.kind, SymbolKind::Interface)));
621 assert!(symbols.iter().any(|s| s.symbol.as_deref() == Some("Button") && matches!(s.kind, SymbolKind::Function)));
622 assert!(symbols.iter().any(|s| s.symbol.as_deref() == Some("useCounter") && matches!(s.kind, SymbolKind::Function)));
623 }
624
625 #[test]
626 fn test_parse_mixed_symbols() {
627 let source = r#"
628 interface Config {
629 debug: boolean;
630 }
631
632 type ConfigKey = keyof Config;
633
634 const DEFAULT_CONFIG: Config = {
635 debug: false
636 };
637
638 class ConfigManager {
639 private config: Config;
640
641 constructor(config: Config) {
642 this.config = config;
643 }
644
645 getConfig(): Config {
646 return this.config;
647 }
648 }
649
650 function loadConfig(): Config {
651 return DEFAULT_CONFIG;
652 }
653 "#;
654
655 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
656
657 assert!(symbols.len() >= 6);
659
660 let kinds: Vec<&SymbolKind> = symbols.iter().map(|s| &s.kind).collect();
661 assert!(kinds.contains(&&SymbolKind::Interface));
662 assert!(kinds.contains(&&SymbolKind::Type));
663 assert!(kinds.contains(&&SymbolKind::Constant));
664 assert!(kinds.contains(&&SymbolKind::Class));
665 assert!(kinds.contains(&&SymbolKind::Method));
666 assert!(kinds.contains(&&SymbolKind::Function));
667 }
668
669 #[test]
670 fn test_parse_async_class_methods() {
671 let source = r#"
672 export class CentralUsersModule {
673 async getAllUsers(params) {
674 return await this.call('get', `/users`, params)
675 }
676
677 async getUser(userId) {
678 return await this.call('get', `/users/${userId}`)
679 }
680
681 deleteUser(userId) {
682 return this.call('delete', `/user/${userId}`)
683 }
684 }
685 "#;
686
687 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
688
689 println!("\nAll symbols found:");
691 for symbol in &symbols {
692 println!(" {:?} - {}", symbol.kind, symbol.symbol.as_deref().unwrap_or(""));
693 }
694
695 let class_symbols: Vec<_> = symbols.iter()
697 .filter(|s| matches!(s.kind, SymbolKind::Class))
698 .collect();
699 assert_eq!(class_symbols.len(), 1);
700 assert_eq!(class_symbols[0].symbol.as_deref(), Some("CentralUsersModule"));
701
702 let method_symbols: Vec<_> = symbols.iter()
703 .filter(|s| matches!(s.kind, SymbolKind::Method))
704 .collect();
705
706 assert_eq!(method_symbols.len(), 3, "Expected 3 methods, found {}", method_symbols.len());
708 assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("getAllUsers")));
709 assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("getUser")));
710 assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("deleteUser")));
711
712 let variable_symbols: Vec<_> = symbols.iter()
714 .filter(|s| matches!(s.kind, SymbolKind::Constant) || matches!(s.kind, SymbolKind::Variable))
715 .collect();
716 assert_eq!(variable_symbols.len(), 0, "Async methods should not be classified as variables");
717
718 for method in method_symbols {
720 }
722 }
723
724 #[test]
725 fn test_parse_user_exact_code() {
726 let source = r#"
728export class CentralUsersModule extends HttpFactory<WatchHookMap, WatchEvents> {
729 protected $events = {
730 //
731 }
732
733 async checkAuthenticated() {
734 return await this.call('get', '/check')
735 }
736
737 async getUser(userId: CentralUser['id']) {
738 return await this.call<CentralUser>('get', `/users/${userId}`)
739 }
740
741 async getAllUsers(params?: PaginatedParams & SortableParams & SearchableParams) {
742 return await this.call<CentralUser[]>('get', `/users`, params)
743 }
744
745 async deleteUser(userId: CentralUser['id']) {
746 return await this.call<void>('delete', `/user/${userId}`)
747 }
748}
749 "#;
750
751 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
752
753 println!("\nAll symbols found in user code:");
755 for symbol in &symbols {
756 println!(" {:?} - {}", symbol.kind, symbol.symbol.as_deref().unwrap_or(""));
757 }
758
759 let get_all_users_symbols: Vec<_> = symbols.iter()
761 .filter(|s| s.symbol.as_deref() == Some("getAllUsers"))
762 .collect();
763
764 assert_eq!(get_all_users_symbols.len(), 1, "Should find exactly one getAllUsers");
765 assert!(
766 matches!(get_all_users_symbols[0].kind, SymbolKind::Method),
767 "getAllUsers should be a Method, not {:?}",
768 get_all_users_symbols[0].kind
769 );
770 }
771
772 #[test]
773 fn test_local_variables_included() {
774 let source = r#"
775 const GLOBAL_CONSTANT = 100;
776 let globalLet = 50;
777 var globalVar = 25;
778
779 function calculate(x: number): number {
780 const localConst = x * 2;
781 let localLet = 5;
782 var localVar = 10;
783 return localConst + localLet + localVar;
784 }
785 "#;
786
787 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
788
789 let var_symbols: Vec<_> = symbols.iter()
790 .filter(|s| matches!(s.kind, SymbolKind::Variable) || matches!(s.kind, SymbolKind::Constant))
791 .collect();
792
793 assert_eq!(var_symbols.len(), 6);
795
796 assert!(var_symbols.iter().any(|s| s.symbol.as_deref() == Some("GLOBAL_CONSTANT")));
798 assert!(var_symbols.iter().any(|s| s.symbol.as_deref() == Some("globalLet")));
799 assert!(var_symbols.iter().any(|s| s.symbol.as_deref() == Some("globalVar")));
800
801 assert!(var_symbols.iter().any(|s| s.symbol.as_deref() == Some("localConst")));
803 assert!(var_symbols.iter().any(|s| s.symbol.as_deref() == Some("localLet")));
804 assert!(var_symbols.iter().any(|s| s.symbol.as_deref() == Some("localVar")));
805
806 let global_const = var_symbols.iter().find(|s| s.symbol.as_deref() == Some("GLOBAL_CONSTANT")).unwrap();
808 assert!(matches!(global_const.kind, SymbolKind::Constant));
809
810 let global_let = var_symbols.iter().find(|s| s.symbol.as_deref() == Some("globalLet")).unwrap();
811 assert!(matches!(global_let.kind, SymbolKind::Variable));
812 }
813}
814
815use crate::models::ImportType;
820use crate::parsers::{DependencyExtractor, ImportInfo};
821
822pub struct TypeScriptDependencyExtractor;
824
825impl DependencyExtractor for TypeScriptDependencyExtractor {
826 fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>> {
827 Self::extract_dependencies_with_alias_map(source, None)
829 }
830}
831
832impl TypeScriptDependencyExtractor {
833 pub fn extract_dependencies_with_alias_map(
838 source: &str,
839 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
840 ) -> Result<Vec<ImportInfo>> {
841 let mut parser = Parser::new();
842 let language = tree_sitter_typescript::LANGUAGE_TSX; parser
845 .set_language(&language.into())
846 .context("Failed to set TypeScript/JavaScript language")?;
847
848 let tree = parser
849 .parse(source, None)
850 .context("Failed to parse TypeScript/JavaScript source")?;
851
852 let root_node = tree.root_node();
853
854 let mut imports = Vec::new();
855
856 imports.extend(extract_import_declarations(source, &root_node, alias_map)?);
858
859 imports.extend(extract_require_statements(source, &root_node, alias_map)?);
861
862 Ok(imports)
863 }
864}
865
866fn extract_import_declarations(
868 source: &str,
869 root: &tree_sitter::Node,
870 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
871) -> Result<Vec<ImportInfo>> {
872 let language = tree_sitter_typescript::LANGUAGE_TSX;
873
874 let query_str = r#"
875 (import_statement
876 source: (string) @import_path) @import
877 "#;
878
879 let query = Query::new(&language.into(), query_str)
880 .context("Failed to create import declaration query")?;
881
882 let mut cursor = QueryCursor::new();
883 let mut matches = cursor.matches(&query, *root, source.as_bytes());
884
885 let mut imports = Vec::new();
886
887 while let Some(match_) = matches.next() {
888 let mut import_path = None;
889 let mut import_node = None;
890
891 for capture in match_.captures {
892 let capture_name: &str = &query.capture_names()[capture.index as usize];
893 match capture_name {
894 "import_path" => {
895 let raw_path = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
897 import_path = Some(raw_path.trim_matches(|c| c == '"' || c == '\'' || c == '`').to_string());
898 }
899 "import" => {
900 import_node = Some(capture.node);
901 }
902 _ => {}
903 }
904 }
905
906 if let (Some(path), Some(node)) = (import_path, import_node) {
907 let import_type = classify_js_import(&path, alias_map);
908 let line_number = node.start_position().row + 1;
909
910 let imported_symbols = extract_imported_symbols_js(source, &node);
912
913 imports.push(ImportInfo {
914 imported_path: path,
915 import_type,
916 line_number,
917 imported_symbols,
918 });
919 }
920 }
921
922 Ok(imports)
923}
924
925fn extract_require_statements(
927 source: &str,
928 root: &tree_sitter::Node,
929 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
930) -> Result<Vec<ImportInfo>> {
931 let language = tree_sitter_typescript::LANGUAGE_TSX;
932
933 let query_str = r#"
934 (call_expression
935 function: (identifier) @func_name
936 arguments: (arguments (string) @require_path)) @require_call
937 "#;
938
939 let query = Query::new(&language.into(), query_str)
940 .context("Failed to create require query")?;
941
942 let mut cursor = QueryCursor::new();
943 let mut matches = cursor.matches(&query, *root, source.as_bytes());
944
945 let mut imports = Vec::new();
946
947 while let Some(match_) = matches.next() {
948 let mut func_name = None;
949 let mut require_path = None;
950 let mut require_node = None;
951
952 for capture in match_.captures {
953 let capture_name: &str = &query.capture_names()[capture.index as usize];
954 match capture_name {
955 "func_name" => {
956 func_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or(""));
957 }
958 "require_path" => {
959 let raw_path = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
961 require_path = Some(raw_path.trim_matches(|c| c == '"' || c == '\'' || c == '`').to_string());
962 }
963 "require_call" => {
964 require_node = Some(capture.node);
965 }
966 _ => {}
967 }
968 }
969
970 if func_name == Some("require") {
972 if let (Some(path), Some(node)) = (require_path, require_node) {
973 let import_type = classify_js_import(&path, alias_map);
974 let line_number = node.start_position().row + 1;
975
976 imports.push(ImportInfo {
977 imported_path: path,
978 import_type,
979 line_number,
980 imported_symbols: None, });
982 }
983 }
984 }
985
986 Ok(imports)
987}
988
989fn extract_imported_symbols_js(source: &str, import_node: &tree_sitter::Node) -> Option<Vec<String>> {
991 let mut symbols = Vec::new();
992
993 let mut cursor = import_node.walk();
995 for child in import_node.children(&mut cursor) {
996 if child.kind() == "import_clause" {
997 let mut clause_cursor = child.walk();
999 for grandchild in child.children(&mut clause_cursor) {
1000 match grandchild.kind() {
1001 "named_imports" => {
1002 let mut specifier_cursor = grandchild.walk();
1004 for specifier in grandchild.children(&mut specifier_cursor) {
1005 if specifier.kind() == "import_specifier" {
1006 if let Ok(text) = specifier.utf8_text(source.as_bytes()) {
1008 let name = text.split_whitespace().next().unwrap_or(text);
1010 symbols.push(name.to_string());
1011 }
1012 }
1013 }
1014 }
1015 "identifier" => {
1016 if let Ok(text) = grandchild.utf8_text(source.as_bytes()) {
1018 symbols.push(text.to_string());
1019 }
1020 }
1021 _ => {}
1022 }
1023 }
1024 }
1025 }
1026
1027 if symbols.is_empty() {
1028 None
1029 } else {
1030 Some(symbols)
1031 }
1032}
1033
1034fn classify_js_import(import_path: &str, alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>) -> ImportType {
1044 if import_path.starts_with("./") || import_path.starts_with("../") {
1046 log::trace!("classify_js_import: '{}' => Internal (relative)", import_path);
1047 return ImportType::Internal;
1048 }
1049
1050 if import_path.starts_with("/") {
1052 log::trace!("classify_js_import: '{}' => Internal (absolute)", import_path);
1053 return ImportType::Internal;
1054 }
1055
1056 if let Some(map) = alias_map {
1058 log::trace!("classify_js_import: checking '{}' against {} aliases", import_path, map.aliases.len());
1059 for alias_pattern in map.aliases.keys() {
1060 if alias_pattern.ends_with("/*") {
1062 let alias_prefix = alias_pattern.trim_end_matches("/*");
1063 if import_path.starts_with(alias_prefix) {
1064 log::info!("classify_js_import: '{}' => Internal (matches alias pattern '{}')", import_path, alias_pattern);
1065 return ImportType::Internal;
1066 }
1067 } else {
1068 if import_path == alias_pattern {
1070 log::info!("classify_js_import: '{}' => Internal (exact match alias '{}')", import_path, alias_pattern);
1071 return ImportType::Internal;
1072 }
1073 }
1074 }
1075 log::trace!("classify_js_import: '{}' did not match any of {} alias patterns", import_path, map.aliases.len());
1076 } else {
1077 log::trace!("classify_js_import: no alias map provided for '{}'", import_path);
1078 }
1079
1080 const STDLIB_MODULES: &[&str] = &[
1082 "fs", "path", "os", "crypto", "util", "events", "stream", "buffer",
1083 "http", "https", "net", "tls", "url", "querystring", "dns",
1084 "child_process", "cluster", "worker_threads", "readline",
1085 "zlib", "assert", "console", "module", "process", "timers",
1086 "vm", "string_decoder", "dgram", "v8", "perf_hooks",
1087 "node:fs", "node:path", "node:os", "node:crypto", "node:util", "node:events",
1089 "node:stream", "node:buffer", "node:http", "node:https", "node:net",
1090 ];
1091
1092 if STDLIB_MODULES.contains(&import_path) {
1094 log::trace!("classify_js_import: '{}' => Stdlib", import_path);
1095 return ImportType::Stdlib;
1096 }
1097
1098 log::info!("classify_js_import: '{}' => External (not alias, relative, absolute, or stdlib)", import_path);
1100 ImportType::External
1101}
1102
1103use crate::parsers::ExportInfo;
1108
1109impl TypeScriptDependencyExtractor {
1110 pub fn extract_export_declarations(
1120 source: &str,
1121 _alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
1122 ) -> Result<Vec<ExportInfo>> {
1123 let mut parser = Parser::new();
1124 let language = tree_sitter_typescript::LANGUAGE_TSX;
1125
1126 parser
1127 .set_language(&language.into())
1128 .context("Failed to set TypeScript/JavaScript language")?;
1129
1130 let tree = parser
1131 .parse(source, None)
1132 .context("Failed to parse TypeScript/JavaScript source for export extraction")?;
1133
1134 let root_node = tree.root_node();
1135
1136 let mut exports = Vec::new();
1137
1138 exports.extend(extract_export_from_statements(source, &root_node)?);
1140
1141 Ok(exports)
1142 }
1143}
1144
1145fn extract_export_from_statements(
1153 source: &str,
1154 root: &tree_sitter::Node,
1155) -> Result<Vec<ExportInfo>> {
1156 let language = tree_sitter_typescript::LANGUAGE_TSX;
1157
1158 let query_str = r#"
1160 (export_statement
1161 source: (string) @source_path) @export
1162 "#;
1163
1164 let query = Query::new(&language.into(), query_str)
1165 .context("Failed to create export statement query")?;
1166
1167 let mut cursor = QueryCursor::new();
1168 let mut matches = cursor.matches(&query, *root, source.as_bytes());
1169
1170 let mut exports = Vec::new();
1171
1172 while let Some(match_) = matches.next() {
1173 let mut source_path = None;
1174 let mut export_node = None;
1175
1176 for capture in match_.captures {
1177 let capture_name: &str = &query.capture_names()[capture.index as usize];
1178 match capture_name {
1179 "source_path" => {
1180 let raw_path = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
1182 source_path = Some(raw_path.trim_matches(|c| c == '"' || c == '\'' || c == '`').to_string());
1183 }
1184 "export" => {
1185 export_node = Some(capture.node);
1186 }
1187 _ => {}
1188 }
1189 }
1190
1191 if let (Some(path), Some(node)) = (source_path, export_node) {
1192 let line_number = node.start_position().row + 1;
1193
1194 let exported_symbols = extract_exported_symbols(source, &node)?;
1196
1197 if exported_symbols.is_empty() {
1199 exports.push(ExportInfo {
1200 exported_symbol: None, source_path: path,
1202 line_number,
1203 });
1204 } else {
1205 for symbol in exported_symbols {
1207 exports.push(ExportInfo {
1208 exported_symbol: Some(symbol),
1209 source_path: path.clone(),
1210 line_number,
1211 });
1212 }
1213 }
1214 }
1215 }
1216
1217 Ok(exports)
1218}
1219
1220fn extract_exported_symbols(source: &str, export_node: &tree_sitter::Node) -> Result<Vec<String>> {
1225 let mut symbols = Vec::new();
1226
1227 let mut cursor = export_node.walk();
1229 for child in export_node.children(&mut cursor) {
1230 if child.kind() == "export_clause" {
1231 let mut specifier_cursor = child.walk();
1233 for specifier in child.children(&mut specifier_cursor) {
1234 if specifier.kind() == "export_specifier" {
1235 if let Ok(text) = specifier.utf8_text(source.as_bytes()) {
1238 let name = text.split_whitespace().next().unwrap_or(text);
1240 symbols.push(name.to_string());
1241 }
1242 }
1243 }
1244 }
1245 }
1246
1247 Ok(symbols)
1248}
1249
1250pub fn resolve_ts_import_to_path(
1265 import_path: &str,
1266 current_file_path: Option<&str>,
1267 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
1268) -> Option<String> {
1269 log::debug!("resolve_ts_import_to_path: import_path={}, current_file={:?}, has_alias_map={}",
1270 import_path, current_file_path, alias_map.is_some());
1271
1272 if let Some(map) = alias_map {
1274 log::debug!(" Trying alias resolution with {} aliases (config_dir: {:?}, base_url: {:?})",
1275 map.aliases.len(), map.config_dir, map.base_url);
1276 if let Some(resolved_alias) = map.resolve_alias(import_path) {
1277 log::debug!(" Alias matched! {} => {}", import_path, resolved_alias);
1278 let resolved_path = map.resolve_relative_to_config(&resolved_alias);
1280 let path_str = resolved_path.to_string_lossy().to_string();
1281 log::debug!(" After resolve_relative_to_config: {}", path_str);
1282
1283 let has_extension = path_str.ends_with(".vue")
1285 || path_str.ends_with(".svelte")
1286 || path_str.ends_with(".ts")
1287 || path_str.ends_with(".tsx")
1288 || path_str.ends_with(".js")
1289 || path_str.ends_with(".jsx")
1290 || path_str.ends_with(".mjs")
1291 || path_str.ends_with(".cjs");
1292
1293 if has_extension {
1294 log::trace!("Resolved alias {} => {}", import_path, path_str);
1295 return Some(path_str);
1296 }
1297
1298 let extensions = vec![
1300 ".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs",
1301 "/index.tsx", "/index.ts", "/index.jsx", "/index.js",
1302 ];
1303
1304 let candidates: Vec<String> = extensions
1305 .iter()
1306 .map(|ext| format!("{}{}", path_str, ext))
1307 .collect();
1308
1309 log::trace!("Resolved alias {} => {} (candidates: {})",
1310 import_path, path_str, candidates.join(" | "));
1311 return Some(candidates.join("|"));
1312 }
1313 }
1314
1315 if !import_path.starts_with("./") && !import_path.starts_with("../") {
1318 return None;
1319 }
1320
1321 let current_file = current_file_path?;
1322
1323 let current_dir = std::path::Path::new(current_file).parent()?;
1325
1326 let resolved = current_dir.join(import_path);
1328
1329 let normalized_path = std::path::Path::new(&resolved)
1332 .components()
1333 .fold(std::path::PathBuf::new(), |mut acc, component| {
1334 match component {
1335 std::path::Component::CurDir => acc, std::path::Component::ParentDir => {
1337 acc.pop(); acc
1339 }
1340 _ => {
1341 acc.push(component);
1342 acc
1343 }
1344 }
1345 });
1346
1347 let normalized = normalized_path.to_string_lossy().to_string();
1348
1349 let has_extension = normalized.ends_with(".vue")
1352 || normalized.ends_with(".svelte")
1353 || normalized.ends_with(".ts")
1354 || normalized.ends_with(".tsx")
1355 || normalized.ends_with(".js")
1356 || normalized.ends_with(".jsx")
1357 || normalized.ends_with(".mjs")
1358 || normalized.ends_with(".cjs");
1359
1360 if has_extension {
1361 log::trace!("TS/JS import with extension: {}", normalized);
1363 return Some(normalized);
1364 }
1365
1366 let extensions = vec![
1374 ".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs",
1375 "/index.tsx", "/index.ts", "/index.jsx", "/index.js",
1376 ];
1377
1378 let candidates: Vec<String> = extensions
1379 .iter()
1380 .map(|ext| format!("{}{}", normalized, ext))
1381 .collect();
1382
1383 log::trace!("TS/JS import candidates (no extension): {}", candidates.join(" | "));
1384
1385 Some(candidates.join("|"))
1388}
1389
1390#[cfg(test)]
1391mod path_resolution_tests {
1392 use super::*;
1393
1394 #[test]
1395 fn test_resolve_relative_import_same_directory() {
1396 let result = resolve_ts_import_to_path(
1398 "./Button",
1399 Some("src/components/App.tsx"),
1400 None,
1401 );
1402
1403 assert!(result.is_some());
1404 let candidates = result.unwrap();
1405 assert!(candidates.contains("Button.tsx"));
1407 assert!(candidates.contains("Button.ts"));
1408 assert!(candidates.starts_with("src/components/Button.tsx") || candidates.contains("/Button.tsx|"));
1410 }
1411
1412 #[test]
1413 fn test_resolve_relative_import_parent_directory() {
1414 let result = resolve_ts_import_to_path(
1416 "../utils/helper",
1417 Some("src/components/Button.tsx"),
1418 None,
1419 );
1420
1421 assert!(result.is_some());
1422 let path = result.unwrap();
1423 assert!(path.contains("utils/helper"));
1424 }
1425
1426 #[test]
1427 fn test_resolve_relative_import_multiple_parents() {
1428 let result = resolve_ts_import_to_path(
1430 "../../config/app",
1431 Some("src/components/ui/Button.tsx"),
1432 None,
1433 );
1434
1435 assert!(result.is_some());
1436 let path = result.unwrap();
1437 assert!(path.contains("config/app"));
1438 }
1439
1440 #[test]
1441 fn test_resolve_index_file() {
1442 let result = resolve_ts_import_to_path(
1444 "./components",
1445 Some("src/App.tsx"),
1446 None,
1447 );
1448
1449 assert!(result.is_some());
1450 assert!(result.unwrap().contains("components"));
1453 }
1454
1455 #[test]
1456 fn test_absolute_import_not_supported_without_alias_map() {
1457 let result = resolve_ts_import_to_path(
1459 "@components/Button",
1460 Some("src/App.tsx"),
1461 None,
1462 );
1463
1464 assert!(result.is_none());
1466 }
1467
1468 #[test]
1469 fn test_node_modules_import_not_supported() {
1470 let result = resolve_ts_import_to_path(
1472 "react",
1473 Some("src/App.tsx"),
1474 None,
1475 );
1476
1477 assert!(result.is_none());
1479 }
1480
1481 #[test]
1482 fn test_resolve_without_current_file() {
1483 let result = resolve_ts_import_to_path(
1484 "./Button",
1485 None,
1486 None,
1487 );
1488
1489 assert!(result.is_none());
1491 }
1492
1493 #[test]
1494 fn test_resolve_nested_directory_structure() {
1495 let result = resolve_ts_import_to_path(
1497 "./api/client",
1498 Some("src/services/http.ts"),
1499 None,
1500 );
1501
1502 assert!(result.is_some());
1503 let path = result.unwrap();
1504 assert!(path.contains("api/client"));
1506 }
1507}
1508
1509#[cfg(test)]
1510mod dependency_extraction_tests {
1511 use super::*;
1512
1513 #[test]
1514 fn test_extract_basic_imports() {
1515 let source = r#"
1516 import { Button } from './components/Button';
1517 import React from 'react';
1518 import fs from 'fs';
1519 import '../styles.css';
1520 "#;
1521
1522 let deps = TypeScriptDependencyExtractor::extract_dependencies(source).unwrap();
1523
1524 assert_eq!(deps.len(), 4, "Should extract 4 import statements");
1525 assert!(deps.iter().any(|d| d.imported_path == "./components/Button"));
1526 assert!(deps.iter().any(|d| d.imported_path == "react"));
1527 assert!(deps.iter().any(|d| d.imported_path == "fs"));
1528 assert!(deps.iter().any(|d| d.imported_path == "../styles.css"));
1529 }
1530
1531 #[test]
1532 fn test_dynamic_imports_filtered() {
1533 let source = r#"
1534 import { Button } from './components/Button';
1535 import React from 'react';
1536 const fs = require('fs');
1537
1538 // Dynamic imports - should be filtered out
1539 const moduleName = './dynamic-module';
1540 import(moduleName);
1541 import(`./templates/${template}`);
1542 require(variable);
1543 require(CONFIG_PATH + '/settings.js');
1544 "#;
1545
1546 let deps = TypeScriptDependencyExtractor::extract_dependencies(source).unwrap();
1547
1548 assert_eq!(deps.len(), 3, "Should extract 3 static imports only");
1551
1552 assert!(deps.iter().any(|d| d.imported_path == "./components/Button"));
1553 assert!(deps.iter().any(|d| d.imported_path == "react"));
1554 assert!(deps.iter().any(|d| d.imported_path == "fs"));
1555
1556 assert!(!deps.iter().any(|d| d.imported_path.contains("moduleName")));
1558 assert!(!deps.iter().any(|d| d.imported_path.contains("template")));
1559 assert!(!deps.iter().any(|d| d.imported_path.contains("variable")));
1560 assert!(!deps.iter().any(|d| d.imported_path.contains("CONFIG_PATH")));
1561 }
1562
1563 #[test]
1564 fn test_require_with_template_literals_filtered() {
1565 let source = r#"
1566 const path = require('path');
1567 const utils = require('./utils');
1568
1569 // Dynamic requires with template literals - should be filtered out
1570 const config = require(`./config/${env}.json`);
1571 const plugin = require(`${PLUGIN_DIR}/loader`);
1572 "#;
1573
1574 let deps = TypeScriptDependencyExtractor::extract_dependencies(source).unwrap();
1575
1576 assert_eq!(deps.len(), 2, "Should extract 2 static requires only");
1579
1580 assert!(deps.iter().any(|d| d.imported_path == "path"));
1581 assert!(deps.iter().any(|d| d.imported_path == "./utils"));
1582
1583 assert!(!deps.iter().any(|d| d.imported_path.contains("env")));
1585 assert!(!deps.iter().any(|d| d.imported_path.contains("PLUGIN_DIR")));
1586 }
1587}