1use crate::models::{Language, SearchResult, Span, SymbolKind};
17use anyhow::{Context, Result};
18use streaming_iterator::StreamingIterator;
19use tree_sitter::{Parser, Query, QueryCursor};
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).context("Failed to create function query")?;
82
83 extract_symbols(source, root, &query, SymbolKind::Function, None)
84}
85
86fn extract_arrow_functions(
88 source: &str,
89 root: &tree_sitter::Node,
90 language: &tree_sitter::Language,
91) -> Result<Vec<SearchResult>> {
92 let query_str = r#"
93 (lexical_declaration
94 (variable_declarator
95 name: (identifier) @name
96 value: (arrow_function))) @arrow_fn
97
98 (variable_declaration
99 (variable_declarator
100 name: (identifier) @name
101 value: (arrow_function))) @arrow_fn
102 "#;
103
104 let query = Query::new(language, query_str).context("Failed to create arrow function query")?;
105
106 extract_symbols(source, root, &query, SymbolKind::Function, None)
107}
108
109fn extract_classes(
111 source: &str,
112 root: &tree_sitter::Node,
113 language: &tree_sitter::Language,
114) -> Result<Vec<SearchResult>> {
115 let query_str = r#"
116 (class_declaration
117 name: (type_identifier) @name) @class
118
119 (abstract_class_declaration
120 name: (type_identifier) @name) @class
121 "#;
122
123 let query = Query::new(language, query_str).context("Failed to create class query")?;
124
125 extract_symbols(source, root, &query, SymbolKind::Class, None)
126}
127
128fn extract_interfaces(
130 source: &str,
131 root: &tree_sitter::Node,
132 language: &tree_sitter::Language,
133) -> Result<Vec<SearchResult>> {
134 let query_str = r#"
135 (interface_declaration
136 name: (type_identifier) @name) @interface
137 "#;
138
139 let query = Query::new(language, query_str).context("Failed to create interface query")?;
140
141 extract_symbols(source, root, &query, SymbolKind::Interface, None)
142}
143
144fn extract_type_aliases(
146 source: &str,
147 root: &tree_sitter::Node,
148 language: &tree_sitter::Language,
149) -> Result<Vec<SearchResult>> {
150 let query_str = r#"
151 (type_alias_declaration
152 name: (type_identifier) @name) @type
153 "#;
154
155 let query = Query::new(language, query_str).context("Failed to create type alias query")?;
156
157 extract_symbols(source, root, &query, SymbolKind::Type, None)
158}
159
160fn extract_enums(
162 source: &str,
163 root: &tree_sitter::Node,
164 language: &tree_sitter::Language,
165) -> Result<Vec<SearchResult>> {
166 let query_str = r#"
167 (enum_declaration
168 name: (identifier) @name) @enum
169 "#;
170
171 let query = Query::new(language, query_str).context("Failed to create enum query")?;
172
173 extract_symbols(source, root, &query, SymbolKind::Enum, None)
174}
175
176fn extract_variables(
178 source: &str,
179 root: &tree_sitter::Node,
180 language: &tree_sitter::Language,
181) -> Result<Vec<SearchResult>> {
182 let query_str = r#"
185 (lexical_declaration
186 (variable_declarator
187 name: (identifier) @name)) @decl
188
189 (variable_declaration
190 (variable_declarator
191 name: (identifier) @name)) @decl
192 "#;
193
194 let query = Query::new(language, query_str).context("Failed to create variable query")?;
195
196 let mut cursor = QueryCursor::new();
197 let mut matches = cursor.matches(&query, *root, source.as_bytes());
198
199 let mut symbols = Vec::new();
200
201 while let Some(match_) = matches.next() {
202 let mut name = None;
203 let mut declarator_node = None;
204
205 for capture in match_.captures {
206 let capture_name: &str = &query.capture_names()[capture.index as usize];
207 if capture_name == "name" {
208 name = Some(
209 capture
210 .node
211 .utf8_text(source.as_bytes())
212 .unwrap_or("")
213 .to_string(),
214 );
215 if let Some(parent) = capture.node.parent() {
217 if parent.kind() == "variable_declarator" {
218 declarator_node = Some(parent);
219 }
220 }
221 }
222 }
223
224 if let (Some(name), Some(declarator)) = (name, declarator_node) {
225 let mut is_arrow_function = false;
227 for i in 0..declarator.child_count() {
228 if let Some(child) = declarator.child(i as u32) {
229 if child.kind() == "arrow_function" {
230 is_arrow_function = true;
231 break;
232 }
233 }
234 }
235
236 if !is_arrow_function {
238 if let Some(decl_node) = declarator.parent() {
239 let span = node_to_span(&decl_node);
240 let preview = extract_preview(source, &span);
241
242 let decl_text = decl_node.utf8_text(source.as_bytes()).unwrap_or("");
244 let kind = if decl_text.trim_start().starts_with("const") {
245 SymbolKind::Constant
246 } else {
247 SymbolKind::Variable
248 };
249
250 symbols.push(SearchResult::new(
251 String::new(),
252 Language::TypeScript,
253 kind,
254 Some(name),
255 span,
256 None,
257 preview,
258 ));
259 }
260 }
261 }
262 }
263
264 Ok(symbols)
265}
266
267fn extract_methods(
269 source: &str,
270 root: &tree_sitter::Node,
271 language: &tree_sitter::Language,
272) -> Result<Vec<SearchResult>> {
273 let query_str = r#"
274 (class_declaration
275 name: (type_identifier) @class_name
276 body: (class_body
277 (method_definition
278 name: (_) @method_name))) @class
279
280 (abstract_class_declaration
281 name: (type_identifier) @class_name
282 body: (class_body
283 (method_definition
284 name: (_) @method_name))) @class
285 "#;
286
287 let query = Query::new(language, query_str).context("Failed to create method query")?;
288
289 let mut cursor = QueryCursor::new();
290 let mut matches = cursor.matches(&query, *root, source.as_bytes());
291
292 let mut symbols = Vec::new();
293
294 while let Some(match_) = matches.next() {
295 let mut class_name = None;
296 let mut method_name = None;
297 let mut method_node = None;
298
299 for capture in match_.captures {
300 let capture_name: &str = &query.capture_names()[capture.index as usize];
301 match capture_name {
302 "class_name" => {
303 class_name = Some(
304 capture
305 .node
306 .utf8_text(source.as_bytes())
307 .unwrap_or("")
308 .to_string(),
309 );
310 }
311 "method_name" => {
312 method_name = Some(
313 capture
314 .node
315 .utf8_text(source.as_bytes())
316 .unwrap_or("")
317 .to_string(),
318 );
319 let mut current = capture.node;
321 while let Some(parent) = current.parent() {
322 if parent.kind() == "method_definition" {
323 method_node = Some(parent);
324 break;
325 }
326 current = parent;
327 }
328 }
329 _ => {}
330 }
331 }
332
333 if let (Some(class_name), Some(method_name), Some(node)) =
334 (class_name, method_name, method_node)
335 {
336 let scope = format!("class {}", class_name);
337 let span = node_to_span(&node);
338 let preview = extract_preview(source, &span);
339
340 symbols.push(SearchResult::new(
341 String::new(),
342 Language::TypeScript,
343 SymbolKind::Method,
344 Some(method_name),
345 span,
346 Some(scope),
347 preview,
348 ));
349 }
350 }
351
352 Ok(symbols)
353}
354
355fn extract_symbols(
357 source: &str,
358 root: &tree_sitter::Node,
359 query: &Query,
360 kind: SymbolKind,
361 scope: Option<String>,
362) -> Result<Vec<SearchResult>> {
363 let mut cursor = QueryCursor::new();
364 let mut matches = cursor.matches(query, *root, source.as_bytes());
365
366 let mut symbols = Vec::new();
367
368 while let Some(match_) = matches.next() {
369 let mut name = None;
371 let mut full_node = None;
372
373 for capture in match_.captures {
374 let capture_name: &str = &query.capture_names()[capture.index as usize];
375 if capture_name == "name" {
376 name = Some(
377 capture
378 .node
379 .utf8_text(source.as_bytes())
380 .unwrap_or("")
381 .to_string(),
382 );
383 } else {
384 full_node = Some(capture.node);
386 }
387 }
388
389 if let (Some(name), Some(node)) = (name, full_node) {
390 let span = node_to_span(&node);
391 let preview = extract_preview(source, &span);
392
393 symbols.push(SearchResult::new(
394 String::new(),
395 Language::TypeScript,
396 kind.clone(),
397 Some(name),
398 span,
399 scope.clone(),
400 preview,
401 ));
402 }
403 }
404
405 Ok(symbols)
406}
407
408fn node_to_span(node: &tree_sitter::Node) -> Span {
410 let start = node.start_position();
411 let end = node.end_position();
412
413 Span::new(
414 start.row + 1, start.column,
416 end.row + 1,
417 end.column,
418 )
419}
420
421fn extract_preview(source: &str, span: &Span) -> String {
423 let lines: Vec<&str> = source.lines().collect();
424
425 let start_idx = (span.start_line - 1) as usize; let end_idx = (start_idx + 7).min(lines.len());
428
429 lines[start_idx..end_idx].join("\n")
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn test_parse_function() {
438 let source = r#"
439 function greet(name: string): string {
440 return `Hello, ${name}!`;
441 }
442 "#;
443
444 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
445 assert_eq!(symbols.len(), 1);
446 assert_eq!(symbols[0].symbol.as_deref(), Some("greet"));
447 assert!(matches!(symbols[0].kind, SymbolKind::Function));
448 }
449
450 #[test]
451 fn test_parse_arrow_function() {
452 let source = r#"
453 const add = (a: number, b: number): number => {
454 return a + b;
455 };
456 "#;
457
458 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
459 assert_eq!(symbols.len(), 1);
460 assert_eq!(symbols[0].symbol.as_deref(), Some("add"));
461 assert!(matches!(symbols[0].kind, SymbolKind::Function));
462 }
463
464 #[test]
465 fn test_parse_async_function() {
466 let source = r#"
467 async function fetchData(url: string): Promise<Response> {
468 return await fetch(url);
469 }
470 "#;
471
472 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
473 assert_eq!(symbols.len(), 1);
474 assert_eq!(symbols[0].symbol.as_deref(), Some("fetchData"));
475 assert!(matches!(symbols[0].kind, SymbolKind::Function));
476 }
477
478 #[test]
479 fn test_parse_class() {
480 let source = r#"
481 class User {
482 name: string;
483 age: number;
484
485 constructor(name: string, age: number) {
486 this.name = name;
487 this.age = age;
488 }
489 }
490 "#;
491
492 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
493
494 let class_symbols: Vec<_> = symbols
496 .iter()
497 .filter(|s| matches!(s.kind, SymbolKind::Class))
498 .collect();
499
500 assert_eq!(class_symbols.len(), 1);
501 assert_eq!(class_symbols[0].symbol.as_deref(), Some("User"));
502 }
503
504 #[test]
505 fn test_parse_class_with_methods() {
506 let source = r#"
507 class Calculator {
508 add(a: number, b: number): number {
509 return a + b;
510 }
511
512 subtract(a: number, b: number): number {
513 return a - b;
514 }
515 }
516 "#;
517
518 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
519
520 assert!(symbols.len() >= 3);
522
523 let method_symbols: Vec<_> = symbols
524 .iter()
525 .filter(|s| matches!(s.kind, SymbolKind::Method))
526 .collect();
527
528 assert_eq!(method_symbols.len(), 2);
529 assert!(
530 method_symbols
531 .iter()
532 .any(|s| s.symbol.as_deref() == Some("add"))
533 );
534 assert!(
535 method_symbols
536 .iter()
537 .any(|s| s.symbol.as_deref() == Some("subtract"))
538 );
539
540 for method in method_symbols {
542 }
544 }
545
546 #[test]
547 fn test_parse_interface() {
548 let source = r#"
549 interface User {
550 name: string;
551 age: number;
552 email?: string;
553 }
554 "#;
555
556 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
557 assert_eq!(symbols.len(), 1);
558 assert_eq!(symbols[0].symbol.as_deref(), Some("User"));
559 assert!(matches!(symbols[0].kind, SymbolKind::Interface));
560 }
561
562 #[test]
563 fn test_parse_type_alias() {
564 let source = r#"
565 type UserId = string | number;
566 type UserRole = 'admin' | 'user' | 'guest';
567 "#;
568
569 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
570 assert_eq!(symbols.len(), 2);
571
572 let type_symbols: Vec<_> = symbols
573 .iter()
574 .filter(|s| matches!(s.kind, SymbolKind::Type))
575 .collect();
576
577 assert_eq!(type_symbols.len(), 2);
578 assert!(
579 type_symbols
580 .iter()
581 .any(|s| s.symbol.as_deref() == Some("UserId"))
582 );
583 assert!(
584 type_symbols
585 .iter()
586 .any(|s| s.symbol.as_deref() == Some("UserRole"))
587 );
588 }
589
590 #[test]
591 fn test_parse_enum() {
592 let source = r#"
593 enum Status {
594 Active,
595 Inactive,
596 Pending
597 }
598 "#;
599
600 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
601 assert_eq!(symbols.len(), 1);
602 assert_eq!(symbols[0].symbol.as_deref(), Some("Status"));
603 assert!(matches!(symbols[0].kind, SymbolKind::Enum));
604 }
605
606 #[test]
607 fn test_parse_const() {
608 let source = r#"
609 const MAX_SIZE = 100;
610 const DEFAULT_USER = {
611 name: "Anonymous",
612 age: 0
613 };
614 "#;
615
616 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
617 assert_eq!(symbols.len(), 2);
618
619 let const_symbols: Vec<_> = symbols
620 .iter()
621 .filter(|s| matches!(s.kind, SymbolKind::Constant))
622 .collect();
623
624 assert_eq!(const_symbols.len(), 2);
625 assert!(
626 const_symbols
627 .iter()
628 .any(|s| s.symbol.as_deref() == Some("MAX_SIZE"))
629 );
630 assert!(
631 const_symbols
632 .iter()
633 .any(|s| s.symbol.as_deref() == Some("DEFAULT_USER"))
634 );
635 }
636
637 #[test]
638 fn test_parse_react_component() {
639 let source = r#"
640 import React, { useState } from 'react';
641
642 interface ButtonProps {
643 label: string;
644 onClick: () => void;
645 }
646
647 const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
648 return (
649 <button onClick={onClick}>
650 {label}
651 </button>
652 );
653 };
654
655 function useCounter(initial: number) {
656 const [count, setCount] = React.useState(initial);
657 return { count, setCount };
658 }
659
660 export default Button;
661 "#;
662
663 let symbols = parse("Button.tsx", source, Language::TypeScript).unwrap();
664
665 assert!(
667 symbols
668 .iter()
669 .any(|s| s.symbol.as_deref() == Some("ButtonProps")
670 && matches!(s.kind, SymbolKind::Interface))
671 );
672 assert!(symbols.iter().any(
673 |s| s.symbol.as_deref() == Some("Button") && matches!(s.kind, SymbolKind::Function)
674 ));
675 assert!(
676 symbols
677 .iter()
678 .any(|s| s.symbol.as_deref() == Some("useCounter")
679 && matches!(s.kind, SymbolKind::Function))
680 );
681 }
682
683 #[test]
684 fn test_parse_mixed_symbols() {
685 let source = r#"
686 interface Config {
687 debug: boolean;
688 }
689
690 type ConfigKey = keyof Config;
691
692 const DEFAULT_CONFIG: Config = {
693 debug: false
694 };
695
696 class ConfigManager {
697 private config: Config;
698
699 constructor(config: Config) {
700 this.config = config;
701 }
702
703 getConfig(): Config {
704 return this.config;
705 }
706 }
707
708 function loadConfig(): Config {
709 return DEFAULT_CONFIG;
710 }
711 "#;
712
713 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
714
715 assert!(symbols.len() >= 6);
717
718 let kinds: Vec<&SymbolKind> = symbols.iter().map(|s| &s.kind).collect();
719 assert!(kinds.contains(&&SymbolKind::Interface));
720 assert!(kinds.contains(&&SymbolKind::Type));
721 assert!(kinds.contains(&&SymbolKind::Constant));
722 assert!(kinds.contains(&&SymbolKind::Class));
723 assert!(kinds.contains(&&SymbolKind::Method));
724 assert!(kinds.contains(&&SymbolKind::Function));
725 }
726
727 #[test]
728 fn test_parse_async_class_methods() {
729 let source = r#"
730 export class CentralUsersModule {
731 async getAllUsers(params) {
732 return await this.call('get', `/users`, params)
733 }
734
735 async getUser(userId) {
736 return await this.call('get', `/users/${userId}`)
737 }
738
739 deleteUser(userId) {
740 return this.call('delete', `/user/${userId}`)
741 }
742 }
743 "#;
744
745 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
746
747 println!("\nAll symbols found:");
749 for symbol in &symbols {
750 println!(
751 " {:?} - {}",
752 symbol.kind,
753 symbol.symbol.as_deref().unwrap_or("")
754 );
755 }
756
757 let class_symbols: Vec<_> = symbols
759 .iter()
760 .filter(|s| matches!(s.kind, SymbolKind::Class))
761 .collect();
762 assert_eq!(class_symbols.len(), 1);
763 assert_eq!(
764 class_symbols[0].symbol.as_deref(),
765 Some("CentralUsersModule")
766 );
767
768 let method_symbols: Vec<_> = symbols
769 .iter()
770 .filter(|s| matches!(s.kind, SymbolKind::Method))
771 .collect();
772
773 assert_eq!(
775 method_symbols.len(),
776 3,
777 "Expected 3 methods, found {}",
778 method_symbols.len()
779 );
780 assert!(
781 method_symbols
782 .iter()
783 .any(|s| s.symbol.as_deref() == Some("getAllUsers"))
784 );
785 assert!(
786 method_symbols
787 .iter()
788 .any(|s| s.symbol.as_deref() == Some("getUser"))
789 );
790 assert!(
791 method_symbols
792 .iter()
793 .any(|s| s.symbol.as_deref() == Some("deleteUser"))
794 );
795
796 let variable_symbols: Vec<_> = symbols
798 .iter()
799 .filter(|s| {
800 matches!(s.kind, SymbolKind::Constant) || matches!(s.kind, SymbolKind::Variable)
801 })
802 .collect();
803 assert_eq!(
804 variable_symbols.len(),
805 0,
806 "Async methods should not be classified as variables"
807 );
808
809 for method in method_symbols {
811 }
813 }
814
815 #[test]
816 fn test_parse_user_exact_code() {
817 let source = r#"
819export class CentralUsersModule extends HttpFactory<WatchHookMap, WatchEvents> {
820 protected $events = {
821 //
822 }
823
824 async checkAuthenticated() {
825 return await this.call('get', '/check')
826 }
827
828 async getUser(userId: CentralUser['id']) {
829 return await this.call<CentralUser>('get', `/users/${userId}`)
830 }
831
832 async getAllUsers(params?: PaginatedParams & SortableParams & SearchableParams) {
833 return await this.call<CentralUser[]>('get', `/users`, params)
834 }
835
836 async deleteUser(userId: CentralUser['id']) {
837 return await this.call<void>('delete', `/user/${userId}`)
838 }
839}
840 "#;
841
842 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
843
844 println!("\nAll symbols found in user code:");
846 for symbol in &symbols {
847 println!(
848 " {:?} - {}",
849 symbol.kind,
850 symbol.symbol.as_deref().unwrap_or("")
851 );
852 }
853
854 let get_all_users_symbols: Vec<_> = symbols
856 .iter()
857 .filter(|s| s.symbol.as_deref() == Some("getAllUsers"))
858 .collect();
859
860 assert_eq!(
861 get_all_users_symbols.len(),
862 1,
863 "Should find exactly one getAllUsers"
864 );
865 assert!(
866 matches!(get_all_users_symbols[0].kind, SymbolKind::Method),
867 "getAllUsers should be a Method, not {:?}",
868 get_all_users_symbols[0].kind
869 );
870 }
871
872 #[test]
873 fn test_local_variables_included() {
874 let source = r#"
875 const GLOBAL_CONSTANT = 100;
876 let globalLet = 50;
877 var globalVar = 25;
878
879 function calculate(x: number): number {
880 const localConst = x * 2;
881 let localLet = 5;
882 var localVar = 10;
883 return localConst + localLet + localVar;
884 }
885 "#;
886
887 let symbols = parse("test.ts", source, Language::TypeScript).unwrap();
888
889 let var_symbols: Vec<_> = symbols
890 .iter()
891 .filter(|s| {
892 matches!(s.kind, SymbolKind::Variable) || matches!(s.kind, SymbolKind::Constant)
893 })
894 .collect();
895
896 assert_eq!(var_symbols.len(), 6);
898
899 assert!(
901 var_symbols
902 .iter()
903 .any(|s| s.symbol.as_deref() == Some("GLOBAL_CONSTANT"))
904 );
905 assert!(
906 var_symbols
907 .iter()
908 .any(|s| s.symbol.as_deref() == Some("globalLet"))
909 );
910 assert!(
911 var_symbols
912 .iter()
913 .any(|s| s.symbol.as_deref() == Some("globalVar"))
914 );
915
916 assert!(
918 var_symbols
919 .iter()
920 .any(|s| s.symbol.as_deref() == Some("localConst"))
921 );
922 assert!(
923 var_symbols
924 .iter()
925 .any(|s| s.symbol.as_deref() == Some("localLet"))
926 );
927 assert!(
928 var_symbols
929 .iter()
930 .any(|s| s.symbol.as_deref() == Some("localVar"))
931 );
932
933 let global_const = var_symbols
935 .iter()
936 .find(|s| s.symbol.as_deref() == Some("GLOBAL_CONSTANT"))
937 .unwrap();
938 assert!(matches!(global_const.kind, SymbolKind::Constant));
939
940 let global_let = var_symbols
941 .iter()
942 .find(|s| s.symbol.as_deref() == Some("globalLet"))
943 .unwrap();
944 assert!(matches!(global_let.kind, SymbolKind::Variable));
945 }
946}
947
948use crate::models::ImportType;
953use crate::parsers::{DependencyExtractor, ImportInfo};
954
955pub struct TypeScriptDependencyExtractor;
957
958impl DependencyExtractor for TypeScriptDependencyExtractor {
959 fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>> {
960 Self::extract_dependencies_with_alias_map(source, None)
962 }
963}
964
965impl TypeScriptDependencyExtractor {
966 pub fn extract_dependencies_with_alias_map(
971 source: &str,
972 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
973 ) -> Result<Vec<ImportInfo>> {
974 let mut parser = Parser::new();
975 let language = tree_sitter_typescript::LANGUAGE_TSX; parser
978 .set_language(&language.into())
979 .context("Failed to set TypeScript/JavaScript language")?;
980
981 let tree = parser
982 .parse(source, None)
983 .context("Failed to parse TypeScript/JavaScript source")?;
984
985 let root_node = tree.root_node();
986
987 let mut imports = Vec::new();
988
989 imports.extend(extract_import_declarations(source, &root_node, alias_map)?);
991
992 imports.extend(extract_require_statements(source, &root_node, alias_map)?);
994
995 Ok(imports)
996 }
997}
998
999fn extract_import_declarations(
1001 source: &str,
1002 root: &tree_sitter::Node,
1003 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
1004) -> Result<Vec<ImportInfo>> {
1005 let language = tree_sitter_typescript::LANGUAGE_TSX;
1006
1007 let query_str = r#"
1008 (import_statement
1009 source: (string) @import_path) @import
1010 "#;
1011
1012 let query = Query::new(&language.into(), query_str)
1013 .context("Failed to create import declaration query")?;
1014
1015 let mut cursor = QueryCursor::new();
1016 let mut matches = cursor.matches(&query, *root, source.as_bytes());
1017
1018 let mut imports = Vec::new();
1019
1020 while let Some(match_) = matches.next() {
1021 let mut import_path = None;
1022 let mut import_node = None;
1023
1024 for capture in match_.captures {
1025 let capture_name: &str = &query.capture_names()[capture.index as usize];
1026 match capture_name {
1027 "import_path" => {
1028 let raw_path = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
1030 import_path = Some(
1031 raw_path
1032 .trim_matches(|c| c == '"' || c == '\'' || c == '`')
1033 .to_string(),
1034 );
1035 }
1036 "import" => {
1037 import_node = Some(capture.node);
1038 }
1039 _ => {}
1040 }
1041 }
1042
1043 if let (Some(path), Some(node)) = (import_path, import_node) {
1044 let import_type = classify_js_import(&path, alias_map);
1045 let line_number = node.start_position().row + 1;
1046
1047 let imported_symbols = extract_imported_symbols_js(source, &node);
1049
1050 imports.push(ImportInfo {
1051 imported_path: path,
1052 import_type,
1053 line_number,
1054 imported_symbols,
1055 });
1056 }
1057 }
1058
1059 Ok(imports)
1060}
1061
1062fn extract_require_statements(
1064 source: &str,
1065 root: &tree_sitter::Node,
1066 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
1067) -> Result<Vec<ImportInfo>> {
1068 let language = tree_sitter_typescript::LANGUAGE_TSX;
1069
1070 let query_str = r#"
1071 (call_expression
1072 function: (identifier) @func_name
1073 arguments: (arguments (string) @require_path)) @require_call
1074 "#;
1075
1076 let query =
1077 Query::new(&language.into(), query_str).context("Failed to create require query")?;
1078
1079 let mut cursor = QueryCursor::new();
1080 let mut matches = cursor.matches(&query, *root, source.as_bytes());
1081
1082 let mut imports = Vec::new();
1083
1084 while let Some(match_) = matches.next() {
1085 let mut func_name = None;
1086 let mut require_path = None;
1087 let mut require_node = None;
1088
1089 for capture in match_.captures {
1090 let capture_name: &str = &query.capture_names()[capture.index as usize];
1091 match capture_name {
1092 "func_name" => {
1093 func_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or(""));
1094 }
1095 "require_path" => {
1096 let raw_path = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
1098 require_path = Some(
1099 raw_path
1100 .trim_matches(|c| c == '"' || c == '\'' || c == '`')
1101 .to_string(),
1102 );
1103 }
1104 "require_call" => {
1105 require_node = Some(capture.node);
1106 }
1107 _ => {}
1108 }
1109 }
1110
1111 if func_name == Some("require") {
1113 if let (Some(path), Some(node)) = (require_path, require_node) {
1114 let import_type = classify_js_import(&path, alias_map);
1115 let line_number = node.start_position().row + 1;
1116
1117 imports.push(ImportInfo {
1118 imported_path: path,
1119 import_type,
1120 line_number,
1121 imported_symbols: None, });
1123 }
1124 }
1125 }
1126
1127 Ok(imports)
1128}
1129
1130fn extract_imported_symbols_js(
1132 source: &str,
1133 import_node: &tree_sitter::Node,
1134) -> Option<Vec<String>> {
1135 let mut symbols = Vec::new();
1136
1137 let mut cursor = import_node.walk();
1139 for child in import_node.children(&mut cursor) {
1140 if child.kind() == "import_clause" {
1141 let mut clause_cursor = child.walk();
1143 for grandchild in child.children(&mut clause_cursor) {
1144 match grandchild.kind() {
1145 "named_imports" => {
1146 let mut specifier_cursor = grandchild.walk();
1148 for specifier in grandchild.children(&mut specifier_cursor) {
1149 if specifier.kind() == "import_specifier" {
1150 if let Ok(text) = specifier.utf8_text(source.as_bytes()) {
1152 let name = text.split_whitespace().next().unwrap_or(text);
1154 symbols.push(name.to_string());
1155 }
1156 }
1157 }
1158 }
1159 "identifier" => {
1160 if let Ok(text) = grandchild.utf8_text(source.as_bytes()) {
1162 symbols.push(text.to_string());
1163 }
1164 }
1165 _ => {}
1166 }
1167 }
1168 }
1169 }
1170
1171 if symbols.is_empty() {
1172 None
1173 } else {
1174 Some(symbols)
1175 }
1176}
1177
1178fn classify_js_import(
1188 import_path: &str,
1189 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
1190) -> ImportType {
1191 if import_path.starts_with("./") || import_path.starts_with("../") {
1193 log::trace!(
1194 "classify_js_import: '{}' => Internal (relative)",
1195 import_path
1196 );
1197 return ImportType::Internal;
1198 }
1199
1200 if import_path.starts_with("/") {
1202 log::trace!(
1203 "classify_js_import: '{}' => Internal (absolute)",
1204 import_path
1205 );
1206 return ImportType::Internal;
1207 }
1208
1209 if let Some(map) = alias_map {
1211 log::trace!(
1212 "classify_js_import: checking '{}' against {} aliases",
1213 import_path,
1214 map.aliases.len()
1215 );
1216 for alias_pattern in map.aliases.keys() {
1217 if alias_pattern.ends_with("/*") {
1219 let alias_prefix = alias_pattern.trim_end_matches("/*");
1220 if import_path.starts_with(alias_prefix) {
1221 log::info!(
1222 "classify_js_import: '{}' => Internal (matches alias pattern '{}')",
1223 import_path,
1224 alias_pattern
1225 );
1226 return ImportType::Internal;
1227 }
1228 } else {
1229 if import_path == alias_pattern {
1231 log::info!(
1232 "classify_js_import: '{}' => Internal (exact match alias '{}')",
1233 import_path,
1234 alias_pattern
1235 );
1236 return ImportType::Internal;
1237 }
1238 }
1239 }
1240 log::trace!(
1241 "classify_js_import: '{}' did not match any of {} alias patterns",
1242 import_path,
1243 map.aliases.len()
1244 );
1245 } else {
1246 log::trace!(
1247 "classify_js_import: no alias map provided for '{}'",
1248 import_path
1249 );
1250 }
1251
1252 const STDLIB_MODULES: &[&str] = &[
1254 "fs",
1255 "path",
1256 "os",
1257 "crypto",
1258 "util",
1259 "events",
1260 "stream",
1261 "buffer",
1262 "http",
1263 "https",
1264 "net",
1265 "tls",
1266 "url",
1267 "querystring",
1268 "dns",
1269 "child_process",
1270 "cluster",
1271 "worker_threads",
1272 "readline",
1273 "zlib",
1274 "assert",
1275 "console",
1276 "module",
1277 "process",
1278 "timers",
1279 "vm",
1280 "string_decoder",
1281 "dgram",
1282 "v8",
1283 "perf_hooks",
1284 "node:fs",
1286 "node:path",
1287 "node:os",
1288 "node:crypto",
1289 "node:util",
1290 "node:events",
1291 "node:stream",
1292 "node:buffer",
1293 "node:http",
1294 "node:https",
1295 "node:net",
1296 ];
1297
1298 if STDLIB_MODULES.contains(&import_path) {
1300 log::trace!("classify_js_import: '{}' => Stdlib", import_path);
1301 return ImportType::Stdlib;
1302 }
1303
1304 log::info!(
1306 "classify_js_import: '{}' => External (not alias, relative, absolute, or stdlib)",
1307 import_path
1308 );
1309 ImportType::External
1310}
1311
1312use crate::parsers::ExportInfo;
1317
1318impl TypeScriptDependencyExtractor {
1319 pub fn extract_export_declarations(
1329 source: &str,
1330 _alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
1331 ) -> Result<Vec<ExportInfo>> {
1332 let mut parser = Parser::new();
1333 let language = tree_sitter_typescript::LANGUAGE_TSX;
1334
1335 parser
1336 .set_language(&language.into())
1337 .context("Failed to set TypeScript/JavaScript language")?;
1338
1339 let tree = parser
1340 .parse(source, None)
1341 .context("Failed to parse TypeScript/JavaScript source for export extraction")?;
1342
1343 let root_node = tree.root_node();
1344
1345 let mut exports = Vec::new();
1346
1347 exports.extend(extract_export_from_statements(source, &root_node)?);
1349
1350 Ok(exports)
1351 }
1352}
1353
1354fn extract_export_from_statements(
1362 source: &str,
1363 root: &tree_sitter::Node,
1364) -> Result<Vec<ExportInfo>> {
1365 let language = tree_sitter_typescript::LANGUAGE_TSX;
1366
1367 let query_str = r#"
1369 (export_statement
1370 source: (string) @source_path) @export
1371 "#;
1372
1373 let query = Query::new(&language.into(), query_str)
1374 .context("Failed to create export statement query")?;
1375
1376 let mut cursor = QueryCursor::new();
1377 let mut matches = cursor.matches(&query, *root, source.as_bytes());
1378
1379 let mut exports = Vec::new();
1380
1381 while let Some(match_) = matches.next() {
1382 let mut source_path = None;
1383 let mut export_node = None;
1384
1385 for capture in match_.captures {
1386 let capture_name: &str = &query.capture_names()[capture.index as usize];
1387 match capture_name {
1388 "source_path" => {
1389 let raw_path = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
1391 source_path = Some(
1392 raw_path
1393 .trim_matches(|c| c == '"' || c == '\'' || c == '`')
1394 .to_string(),
1395 );
1396 }
1397 "export" => {
1398 export_node = Some(capture.node);
1399 }
1400 _ => {}
1401 }
1402 }
1403
1404 if let (Some(path), Some(node)) = (source_path, export_node) {
1405 let line_number = node.start_position().row + 1;
1406
1407 let exported_symbols = extract_exported_symbols(source, &node)?;
1409
1410 if exported_symbols.is_empty() {
1412 exports.push(ExportInfo {
1413 exported_symbol: None, source_path: path,
1415 line_number,
1416 });
1417 } else {
1418 for symbol in exported_symbols {
1420 exports.push(ExportInfo {
1421 exported_symbol: Some(symbol),
1422 source_path: path.clone(),
1423 line_number,
1424 });
1425 }
1426 }
1427 }
1428 }
1429
1430 Ok(exports)
1431}
1432
1433fn extract_exported_symbols(source: &str, export_node: &tree_sitter::Node) -> Result<Vec<String>> {
1438 let mut symbols = Vec::new();
1439
1440 let mut cursor = export_node.walk();
1442 for child in export_node.children(&mut cursor) {
1443 if child.kind() == "export_clause" {
1444 let mut specifier_cursor = child.walk();
1446 for specifier in child.children(&mut specifier_cursor) {
1447 if specifier.kind() == "export_specifier" {
1448 if let Ok(text) = specifier.utf8_text(source.as_bytes()) {
1451 let name = text.split_whitespace().next().unwrap_or(text);
1453 symbols.push(name.to_string());
1454 }
1455 }
1456 }
1457 }
1458 }
1459
1460 Ok(symbols)
1461}
1462
1463pub fn resolve_ts_import_to_path(
1478 import_path: &str,
1479 current_file_path: Option<&str>,
1480 alias_map: Option<&crate::parsers::tsconfig::PathAliasMap>,
1481) -> Option<String> {
1482 log::debug!(
1483 "resolve_ts_import_to_path: import_path={}, current_file={:?}, has_alias_map={}",
1484 import_path,
1485 current_file_path,
1486 alias_map.is_some()
1487 );
1488
1489 if let Some(map) = alias_map {
1491 log::debug!(
1492 " Trying alias resolution with {} aliases (config_dir: {:?}, base_url: {:?})",
1493 map.aliases.len(),
1494 map.config_dir,
1495 map.base_url
1496 );
1497 if let Some(resolved_alias) = map.resolve_alias(import_path) {
1498 log::debug!(" Alias matched! {} => {}", import_path, resolved_alias);
1499 let resolved_path = map.resolve_relative_to_config(&resolved_alias);
1501 let path_str = resolved_path.to_string_lossy().to_string();
1502 log::debug!(" After resolve_relative_to_config: {}", path_str);
1503
1504 let has_extension = path_str.ends_with(".vue")
1506 || path_str.ends_with(".svelte")
1507 || path_str.ends_with(".ts")
1508 || path_str.ends_with(".tsx")
1509 || path_str.ends_with(".js")
1510 || path_str.ends_with(".jsx")
1511 || path_str.ends_with(".mjs")
1512 || path_str.ends_with(".cjs");
1513
1514 if has_extension {
1515 log::trace!("Resolved alias {} => {}", import_path, path_str);
1516 return Some(path_str);
1517 }
1518
1519 let extensions = vec![
1521 ".tsx",
1522 ".ts",
1523 ".jsx",
1524 ".js",
1525 ".mjs",
1526 ".cjs",
1527 "/index.tsx",
1528 "/index.ts",
1529 "/index.jsx",
1530 "/index.js",
1531 ];
1532
1533 let candidates: Vec<String> = extensions
1534 .iter()
1535 .map(|ext| format!("{}{}", path_str, ext))
1536 .collect();
1537
1538 log::trace!(
1539 "Resolved alias {} => {} (candidates: {})",
1540 import_path,
1541 path_str,
1542 candidates.join(" | ")
1543 );
1544 return Some(candidates.join("|"));
1545 }
1546 }
1547
1548 if !import_path.starts_with("./") && !import_path.starts_with("../") {
1551 return None;
1552 }
1553
1554 let current_file = current_file_path?;
1555
1556 let current_dir = std::path::Path::new(current_file).parent()?;
1558
1559 let resolved = current_dir.join(import_path);
1561
1562 let normalized_path = std::path::Path::new(&resolved).components().fold(
1565 std::path::PathBuf::new(),
1566 |mut acc, component| {
1567 match component {
1568 std::path::Component::CurDir => acc, std::path::Component::ParentDir => {
1570 acc.pop(); acc
1572 }
1573 _ => {
1574 acc.push(component);
1575 acc
1576 }
1577 }
1578 },
1579 );
1580
1581 let normalized = normalized_path.to_string_lossy().to_string();
1582
1583 let has_extension = normalized.ends_with(".vue")
1586 || normalized.ends_with(".svelte")
1587 || normalized.ends_with(".ts")
1588 || normalized.ends_with(".tsx")
1589 || normalized.ends_with(".js")
1590 || normalized.ends_with(".jsx")
1591 || normalized.ends_with(".mjs")
1592 || normalized.ends_with(".cjs");
1593
1594 if has_extension {
1595 log::trace!("TS/JS import with extension: {}", normalized);
1597 return Some(normalized);
1598 }
1599
1600 let extensions = vec![
1608 ".tsx",
1609 ".ts",
1610 ".jsx",
1611 ".js",
1612 ".mjs",
1613 ".cjs",
1614 "/index.tsx",
1615 "/index.ts",
1616 "/index.jsx",
1617 "/index.js",
1618 ];
1619
1620 let candidates: Vec<String> = extensions
1621 .iter()
1622 .map(|ext| format!("{}{}", normalized, ext))
1623 .collect();
1624
1625 log::trace!(
1626 "TS/JS import candidates (no extension): {}",
1627 candidates.join(" | ")
1628 );
1629
1630 Some(candidates.join("|"))
1633}
1634
1635#[cfg(test)]
1636mod path_resolution_tests {
1637 use super::*;
1638
1639 #[test]
1640 fn test_resolve_relative_import_same_directory() {
1641 let result = resolve_ts_import_to_path("./Button", Some("src/components/App.tsx"), None);
1643
1644 assert!(result.is_some());
1645 let candidates = result.unwrap();
1646 assert!(candidates.contains("Button.tsx"));
1648 assert!(candidates.contains("Button.ts"));
1649 assert!(
1651 candidates.starts_with("src/components/Button.tsx")
1652 || candidates.contains("/Button.tsx|")
1653 );
1654 }
1655
1656 #[test]
1657 fn test_resolve_relative_import_parent_directory() {
1658 let result =
1660 resolve_ts_import_to_path("../utils/helper", Some("src/components/Button.tsx"), None);
1661
1662 assert!(result.is_some());
1663 let path = result.unwrap();
1664 assert!(path.contains("utils/helper"));
1665 }
1666
1667 #[test]
1668 fn test_resolve_relative_import_multiple_parents() {
1669 let result = resolve_ts_import_to_path(
1671 "../../config/app",
1672 Some("src/components/ui/Button.tsx"),
1673 None,
1674 );
1675
1676 assert!(result.is_some());
1677 let path = result.unwrap();
1678 assert!(path.contains("config/app"));
1679 }
1680
1681 #[test]
1682 fn test_resolve_index_file() {
1683 let result = resolve_ts_import_to_path("./components", Some("src/App.tsx"), None);
1685
1686 assert!(result.is_some());
1687 assert!(result.unwrap().contains("components"));
1690 }
1691
1692 #[test]
1693 fn test_absolute_import_not_supported_without_alias_map() {
1694 let result = resolve_ts_import_to_path("@components/Button", Some("src/App.tsx"), None);
1696
1697 assert!(result.is_none());
1699 }
1700
1701 #[test]
1702 fn test_node_modules_import_not_supported() {
1703 let result = resolve_ts_import_to_path("react", Some("src/App.tsx"), None);
1705
1706 assert!(result.is_none());
1708 }
1709
1710 #[test]
1711 fn test_resolve_without_current_file() {
1712 let result = resolve_ts_import_to_path("./Button", None, None);
1713
1714 assert!(result.is_none());
1716 }
1717
1718 #[test]
1719 fn test_resolve_nested_directory_structure() {
1720 let result = resolve_ts_import_to_path("./api/client", Some("src/services/http.ts"), None);
1722
1723 assert!(result.is_some());
1724 let path = result.unwrap();
1725 assert!(path.contains("api/client"));
1727 }
1728}
1729
1730#[cfg(test)]
1731mod dependency_extraction_tests {
1732 use super::*;
1733
1734 #[test]
1735 fn test_extract_basic_imports() {
1736 let source = r#"
1737 import { Button } from './components/Button';
1738 import React from 'react';
1739 import fs from 'fs';
1740 import '../styles.css';
1741 "#;
1742
1743 let deps = TypeScriptDependencyExtractor::extract_dependencies(source).unwrap();
1744
1745 assert_eq!(deps.len(), 4, "Should extract 4 import statements");
1746 assert!(
1747 deps.iter()
1748 .any(|d| d.imported_path == "./components/Button")
1749 );
1750 assert!(deps.iter().any(|d| d.imported_path == "react"));
1751 assert!(deps.iter().any(|d| d.imported_path == "fs"));
1752 assert!(deps.iter().any(|d| d.imported_path == "../styles.css"));
1753 }
1754
1755 #[test]
1756 fn test_dynamic_imports_filtered() {
1757 let source = r#"
1758 import { Button } from './components/Button';
1759 import React from 'react';
1760 const fs = require('fs');
1761
1762 // Dynamic imports - should be filtered out
1763 const moduleName = './dynamic-module';
1764 import(moduleName);
1765 import(`./templates/${template}`);
1766 require(variable);
1767 require(CONFIG_PATH + '/settings.js');
1768 "#;
1769
1770 let deps = TypeScriptDependencyExtractor::extract_dependencies(source).unwrap();
1771
1772 assert_eq!(deps.len(), 3, "Should extract 3 static imports only");
1775
1776 assert!(
1777 deps.iter()
1778 .any(|d| d.imported_path == "./components/Button")
1779 );
1780 assert!(deps.iter().any(|d| d.imported_path == "react"));
1781 assert!(deps.iter().any(|d| d.imported_path == "fs"));
1782
1783 assert!(!deps.iter().any(|d| d.imported_path.contains("moduleName")));
1785 assert!(!deps.iter().any(|d| d.imported_path.contains("template")));
1786 assert!(!deps.iter().any(|d| d.imported_path.contains("variable")));
1787 assert!(!deps.iter().any(|d| d.imported_path.contains("CONFIG_PATH")));
1788 }
1789
1790 #[test]
1791 fn test_require_with_template_literals_filtered() {
1792 let source = r#"
1793 const path = require('path');
1794 const utils = require('./utils');
1795
1796 // Dynamic requires with template literals - should be filtered out
1797 const config = require(`./config/${env}.json`);
1798 const plugin = require(`${PLUGIN_DIR}/loader`);
1799 "#;
1800
1801 let deps = TypeScriptDependencyExtractor::extract_dependencies(source).unwrap();
1802
1803 assert_eq!(deps.len(), 2, "Should extract 2 static requires only");
1806
1807 assert!(deps.iter().any(|d| d.imported_path == "path"));
1808 assert!(deps.iter().any(|d| d.imported_path == "./utils"));
1809
1810 assert!(!deps.iter().any(|d| d.imported_path.contains("env")));
1812 assert!(!deps.iter().any(|d| d.imported_path.contains("PLUGIN_DIR")));
1813 }
1814}