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