acp/ast/languages/
typescript.rs

1//! @acp:module "TypeScript Extractor"
2//! @acp:summary "Symbol extraction for TypeScript source files"
3//! @acp:domain cli
4//! @acp:layer parsing
5
6use 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
13/// TypeScript language extractor
14pub 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        // Second pass: mark symbols that are exported via `export { name1, name2 }` clauses
35        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        // Look for comment nodes before this node
66        if let Some(prev) = node.prev_sibling() {
67            if prev.kind() == "comment" {
68                let comment = node_text(&prev, source);
69                // Check for JSDoc style comments
70                if comment.starts_with("/**") {
71                    return Some(Self::clean_jsdoc(comment));
72                }
73                // Or regular // comments
74                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 declarations
93            "function_declaration" => {
94                if let Some(sym) = self.extract_function(node, source, parent) {
95                    symbols.push(sym);
96                }
97            }
98
99            // Arrow functions assigned to const/let
100            "lexical_declaration" | "variable_declaration" => {
101                self.extract_variable_symbols(node, source, symbols, parent);
102            }
103
104            // Class declarations
105            "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                    // Extract class body
111                    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 declarations
118            "interface_declaration" => {
119                if let Some(sym) = self.extract_interface(node, source, parent) {
120                    symbols.push(sym);
121                }
122            }
123
124            // Type alias declarations
125            "type_alias_declaration" => {
126                if let Some(sym) = self.extract_type_alias(node, source, parent) {
127                    symbols.push(sym);
128                }
129            }
130
131            // Enum declarations
132            "enum_declaration" => {
133                if let Some(sym) = self.extract_enum(node, source, parent) {
134                    symbols.push(sym);
135                }
136            }
137
138            // Export statements
139            "export_statement" => {
140                self.extract_export_symbols(node, source, symbols, parent);
141            }
142
143            _ => {}
144        }
145
146        // Recurse into children
147        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        // Check for async
171        let text = node_text(node, source);
172        if text.starts_with("async") {
173            sym = sym.async_fn();
174        }
175
176        // Extract parameters
177        if let Some(params) = node.child_by_field_name("parameters") {
178            self.extract_parameters(&params, source, &mut sym);
179        }
180
181        // Extract return type
182        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        // Extract doc comment
192        sym.doc_comment = self.extract_doc_comment(node, source);
193
194        // Set parent
195        if let Some(p) = parent {
196            sym = sym.with_parent(p);
197        }
198
199        // Build signature
200        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                    // Check if this is an arrow function or function expression
223                    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                        // Check for async
232                        let text = node_text(&value, source);
233                        if text.starts_with("async") {
234                            sym = sym.async_fn();
235                        }
236
237                        // Extract parameters
238                        if let Some(params) = value.child_by_field_name("parameters") {
239                            self.extract_parameters(&params, source, &mut sym);
240                        }
241
242                        // Extract return type
243                        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                        // Check if exported
259                        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                        // Regular variable/constant
268                        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                        // Extract type annotation
282                        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        // Extract generics
320        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        // Check visibility modifiers
375        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        // Check for static
383        if text.contains("static") {
384            sym = sym.static_fn();
385        }
386
387        // Check for async
388        if text.contains("async") {
389            sym = sym.async_fn();
390        }
391
392        // Extract parameters
393        if let Some(params) = node.child_by_field_name("parameters") {
394            self.extract_parameters(&params, source, &mut sym);
395        }
396
397        // Extract return type
398        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        // Extract type
433        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        // Extract generics
466        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        // Extract the type value
496        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                    // Mark as exported
576                    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        // Parse import clause
683        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                    // Default import
699                    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 * as foo
707                    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                    // import { foo, bar as baz }
717                    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        // Track current function for nested calls
756        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                // Method call: obj.method()
784                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                // Direct function call: foo()
794                (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    /// Find all names exported via `export { name1, name2 }` clauses
839    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        // Handle export statements with export_clause (e.g., `export { Button, Card as CardComponent }`)
852        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        // Recurse into children
862        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            // export_specifier: name, or name as alias
877            if child.kind() == "export_specifier" {
878                // Get the local name (the original identifier, not the alias)
879                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                    // Fallback: get the first identifier
884                    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        // Should have: class, constructor, greet method
949        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        // Test that symbols exported via `export { name }` are marked as exported
1013        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        // Button should be marked as exported
1027        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        // buttonVariants should be marked as exported
1034        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}