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 sym.definition_start_line = Some(self.find_definition_start_line(node, source));
204
205 Some(sym)
206 }
207
208 fn extract_variable_symbols(
209 &self,
210 node: &Node,
211 source: &str,
212 symbols: &mut Vec<ExtractedSymbol>,
213 parent: Option<&str>,
214 ) {
215 let mut cursor = node.walk();
216 for child in node.children(&mut cursor) {
217 if child.kind() == "variable_declarator" {
218 if let (Some(name_node), Some(value)) = (
219 child.child_by_field_name("name"),
220 child.child_by_field_name("value"),
221 ) {
222 let name = node_text(&name_node, source);
223 let value_kind = value.kind();
224
225 if value_kind == "arrow_function" || value_kind == "function_expression" {
227 let mut sym = ExtractedSymbol::new(
228 name.to_string(),
229 SymbolKind::Function,
230 node.start_position().row + 1,
231 node.end_position().row + 1,
232 );
233
234 let text = node_text(&value, source);
236 if text.starts_with("async") {
237 sym = sym.async_fn();
238 }
239
240 if let Some(params) = value.child_by_field_name("parameters") {
242 self.extract_parameters(¶ms, source, &mut sym);
243 }
244
245 if let Some(ret_type) = value.child_by_field_name("return_type") {
247 sym.return_type = Some(
248 node_text(&ret_type, source)
249 .trim_start_matches(':')
250 .trim()
251 .to_string(),
252 );
253 }
254
255 sym.doc_comment = self.extract_doc_comment(node, source);
256
257 if let Some(p) = parent {
258 sym = sym.with_parent(p);
259 }
260
261 if let Some(parent_node) = node.parent() {
263 if parent_node.kind() == "export_statement" {
264 sym = sym.exported();
265 }
266 }
267
268 sym.definition_start_line =
270 Some(self.find_definition_start_line(node, source));
271
272 symbols.push(sym);
273 } else {
274 let kind = if node_text(node, source).starts_with("const") {
276 SymbolKind::Constant
277 } else {
278 SymbolKind::Variable
279 };
280
281 let mut sym = ExtractedSymbol::new(
282 name.to_string(),
283 kind,
284 node.start_position().row + 1,
285 node.end_position().row + 1,
286 );
287
288 if let Some(type_ann) = child.child_by_field_name("type") {
290 sym.type_info = Some(
291 node_text(&type_ann, source)
292 .trim_start_matches(':')
293 .trim()
294 .to_string(),
295 );
296 }
297
298 if let Some(p) = parent {
299 sym = sym.with_parent(p);
300 }
301
302 sym.definition_start_line =
304 Some(self.find_definition_start_line(node, source));
305
306 symbols.push(sym);
307 }
308 }
309 }
310 }
311 }
312
313 fn extract_class(
314 &self,
315 node: &Node,
316 source: &str,
317 parent: Option<&str>,
318 ) -> Option<ExtractedSymbol> {
319 let name_node = node.child_by_field_name("name")?;
320 let name = node_text(&name_node, source).to_string();
321
322 let mut sym = ExtractedSymbol::new(
323 name,
324 SymbolKind::Class,
325 node.start_position().row + 1,
326 node.end_position().row + 1,
327 )
328 .with_columns(node.start_position().column, node.end_position().column);
329
330 if let Some(type_params) = node.child_by_field_name("type_parameters") {
332 self.extract_generics(&type_params, source, &mut sym);
333 }
334
335 sym.doc_comment = self.extract_doc_comment(node, source);
336
337 if let Some(p) = parent {
338 sym = sym.with_parent(p);
339 }
340
341 sym.definition_start_line = Some(self.find_definition_start_line(node, source));
343
344 Some(sym)
345 }
346
347 fn extract_class_members(
348 &self,
349 body: &Node,
350 source: &str,
351 symbols: &mut Vec<ExtractedSymbol>,
352 class_name: Option<&str>,
353 ) {
354 let mut cursor = body.walk();
355 for child in body.children(&mut cursor) {
356 match child.kind() {
357 "method_definition" | "public_field_definition" => {
358 if let Some(sym) = self.extract_method(&child, source, class_name) {
359 symbols.push(sym);
360 }
361 }
362 "property_signature" => {
363 if let Some(sym) = self.extract_property(&child, source, class_name) {
364 symbols.push(sym);
365 }
366 }
367 _ => {}
368 }
369 }
370 }
371
372 fn extract_method(
373 &self,
374 node: &Node,
375 source: &str,
376 class_name: Option<&str>,
377 ) -> Option<ExtractedSymbol> {
378 let name_node = node.child_by_field_name("name")?;
379 let name = node_text(&name_node, source).to_string();
380
381 let mut sym = ExtractedSymbol::new(
382 name,
383 SymbolKind::Method,
384 node.start_position().row + 1,
385 node.end_position().row + 1,
386 );
387
388 let text = node_text(node, source);
390 if text.contains("private") {
391 sym.visibility = Visibility::Private;
392 } else if text.contains("protected") {
393 sym.visibility = Visibility::Protected;
394 }
395
396 if text.contains("static") {
398 sym = sym.static_fn();
399 }
400
401 if text.contains("async") {
403 sym = sym.async_fn();
404 }
405
406 if let Some(params) = node.child_by_field_name("parameters") {
408 self.extract_parameters(¶ms, source, &mut sym);
409 }
410
411 if let Some(ret_type) = node.child_by_field_name("return_type") {
413 sym.return_type = Some(
414 node_text(&ret_type, source)
415 .trim_start_matches(':')
416 .trim()
417 .to_string(),
418 );
419 }
420
421 sym.doc_comment = self.extract_doc_comment(node, source);
422
423 if let Some(p) = class_name {
424 sym = sym.with_parent(p);
425 }
426
427 sym.definition_start_line = Some(self.find_definition_start_line(node, source));
429
430 Some(sym)
431 }
432
433 fn extract_property(
434 &self,
435 node: &Node,
436 source: &str,
437 class_name: Option<&str>,
438 ) -> Option<ExtractedSymbol> {
439 let name_node = node.child_by_field_name("name")?;
440 let name = node_text(&name_node, source).to_string();
441
442 let mut sym = ExtractedSymbol::new(
443 name,
444 SymbolKind::Property,
445 node.start_position().row + 1,
446 node.end_position().row + 1,
447 );
448
449 if let Some(type_node) = node.child_by_field_name("type") {
451 sym.type_info = Some(
452 node_text(&type_node, source)
453 .trim_start_matches(':')
454 .trim()
455 .to_string(),
456 );
457 }
458
459 if let Some(p) = class_name {
460 sym = sym.with_parent(p);
461 }
462
463 sym.definition_start_line = Some(self.find_definition_start_line(node, source));
465
466 Some(sym)
467 }
468
469 fn extract_interface(
470 &self,
471 node: &Node,
472 source: &str,
473 parent: Option<&str>,
474 ) -> Option<ExtractedSymbol> {
475 let name_node = node.child_by_field_name("name")?;
476 let name = node_text(&name_node, source).to_string();
477
478 let mut sym = ExtractedSymbol::new(
479 name,
480 SymbolKind::Interface,
481 node.start_position().row + 1,
482 node.end_position().row + 1,
483 );
484
485 if let Some(type_params) = node.child_by_field_name("type_parameters") {
487 self.extract_generics(&type_params, source, &mut sym);
488 }
489
490 sym.doc_comment = self.extract_doc_comment(node, source);
491
492 if let Some(p) = parent {
493 sym = sym.with_parent(p);
494 }
495
496 sym.definition_start_line = Some(self.find_definition_start_line(node, source));
498
499 Some(sym)
500 }
501
502 fn extract_type_alias(
503 &self,
504 node: &Node,
505 source: &str,
506 parent: Option<&str>,
507 ) -> Option<ExtractedSymbol> {
508 let name_node = node.child_by_field_name("name")?;
509 let name = node_text(&name_node, source).to_string();
510
511 let mut sym = ExtractedSymbol::new(
512 name,
513 SymbolKind::TypeAlias,
514 node.start_position().row + 1,
515 node.end_position().row + 1,
516 );
517
518 if let Some(type_value) = node.child_by_field_name("value") {
520 sym.type_info = Some(node_text(&type_value, source).to_string());
521 }
522
523 sym.doc_comment = self.extract_doc_comment(node, source);
524
525 if let Some(p) = parent {
526 sym = sym.with_parent(p);
527 }
528
529 sym.definition_start_line = Some(self.find_definition_start_line(node, source));
531
532 Some(sym)
533 }
534
535 fn extract_enum(
536 &self,
537 node: &Node,
538 source: &str,
539 parent: Option<&str>,
540 ) -> Option<ExtractedSymbol> {
541 let name_node = node.child_by_field_name("name")?;
542 let name = node_text(&name_node, source).to_string();
543
544 let mut sym = ExtractedSymbol::new(
545 name,
546 SymbolKind::Enum,
547 node.start_position().row + 1,
548 node.end_position().row + 1,
549 );
550
551 sym.doc_comment = self.extract_doc_comment(node, source);
552
553 if let Some(p) = parent {
554 sym = sym.with_parent(p);
555 }
556
557 sym.definition_start_line = Some(self.find_definition_start_line(node, source));
559
560 Some(sym)
561 }
562
563 fn extract_export_symbols(
564 &self,
565 node: &Node,
566 source: &str,
567 symbols: &mut Vec<ExtractedSymbol>,
568 parent: Option<&str>,
569 ) {
570 let mut cursor = node.walk();
571 for child in node.children(&mut cursor) {
572 match child.kind() {
573 "function_declaration" => {
574 if let Some(mut sym) = self.extract_function(&child, source, parent) {
575 sym = sym.exported();
576 symbols.push(sym);
577 }
578 }
579 "class_declaration" => {
580 if let Some(mut sym) = self.extract_class(&child, source, parent) {
581 sym = sym.exported();
582 let class_name = sym.name.clone();
583 symbols.push(sym);
584
585 if let Some(body) = child.child_by_field_name("body") {
586 self.extract_class_members(&body, source, symbols, Some(&class_name));
587 }
588 }
589 }
590 "interface_declaration" => {
591 if let Some(mut sym) = self.extract_interface(&child, source, parent) {
592 sym = sym.exported();
593 symbols.push(sym);
594 }
595 }
596 "type_alias_declaration" => {
597 if let Some(mut sym) = self.extract_type_alias(&child, source, parent) {
598 sym = sym.exported();
599 symbols.push(sym);
600 }
601 }
602 "lexical_declaration" | "variable_declaration" => {
603 self.extract_variable_symbols(&child, source, symbols, parent);
604 if let Some(last) = symbols.last_mut() {
606 last.exported = true;
607 }
608 }
609 _ => {}
610 }
611 }
612 }
613
614 fn extract_parameters(&self, params: &Node, source: &str, sym: &mut ExtractedSymbol) {
615 let mut cursor = params.walk();
616 for child in params.children(&mut cursor) {
617 match child.kind() {
618 "required_parameter" | "optional_parameter" => {
619 let is_optional = child.kind() == "optional_parameter";
620
621 let name = child
622 .child_by_field_name("pattern")
623 .or_else(|| child.child_by_field_name("name"))
624 .map(|n| node_text(&n, source).to_string())
625 .unwrap_or_default();
626
627 let type_info = child.child_by_field_name("type").map(|n| {
628 node_text(&n, source)
629 .trim_start_matches(':')
630 .trim()
631 .to_string()
632 });
633
634 let default_value = child
635 .child_by_field_name("value")
636 .map(|n| node_text(&n, source).to_string());
637
638 sym.add_parameter(Parameter {
639 name,
640 type_info,
641 default_value,
642 is_rest: false,
643 is_optional,
644 });
645 }
646 "rest_parameter" => {
647 let name = child
648 .child_by_field_name("pattern")
649 .or_else(|| child.child_by_field_name("name"))
650 .map(|n| node_text(&n, source).trim_start_matches("...").to_string())
651 .unwrap_or_default();
652
653 let type_info = child.child_by_field_name("type").map(|n| {
654 node_text(&n, source)
655 .trim_start_matches(':')
656 .trim()
657 .to_string()
658 });
659
660 sym.add_parameter(Parameter {
661 name,
662 type_info,
663 default_value: None,
664 is_rest: true,
665 is_optional: false,
666 });
667 }
668 _ => {}
669 }
670 }
671 }
672
673 fn extract_generics(&self, type_params: &Node, source: &str, sym: &mut ExtractedSymbol) {
674 let mut cursor = type_params.walk();
675 for child in type_params.children(&mut cursor) {
676 if child.kind() == "type_parameter" {
677 if let Some(name) = child.child_by_field_name("name") {
678 sym.add_generic(node_text(&name, source));
679 }
680 }
681 }
682 }
683
684 fn extract_imports_recursive(&self, node: &Node, source: &str, imports: &mut Vec<Import>) {
685 if node.kind() == "import_statement" {
686 if let Some(import) = self.parse_import(node, source) {
687 imports.push(import);
688 }
689 }
690
691 let mut cursor = node.walk();
692 for child in node.children(&mut cursor) {
693 self.extract_imports_recursive(&child, source, imports);
694 }
695 }
696
697 fn parse_import(&self, node: &Node, source: &str) -> Option<Import> {
698 let source_node = node.child_by_field_name("source")?;
699 let source_path = node_text(&source_node, source)
700 .trim_matches(|c| c == '"' || c == '\'')
701 .to_string();
702
703 let mut import = Import {
704 source: source_path,
705 names: Vec::new(),
706 is_default: false,
707 is_namespace: false,
708 line: node.start_position().row + 1,
709 };
710
711 let mut cursor = node.walk();
713 for child in node.children(&mut cursor) {
714 if child.kind() == "import_clause" {
715 self.parse_import_clause(&child, source, &mut import);
716 }
717 }
718
719 Some(import)
720 }
721
722 fn parse_import_clause(&self, clause: &Node, source: &str, import: &mut Import) {
723 let mut cursor = clause.walk();
724 for child in clause.children(&mut cursor) {
725 match child.kind() {
726 "identifier" => {
727 import.is_default = true;
729 import.names.push(ImportedName {
730 name: "default".to_string(),
731 alias: Some(node_text(&child, source).to_string()),
732 });
733 }
734 "namespace_import" => {
735 import.is_namespace = true;
737 if let Some(name_node) = child.child_by_field_name("name") {
738 import.names.push(ImportedName {
739 name: "*".to_string(),
740 alias: Some(node_text(&name_node, source).to_string()),
741 });
742 }
743 }
744 "named_imports" => {
745 self.parse_named_imports(&child, source, import);
747 }
748 _ => {}
749 }
750 }
751 }
752
753 fn parse_named_imports(&self, node: &Node, source: &str, import: &mut Import) {
754 let mut cursor = node.walk();
755 for child in node.children(&mut cursor) {
756 if child.kind() == "import_specifier" {
757 let name = child
758 .child_by_field_name("name")
759 .map(|n| node_text(&n, source).to_string())
760 .unwrap_or_default();
761
762 let alias = child
763 .child_by_field_name("alias")
764 .map(|n| node_text(&n, source).to_string());
765
766 import.names.push(ImportedName { name, alias });
767 }
768 }
769 }
770
771 fn extract_calls_recursive(
772 &self,
773 node: &Node,
774 source: &str,
775 calls: &mut Vec<FunctionCall>,
776 current_function: Option<&str>,
777 ) {
778 if node.kind() == "call_expression" {
779 if let Some(call) = self.parse_call(node, source, current_function) {
780 calls.push(call);
781 }
782 }
783
784 let func_name = match node.kind() {
786 "function_declaration" | "method_definition" => node
787 .child_by_field_name("name")
788 .map(|n| node_text(&n, source)),
789 _ => None,
790 };
791
792 let current = func_name
793 .map(String::from)
794 .or_else(|| current_function.map(String::from));
795
796 let mut cursor = node.walk();
797 for child in node.children(&mut cursor) {
798 self.extract_calls_recursive(&child, source, calls, current.as_deref());
799 }
800 }
801
802 fn parse_call(
803 &self,
804 node: &Node,
805 source: &str,
806 current_function: Option<&str>,
807 ) -> Option<FunctionCall> {
808 let function = node.child_by_field_name("function")?;
809
810 let (callee, is_method, receiver) = match function.kind() {
811 "member_expression" => {
812 let object = function
814 .child_by_field_name("object")
815 .map(|n| node_text(&n, source).to_string());
816 let property = function
817 .child_by_field_name("property")
818 .map(|n| node_text(&n, source).to_string())?;
819 (property, true, object)
820 }
821 "identifier" => {
822 (node_text(&function, source).to_string(), false, None)
824 }
825 _ => return None,
826 };
827
828 Some(FunctionCall {
829 caller: current_function.unwrap_or("<module>").to_string(),
830 callee,
831 line: node.start_position().row + 1,
832 is_method,
833 receiver,
834 })
835 }
836
837 fn build_function_signature(&self, node: &Node, source: &str) -> String {
838 let name = node
839 .child_by_field_name("name")
840 .map(|n| node_text(&n, source))
841 .unwrap_or("anonymous");
842
843 let params = node
844 .child_by_field_name("parameters")
845 .map(|n| node_text(&n, source))
846 .unwrap_or("()");
847
848 let return_type = node
849 .child_by_field_name("return_type")
850 .map(|n| node_text(&n, source))
851 .unwrap_or("");
852
853 format!("function {}{}{}", name, params, return_type)
854 }
855
856 fn clean_jsdoc(comment: &str) -> String {
857 comment
858 .trim_start_matches("/**")
859 .trim_end_matches("*/")
860 .lines()
861 .map(|line| line.trim().trim_start_matches('*').trim())
862 .filter(|line| !line.is_empty())
863 .collect::<Vec<_>>()
864 .join("\n")
865 }
866
867 fn find_definition_start_line(&self, node: &Node, source: &str) -> usize {
870 let node_start = node.start_position().row + 1;
871 let mut earliest_line = node_start;
872
873 let mut current = node.prev_sibling();
875 while let Some(prev) = current {
876 match prev.kind() {
877 "decorator" => {
878 earliest_line = prev.start_position().row + 1;
880 current = prev.prev_sibling();
881 }
882 "comment" => {
883 let comment = node_text(&prev, source);
884 if comment.starts_with("/**") {
886 earliest_line = prev.start_position().row + 1;
887 current = prev.prev_sibling();
888 } else {
889 break;
891 }
892 }
893 _ => break,
894 }
895 }
896
897 earliest_line
898 }
899
900 fn find_named_exports(&self, node: &Node, source: &str) -> std::collections::HashSet<String> {
902 let mut exports = std::collections::HashSet::new();
903 self.collect_named_exports(node, source, &mut exports);
904 exports
905 }
906
907 fn collect_named_exports(
908 &self,
909 node: &Node,
910 source: &str,
911 exports: &mut std::collections::HashSet<String>,
912 ) {
913 if node.kind() == "export_statement" {
915 let mut cursor = node.walk();
916 for child in node.children(&mut cursor) {
917 if child.kind() == "export_clause" {
918 self.parse_export_clause(&child, source, exports);
919 }
920 }
921 }
922
923 let mut cursor = node.walk();
925 for child in node.children(&mut cursor) {
926 self.collect_named_exports(&child, source, exports);
927 }
928 }
929
930 fn parse_export_clause(
931 &self,
932 node: &Node,
933 source: &str,
934 exports: &mut std::collections::HashSet<String>,
935 ) {
936 let mut cursor = node.walk();
937 for child in node.children(&mut cursor) {
938 if child.kind() == "export_specifier" {
940 if let Some(name_node) = child.child_by_field_name("name") {
942 let name = node_text(&name_node, source).to_string();
943 exports.insert(name);
944 } else {
945 let mut inner_cursor = child.walk();
947 for inner_child in child.children(&mut inner_cursor) {
948 if inner_child.kind() == "identifier" {
949 let name = node_text(&inner_child, source).to_string();
950 exports.insert(name);
951 break;
952 }
953 }
954 }
955 }
956 }
957 }
958}
959
960#[cfg(test)]
961mod tests {
962 use super::*;
963
964 fn parse_ts(source: &str) -> (Tree, String) {
965 let mut parser = tree_sitter::Parser::new();
966 parser
967 .set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
968 .unwrap();
969 let tree = parser.parse(source, None).unwrap();
970 (tree, source.to_string())
971 }
972
973 #[test]
974 fn test_extract_function() {
975 let source = r#"
976function greet(name: string): string {
977 return `Hello, ${name}!`;
978}
979"#;
980 let (tree, src) = parse_ts(source);
981 let extractor = TypeScriptExtractor;
982 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
983
984 assert_eq!(symbols.len(), 1);
985 assert_eq!(symbols[0].name, "greet");
986 assert_eq!(symbols[0].kind, SymbolKind::Function);
987 assert_eq!(symbols[0].parameters.len(), 1);
988 assert_eq!(symbols[0].parameters[0].name, "name");
989 }
990
991 #[test]
992 fn test_extract_class() {
993 let source = r#"
994class UserService {
995 private name: string;
996
997 constructor(name: string) {
998 this.name = name;
999 }
1000
1001 public greet(): string {
1002 return `Hello, ${this.name}!`;
1003 }
1004}
1005"#;
1006 let (tree, src) = parse_ts(source);
1007 let extractor = TypeScriptExtractor;
1008 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
1009
1010 assert!(symbols
1012 .iter()
1013 .any(|s| s.name == "UserService" && s.kind == SymbolKind::Class));
1014 assert!(symbols
1015 .iter()
1016 .any(|s| s.name == "greet" && s.kind == SymbolKind::Method));
1017 }
1018
1019 #[test]
1020 fn test_extract_interface() {
1021 let source = r#"
1022interface User<T> {
1023 name: string;
1024 age: number;
1025 data: T;
1026}
1027"#;
1028 let (tree, src) = parse_ts(source);
1029 let extractor = TypeScriptExtractor;
1030 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
1031
1032 assert_eq!(symbols.len(), 1);
1033 assert_eq!(symbols[0].name, "User");
1034 assert_eq!(symbols[0].kind, SymbolKind::Interface);
1035 assert!(symbols[0].generics.contains(&"T".to_string()));
1036 }
1037
1038 #[test]
1039 fn test_extract_arrow_function() {
1040 let source = r#"
1041const add = (a: number, b: number): number => a + b;
1042"#;
1043 let (tree, src) = parse_ts(source);
1044 let extractor = TypeScriptExtractor;
1045 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
1046
1047 assert_eq!(symbols.len(), 1);
1048 assert_eq!(symbols[0].name, "add");
1049 assert_eq!(symbols[0].kind, SymbolKind::Function);
1050 assert_eq!(symbols[0].parameters.len(), 2);
1051 }
1052
1053 #[test]
1054 fn test_extract_imports() {
1055 let source = r#"
1056import { foo, bar as baz } from './module';
1057import * as utils from 'utils';
1058import defaultExport from './default';
1059"#;
1060 let (tree, src) = parse_ts(source);
1061 let extractor = TypeScriptExtractor;
1062 let imports = extractor.extract_imports(&tree, &src).unwrap();
1063
1064 assert_eq!(imports.len(), 3);
1065 assert_eq!(imports[0].source, "./module");
1066 assert_eq!(imports[1].source, "utils");
1067 assert!(imports[1].is_namespace);
1068 assert_eq!(imports[2].source, "./default");
1069 assert!(imports[2].is_default);
1070 }
1071
1072 #[test]
1073 fn test_named_export_clause() {
1074 let source = r#"
1076function Button() {
1077 return <button>Click me</button>;
1078}
1079
1080const buttonVariants = {};
1081
1082export { Button, buttonVariants };
1083"#;
1084 let (tree, src) = parse_ts(source);
1085 let extractor = TypeScriptExtractor;
1086 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
1087
1088 let button = symbols
1090 .iter()
1091 .find(|s| s.name == "Button")
1092 .expect("Button not found");
1093 assert!(button.exported, "Button should be marked as exported");
1094
1095 let variants = symbols
1097 .iter()
1098 .find(|s| s.name == "buttonVariants")
1099 .expect("buttonVariants not found");
1100 assert!(
1101 variants.exported,
1102 "buttonVariants should be marked as exported"
1103 );
1104 }
1105
1106 #[test]
1107 fn test_definition_start_line_with_jsdoc() {
1108 let source = r#"
1109/**
1110 * A greeting function
1111 * @param name The name to greet
1112 */
1113function greet(name: string): string {
1114 return `Hello, ${name}!`;
1115}
1116"#;
1117 let (tree, src) = parse_ts(source);
1118 let extractor = TypeScriptExtractor;
1119 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
1120
1121 let func = symbols.iter().find(|s| s.name == "greet").unwrap();
1122 assert_eq!(func.start_line, 6);
1124 assert_eq!(func.definition_start_line, Some(2));
1125 }
1126
1127 #[test]
1128 fn test_definition_start_line_no_decorations() {
1129 let source = r#"
1130function simple(): void {}
1131"#;
1132 let (tree, src) = parse_ts(source);
1133 let extractor = TypeScriptExtractor;
1134 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
1135
1136 let func = symbols.iter().find(|s| s.name == "simple").unwrap();
1137 assert_eq!(func.start_line, 2);
1139 assert_eq!(func.definition_start_line, Some(2));
1140 }
1141}