1use super::{node_text, LanguageExtractor};
7use crate::ast::{
8 ExtractedSymbol, FunctionCall, Import, ImportedName, Parameter, SymbolKind, Visibility,
9};
10use crate::error::Result;
11use tree_sitter::{Language, Node, Tree};
12
13pub struct TypeScriptExtractor;
15
16impl LanguageExtractor for TypeScriptExtractor {
17 fn language(&self) -> Language {
18 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
19 }
20
21 fn name(&self) -> &'static str {
22 "typescript"
23 }
24
25 fn extensions(&self) -> &'static [&'static str] {
26 &["ts", "tsx"]
27 }
28
29 fn extract_symbols(&self, tree: &Tree, source: &str) -> Result<Vec<ExtractedSymbol>> {
30 let mut symbols = Vec::new();
31 let root = tree.root_node();
32 self.extract_symbols_recursive(&root, source, &mut symbols, None);
33
34 let exported_names = self.find_named_exports(&root, source);
36 for sym in &mut symbols {
37 if exported_names.contains(&sym.name) {
38 sym.exported = true;
39 }
40 }
41
42 Ok(symbols)
43 }
44
45 fn extract_imports(&self, tree: &Tree, source: &str) -> Result<Vec<Import>> {
46 let mut imports = Vec::new();
47 let root = tree.root_node();
48 self.extract_imports_recursive(&root, source, &mut imports);
49 Ok(imports)
50 }
51
52 fn extract_calls(
53 &self,
54 tree: &Tree,
55 source: &str,
56 current_function: Option<&str>,
57 ) -> Result<Vec<FunctionCall>> {
58 let mut calls = Vec::new();
59 let root = tree.root_node();
60 self.extract_calls_recursive(&root, source, &mut calls, current_function);
61 Ok(calls)
62 }
63
64 fn extract_doc_comment(&self, node: &Node, source: &str) -> Option<String> {
65 if let Some(prev) = node.prev_sibling() {
67 if prev.kind() == "comment" {
68 let comment = node_text(&prev, source);
69 if comment.starts_with("/**") {
71 return Some(Self::clean_jsdoc(comment));
72 }
73 if let Some(rest) = comment.strip_prefix("//") {
75 return Some(rest.trim().to_string());
76 }
77 }
78 }
79 None
80 }
81}
82
83impl TypeScriptExtractor {
84 fn extract_symbols_recursive(
85 &self,
86 node: &Node,
87 source: &str,
88 symbols: &mut Vec<ExtractedSymbol>,
89 parent: Option<&str>,
90 ) {
91 match node.kind() {
92 "function_declaration" => {
94 if let Some(sym) = self.extract_function(node, source, parent) {
95 symbols.push(sym);
96 }
97 }
98
99 "lexical_declaration" | "variable_declaration" => {
101 self.extract_variable_symbols(node, source, symbols, parent);
102 }
103
104 "class_declaration" => {
106 if let Some(sym) = self.extract_class(node, source, parent) {
107 let class_name = sym.name.clone();
108 symbols.push(sym);
109
110 if let Some(body) = node.child_by_field_name("body") {
112 self.extract_class_members(&body, source, symbols, Some(&class_name));
113 }
114 }
115 }
116
117 "interface_declaration" => {
119 if let Some(sym) = self.extract_interface(node, source, parent) {
120 symbols.push(sym);
121 }
122 }
123
124 "type_alias_declaration" => {
126 if let Some(sym) = self.extract_type_alias(node, source, parent) {
127 symbols.push(sym);
128 }
129 }
130
131 "enum_declaration" => {
133 if let Some(sym) = self.extract_enum(node, source, parent) {
134 symbols.push(sym);
135 }
136 }
137
138 "export_statement" => {
140 self.extract_export_symbols(node, source, symbols, parent);
141 }
142
143 _ => {}
144 }
145
146 let mut cursor = node.walk();
148 for child in node.children(&mut cursor) {
149 self.extract_symbols_recursive(&child, source, symbols, parent);
150 }
151 }
152
153 fn extract_function(
154 &self,
155 node: &Node,
156 source: &str,
157 parent: Option<&str>,
158 ) -> Option<ExtractedSymbol> {
159 let name_node = node.child_by_field_name("name")?;
160 let name = node_text(&name_node, source).to_string();
161
162 let mut sym = ExtractedSymbol::new(
163 name,
164 SymbolKind::Function,
165 node.start_position().row + 1,
166 node.end_position().row + 1,
167 )
168 .with_columns(node.start_position().column, node.end_position().column);
169
170 let text = node_text(node, source);
172 if text.starts_with("async") {
173 sym = sym.async_fn();
174 }
175
176 if let Some(params) = node.child_by_field_name("parameters") {
178 self.extract_parameters(¶ms, source, &mut sym);
179 }
180
181 if let Some(ret_type) = node.child_by_field_name("return_type") {
183 sym.return_type = Some(
184 node_text(&ret_type, source)
185 .trim_start_matches(':')
186 .trim()
187 .to_string(),
188 );
189 }
190
191 sym.doc_comment = self.extract_doc_comment(node, source);
193
194 if let Some(p) = parent {
196 sym = sym.with_parent(p);
197 }
198
199 sym.signature = Some(self.build_function_signature(node, source));
201
202 Some(sym)
203 }
204
205 fn extract_variable_symbols(
206 &self,
207 node: &Node,
208 source: &str,
209 symbols: &mut Vec<ExtractedSymbol>,
210 parent: Option<&str>,
211 ) {
212 let mut cursor = node.walk();
213 for child in node.children(&mut cursor) {
214 if child.kind() == "variable_declarator" {
215 if let (Some(name_node), Some(value)) = (
216 child.child_by_field_name("name"),
217 child.child_by_field_name("value"),
218 ) {
219 let name = node_text(&name_node, source);
220 let value_kind = value.kind();
221
222 if value_kind == "arrow_function" || value_kind == "function_expression" {
224 let mut sym = ExtractedSymbol::new(
225 name.to_string(),
226 SymbolKind::Function,
227 node.start_position().row + 1,
228 node.end_position().row + 1,
229 );
230
231 let text = node_text(&value, source);
233 if text.starts_with("async") {
234 sym = sym.async_fn();
235 }
236
237 if let Some(params) = value.child_by_field_name("parameters") {
239 self.extract_parameters(¶ms, source, &mut sym);
240 }
241
242 if let Some(ret_type) = value.child_by_field_name("return_type") {
244 sym.return_type = Some(
245 node_text(&ret_type, source)
246 .trim_start_matches(':')
247 .trim()
248 .to_string(),
249 );
250 }
251
252 sym.doc_comment = self.extract_doc_comment(node, source);
253
254 if let Some(p) = parent {
255 sym = sym.with_parent(p);
256 }
257
258 if let Some(parent_node) = node.parent() {
260 if parent_node.kind() == "export_statement" {
261 sym = sym.exported();
262 }
263 }
264
265 symbols.push(sym);
266 } else {
267 let kind = if node_text(node, source).starts_with("const") {
269 SymbolKind::Constant
270 } else {
271 SymbolKind::Variable
272 };
273
274 let mut sym = ExtractedSymbol::new(
275 name.to_string(),
276 kind,
277 node.start_position().row + 1,
278 node.end_position().row + 1,
279 );
280
281 if let Some(type_ann) = child.child_by_field_name("type") {
283 sym.type_info = Some(
284 node_text(&type_ann, source)
285 .trim_start_matches(':')
286 .trim()
287 .to_string(),
288 );
289 }
290
291 if let Some(p) = parent {
292 sym = sym.with_parent(p);
293 }
294
295 symbols.push(sym);
296 }
297 }
298 }
299 }
300 }
301
302 fn extract_class(
303 &self,
304 node: &Node,
305 source: &str,
306 parent: Option<&str>,
307 ) -> Option<ExtractedSymbol> {
308 let name_node = node.child_by_field_name("name")?;
309 let name = node_text(&name_node, source).to_string();
310
311 let mut sym = ExtractedSymbol::new(
312 name,
313 SymbolKind::Class,
314 node.start_position().row + 1,
315 node.end_position().row + 1,
316 )
317 .with_columns(node.start_position().column, node.end_position().column);
318
319 if let Some(type_params) = node.child_by_field_name("type_parameters") {
321 self.extract_generics(&type_params, source, &mut sym);
322 }
323
324 sym.doc_comment = self.extract_doc_comment(node, source);
325
326 if let Some(p) = parent {
327 sym = sym.with_parent(p);
328 }
329
330 Some(sym)
331 }
332
333 fn extract_class_members(
334 &self,
335 body: &Node,
336 source: &str,
337 symbols: &mut Vec<ExtractedSymbol>,
338 class_name: Option<&str>,
339 ) {
340 let mut cursor = body.walk();
341 for child in body.children(&mut cursor) {
342 match child.kind() {
343 "method_definition" | "public_field_definition" => {
344 if let Some(sym) = self.extract_method(&child, source, class_name) {
345 symbols.push(sym);
346 }
347 }
348 "property_signature" => {
349 if let Some(sym) = self.extract_property(&child, source, class_name) {
350 symbols.push(sym);
351 }
352 }
353 _ => {}
354 }
355 }
356 }
357
358 fn extract_method(
359 &self,
360 node: &Node,
361 source: &str,
362 class_name: Option<&str>,
363 ) -> Option<ExtractedSymbol> {
364 let name_node = node.child_by_field_name("name")?;
365 let name = node_text(&name_node, source).to_string();
366
367 let mut sym = ExtractedSymbol::new(
368 name,
369 SymbolKind::Method,
370 node.start_position().row + 1,
371 node.end_position().row + 1,
372 );
373
374 let text = node_text(node, source);
376 if text.contains("private") {
377 sym.visibility = Visibility::Private;
378 } else if text.contains("protected") {
379 sym.visibility = Visibility::Protected;
380 }
381
382 if text.contains("static") {
384 sym = sym.static_fn();
385 }
386
387 if text.contains("async") {
389 sym = sym.async_fn();
390 }
391
392 if let Some(params) = node.child_by_field_name("parameters") {
394 self.extract_parameters(¶ms, source, &mut sym);
395 }
396
397 if let Some(ret_type) = node.child_by_field_name("return_type") {
399 sym.return_type = Some(
400 node_text(&ret_type, source)
401 .trim_start_matches(':')
402 .trim()
403 .to_string(),
404 );
405 }
406
407 sym.doc_comment = self.extract_doc_comment(node, source);
408
409 if let Some(p) = class_name {
410 sym = sym.with_parent(p);
411 }
412
413 Some(sym)
414 }
415
416 fn extract_property(
417 &self,
418 node: &Node,
419 source: &str,
420 class_name: Option<&str>,
421 ) -> Option<ExtractedSymbol> {
422 let name_node = node.child_by_field_name("name")?;
423 let name = node_text(&name_node, source).to_string();
424
425 let mut sym = ExtractedSymbol::new(
426 name,
427 SymbolKind::Property,
428 node.start_position().row + 1,
429 node.end_position().row + 1,
430 );
431
432 if let Some(type_node) = node.child_by_field_name("type") {
434 sym.type_info = Some(
435 node_text(&type_node, source)
436 .trim_start_matches(':')
437 .trim()
438 .to_string(),
439 );
440 }
441
442 if let Some(p) = class_name {
443 sym = sym.with_parent(p);
444 }
445
446 Some(sym)
447 }
448
449 fn extract_interface(
450 &self,
451 node: &Node,
452 source: &str,
453 parent: Option<&str>,
454 ) -> Option<ExtractedSymbol> {
455 let name_node = node.child_by_field_name("name")?;
456 let name = node_text(&name_node, source).to_string();
457
458 let mut sym = ExtractedSymbol::new(
459 name,
460 SymbolKind::Interface,
461 node.start_position().row + 1,
462 node.end_position().row + 1,
463 );
464
465 if let Some(type_params) = node.child_by_field_name("type_parameters") {
467 self.extract_generics(&type_params, source, &mut sym);
468 }
469
470 sym.doc_comment = self.extract_doc_comment(node, source);
471
472 if let Some(p) = parent {
473 sym = sym.with_parent(p);
474 }
475
476 Some(sym)
477 }
478
479 fn extract_type_alias(
480 &self,
481 node: &Node,
482 source: &str,
483 parent: Option<&str>,
484 ) -> Option<ExtractedSymbol> {
485 let name_node = node.child_by_field_name("name")?;
486 let name = node_text(&name_node, source).to_string();
487
488 let mut sym = ExtractedSymbol::new(
489 name,
490 SymbolKind::TypeAlias,
491 node.start_position().row + 1,
492 node.end_position().row + 1,
493 );
494
495 if let Some(type_value) = node.child_by_field_name("value") {
497 sym.type_info = Some(node_text(&type_value, source).to_string());
498 }
499
500 sym.doc_comment = self.extract_doc_comment(node, source);
501
502 if let Some(p) = parent {
503 sym = sym.with_parent(p);
504 }
505
506 Some(sym)
507 }
508
509 fn extract_enum(
510 &self,
511 node: &Node,
512 source: &str,
513 parent: Option<&str>,
514 ) -> Option<ExtractedSymbol> {
515 let name_node = node.child_by_field_name("name")?;
516 let name = node_text(&name_node, source).to_string();
517
518 let mut sym = ExtractedSymbol::new(
519 name,
520 SymbolKind::Enum,
521 node.start_position().row + 1,
522 node.end_position().row + 1,
523 );
524
525 sym.doc_comment = self.extract_doc_comment(node, source);
526
527 if let Some(p) = parent {
528 sym = sym.with_parent(p);
529 }
530
531 Some(sym)
532 }
533
534 fn extract_export_symbols(
535 &self,
536 node: &Node,
537 source: &str,
538 symbols: &mut Vec<ExtractedSymbol>,
539 parent: Option<&str>,
540 ) {
541 let mut cursor = node.walk();
542 for child in node.children(&mut cursor) {
543 match child.kind() {
544 "function_declaration" => {
545 if let Some(mut sym) = self.extract_function(&child, source, parent) {
546 sym = sym.exported();
547 symbols.push(sym);
548 }
549 }
550 "class_declaration" => {
551 if let Some(mut sym) = self.extract_class(&child, source, parent) {
552 sym = sym.exported();
553 let class_name = sym.name.clone();
554 symbols.push(sym);
555
556 if let Some(body) = child.child_by_field_name("body") {
557 self.extract_class_members(&body, source, symbols, Some(&class_name));
558 }
559 }
560 }
561 "interface_declaration" => {
562 if let Some(mut sym) = self.extract_interface(&child, source, parent) {
563 sym = sym.exported();
564 symbols.push(sym);
565 }
566 }
567 "type_alias_declaration" => {
568 if let Some(mut sym) = self.extract_type_alias(&child, source, parent) {
569 sym = sym.exported();
570 symbols.push(sym);
571 }
572 }
573 "lexical_declaration" | "variable_declaration" => {
574 self.extract_variable_symbols(&child, source, symbols, parent);
575 if let Some(last) = symbols.last_mut() {
577 last.exported = true;
578 }
579 }
580 _ => {}
581 }
582 }
583 }
584
585 fn extract_parameters(&self, params: &Node, source: &str, sym: &mut ExtractedSymbol) {
586 let mut cursor = params.walk();
587 for child in params.children(&mut cursor) {
588 match child.kind() {
589 "required_parameter" | "optional_parameter" => {
590 let is_optional = child.kind() == "optional_parameter";
591
592 let name = child
593 .child_by_field_name("pattern")
594 .or_else(|| child.child_by_field_name("name"))
595 .map(|n| node_text(&n, source).to_string())
596 .unwrap_or_default();
597
598 let type_info = child.child_by_field_name("type").map(|n| {
599 node_text(&n, source)
600 .trim_start_matches(':')
601 .trim()
602 .to_string()
603 });
604
605 let default_value = child
606 .child_by_field_name("value")
607 .map(|n| node_text(&n, source).to_string());
608
609 sym.add_parameter(Parameter {
610 name,
611 type_info,
612 default_value,
613 is_rest: false,
614 is_optional,
615 });
616 }
617 "rest_parameter" => {
618 let name = child
619 .child_by_field_name("pattern")
620 .or_else(|| child.child_by_field_name("name"))
621 .map(|n| node_text(&n, source).trim_start_matches("...").to_string())
622 .unwrap_or_default();
623
624 let type_info = child.child_by_field_name("type").map(|n| {
625 node_text(&n, source)
626 .trim_start_matches(':')
627 .trim()
628 .to_string()
629 });
630
631 sym.add_parameter(Parameter {
632 name,
633 type_info,
634 default_value: None,
635 is_rest: true,
636 is_optional: false,
637 });
638 }
639 _ => {}
640 }
641 }
642 }
643
644 fn extract_generics(&self, type_params: &Node, source: &str, sym: &mut ExtractedSymbol) {
645 let mut cursor = type_params.walk();
646 for child in type_params.children(&mut cursor) {
647 if child.kind() == "type_parameter" {
648 if let Some(name) = child.child_by_field_name("name") {
649 sym.add_generic(node_text(&name, source));
650 }
651 }
652 }
653 }
654
655 fn extract_imports_recursive(&self, node: &Node, source: &str, imports: &mut Vec<Import>) {
656 if node.kind() == "import_statement" {
657 if let Some(import) = self.parse_import(node, source) {
658 imports.push(import);
659 }
660 }
661
662 let mut cursor = node.walk();
663 for child in node.children(&mut cursor) {
664 self.extract_imports_recursive(&child, source, imports);
665 }
666 }
667
668 fn parse_import(&self, node: &Node, source: &str) -> Option<Import> {
669 let source_node = node.child_by_field_name("source")?;
670 let source_path = node_text(&source_node, source)
671 .trim_matches(|c| c == '"' || c == '\'')
672 .to_string();
673
674 let mut import = Import {
675 source: source_path,
676 names: Vec::new(),
677 is_default: false,
678 is_namespace: false,
679 line: node.start_position().row + 1,
680 };
681
682 let mut cursor = node.walk();
684 for child in node.children(&mut cursor) {
685 if child.kind() == "import_clause" {
686 self.parse_import_clause(&child, source, &mut import);
687 }
688 }
689
690 Some(import)
691 }
692
693 fn parse_import_clause(&self, clause: &Node, source: &str, import: &mut Import) {
694 let mut cursor = clause.walk();
695 for child in clause.children(&mut cursor) {
696 match child.kind() {
697 "identifier" => {
698 import.is_default = true;
700 import.names.push(ImportedName {
701 name: "default".to_string(),
702 alias: Some(node_text(&child, source).to_string()),
703 });
704 }
705 "namespace_import" => {
706 import.is_namespace = true;
708 if let Some(name_node) = child.child_by_field_name("name") {
709 import.names.push(ImportedName {
710 name: "*".to_string(),
711 alias: Some(node_text(&name_node, source).to_string()),
712 });
713 }
714 }
715 "named_imports" => {
716 self.parse_named_imports(&child, source, import);
718 }
719 _ => {}
720 }
721 }
722 }
723
724 fn parse_named_imports(&self, node: &Node, source: &str, import: &mut Import) {
725 let mut cursor = node.walk();
726 for child in node.children(&mut cursor) {
727 if child.kind() == "import_specifier" {
728 let name = child
729 .child_by_field_name("name")
730 .map(|n| node_text(&n, source).to_string())
731 .unwrap_or_default();
732
733 let alias = child
734 .child_by_field_name("alias")
735 .map(|n| node_text(&n, source).to_string());
736
737 import.names.push(ImportedName { name, alias });
738 }
739 }
740 }
741
742 fn extract_calls_recursive(
743 &self,
744 node: &Node,
745 source: &str,
746 calls: &mut Vec<FunctionCall>,
747 current_function: Option<&str>,
748 ) {
749 if node.kind() == "call_expression" {
750 if let Some(call) = self.parse_call(node, source, current_function) {
751 calls.push(call);
752 }
753 }
754
755 let func_name = match node.kind() {
757 "function_declaration" | "method_definition" => node
758 .child_by_field_name("name")
759 .map(|n| node_text(&n, source)),
760 _ => None,
761 };
762
763 let current = func_name
764 .map(String::from)
765 .or_else(|| current_function.map(String::from));
766
767 let mut cursor = node.walk();
768 for child in node.children(&mut cursor) {
769 self.extract_calls_recursive(&child, source, calls, current.as_deref());
770 }
771 }
772
773 fn parse_call(
774 &self,
775 node: &Node,
776 source: &str,
777 current_function: Option<&str>,
778 ) -> Option<FunctionCall> {
779 let function = node.child_by_field_name("function")?;
780
781 let (callee, is_method, receiver) = match function.kind() {
782 "member_expression" => {
783 let object = function
785 .child_by_field_name("object")
786 .map(|n| node_text(&n, source).to_string());
787 let property = function
788 .child_by_field_name("property")
789 .map(|n| node_text(&n, source).to_string())?;
790 (property, true, object)
791 }
792 "identifier" => {
793 (node_text(&function, source).to_string(), false, None)
795 }
796 _ => return None,
797 };
798
799 Some(FunctionCall {
800 caller: current_function.unwrap_or("<module>").to_string(),
801 callee,
802 line: node.start_position().row + 1,
803 is_method,
804 receiver,
805 })
806 }
807
808 fn build_function_signature(&self, node: &Node, source: &str) -> String {
809 let name = node
810 .child_by_field_name("name")
811 .map(|n| node_text(&n, source))
812 .unwrap_or("anonymous");
813
814 let params = node
815 .child_by_field_name("parameters")
816 .map(|n| node_text(&n, source))
817 .unwrap_or("()");
818
819 let return_type = node
820 .child_by_field_name("return_type")
821 .map(|n| node_text(&n, source))
822 .unwrap_or("");
823
824 format!("function {}{}{}", name, params, return_type)
825 }
826
827 fn clean_jsdoc(comment: &str) -> String {
828 comment
829 .trim_start_matches("/**")
830 .trim_end_matches("*/")
831 .lines()
832 .map(|line| line.trim().trim_start_matches('*').trim())
833 .filter(|line| !line.is_empty())
834 .collect::<Vec<_>>()
835 .join("\n")
836 }
837
838 fn find_named_exports(&self, node: &Node, source: &str) -> std::collections::HashSet<String> {
840 let mut exports = std::collections::HashSet::new();
841 self.collect_named_exports(node, source, &mut exports);
842 exports
843 }
844
845 fn collect_named_exports(
846 &self,
847 node: &Node,
848 source: &str,
849 exports: &mut std::collections::HashSet<String>,
850 ) {
851 if node.kind() == "export_statement" {
853 let mut cursor = node.walk();
854 for child in node.children(&mut cursor) {
855 if child.kind() == "export_clause" {
856 self.parse_export_clause(&child, source, exports);
857 }
858 }
859 }
860
861 let mut cursor = node.walk();
863 for child in node.children(&mut cursor) {
864 self.collect_named_exports(&child, source, exports);
865 }
866 }
867
868 fn parse_export_clause(
869 &self,
870 node: &Node,
871 source: &str,
872 exports: &mut std::collections::HashSet<String>,
873 ) {
874 let mut cursor = node.walk();
875 for child in node.children(&mut cursor) {
876 if child.kind() == "export_specifier" {
878 if let Some(name_node) = child.child_by_field_name("name") {
880 let name = node_text(&name_node, source).to_string();
881 exports.insert(name);
882 } else {
883 let mut inner_cursor = child.walk();
885 for inner_child in child.children(&mut inner_cursor) {
886 if inner_child.kind() == "identifier" {
887 let name = node_text(&inner_child, source).to_string();
888 exports.insert(name);
889 break;
890 }
891 }
892 }
893 }
894 }
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 fn parse_ts(source: &str) -> (Tree, String) {
903 let mut parser = tree_sitter::Parser::new();
904 parser
905 .set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
906 .unwrap();
907 let tree = parser.parse(source, None).unwrap();
908 (tree, source.to_string())
909 }
910
911 #[test]
912 fn test_extract_function() {
913 let source = r#"
914function greet(name: string): string {
915 return `Hello, ${name}!`;
916}
917"#;
918 let (tree, src) = parse_ts(source);
919 let extractor = TypeScriptExtractor;
920 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
921
922 assert_eq!(symbols.len(), 1);
923 assert_eq!(symbols[0].name, "greet");
924 assert_eq!(symbols[0].kind, SymbolKind::Function);
925 assert_eq!(symbols[0].parameters.len(), 1);
926 assert_eq!(symbols[0].parameters[0].name, "name");
927 }
928
929 #[test]
930 fn test_extract_class() {
931 let source = r#"
932class UserService {
933 private name: string;
934
935 constructor(name: string) {
936 this.name = name;
937 }
938
939 public greet(): string {
940 return `Hello, ${this.name}!`;
941 }
942}
943"#;
944 let (tree, src) = parse_ts(source);
945 let extractor = TypeScriptExtractor;
946 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
947
948 assert!(symbols
950 .iter()
951 .any(|s| s.name == "UserService" && s.kind == SymbolKind::Class));
952 assert!(symbols
953 .iter()
954 .any(|s| s.name == "greet" && s.kind == SymbolKind::Method));
955 }
956
957 #[test]
958 fn test_extract_interface() {
959 let source = r#"
960interface User<T> {
961 name: string;
962 age: number;
963 data: T;
964}
965"#;
966 let (tree, src) = parse_ts(source);
967 let extractor = TypeScriptExtractor;
968 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
969
970 assert_eq!(symbols.len(), 1);
971 assert_eq!(symbols[0].name, "User");
972 assert_eq!(symbols[0].kind, SymbolKind::Interface);
973 assert!(symbols[0].generics.contains(&"T".to_string()));
974 }
975
976 #[test]
977 fn test_extract_arrow_function() {
978 let source = r#"
979const add = (a: number, b: number): number => a + b;
980"#;
981 let (tree, src) = parse_ts(source);
982 let extractor = TypeScriptExtractor;
983 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
984
985 assert_eq!(symbols.len(), 1);
986 assert_eq!(symbols[0].name, "add");
987 assert_eq!(symbols[0].kind, SymbolKind::Function);
988 assert_eq!(symbols[0].parameters.len(), 2);
989 }
990
991 #[test]
992 fn test_extract_imports() {
993 let source = r#"
994import { foo, bar as baz } from './module';
995import * as utils from 'utils';
996import defaultExport from './default';
997"#;
998 let (tree, src) = parse_ts(source);
999 let extractor = TypeScriptExtractor;
1000 let imports = extractor.extract_imports(&tree, &src).unwrap();
1001
1002 assert_eq!(imports.len(), 3);
1003 assert_eq!(imports[0].source, "./module");
1004 assert_eq!(imports[1].source, "utils");
1005 assert!(imports[1].is_namespace);
1006 assert_eq!(imports[2].source, "./default");
1007 assert!(imports[2].is_default);
1008 }
1009
1010 #[test]
1011 fn test_named_export_clause() {
1012 let source = r#"
1014function Button() {
1015 return <button>Click me</button>;
1016}
1017
1018const buttonVariants = {};
1019
1020export { Button, buttonVariants };
1021"#;
1022 let (tree, src) = parse_ts(source);
1023 let extractor = TypeScriptExtractor;
1024 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
1025
1026 let button = symbols
1028 .iter()
1029 .find(|s| s.name == "Button")
1030 .expect("Button not found");
1031 assert!(button.exported, "Button should be marked as exported");
1032
1033 let variants = symbols
1035 .iter()
1036 .find(|s| s.name == "buttonVariants")
1037 .expect("buttonVariants not found");
1038 assert!(
1039 variants.exported,
1040 "buttonVariants should be marked as exported"
1041 );
1042 }
1043}