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        // Set definition_start_line (before decorators/doc comments)
203        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                    // Check if this is an arrow function or function expression
226                    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                        // Check for async
235                        let text = node_text(&value, source);
236                        if text.starts_with("async") {
237                            sym = sym.async_fn();
238                        }
239
240                        // Extract parameters
241                        if let Some(params) = value.child_by_field_name("parameters") {
242                            self.extract_parameters(&params, source, &mut sym);
243                        }
244
245                        // Extract return type
246                        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                        // Check if exported
262                        if let Some(parent_node) = node.parent() {
263                            if parent_node.kind() == "export_statement" {
264                                sym = sym.exported();
265                            }
266                        }
267
268                        // Set definition_start_line (before decorators/doc comments)
269                        sym.definition_start_line =
270                            Some(self.find_definition_start_line(node, source));
271
272                        symbols.push(sym);
273                    } else {
274                        // Regular variable/constant
275                        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                        // Extract type annotation
289                        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                        // Set definition_start_line (before decorators/doc comments)
303                        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        // Extract generics
331        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        // Set definition_start_line (before decorators/doc comments)
342        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        // Check visibility modifiers
389        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        // Check for static
397        if text.contains("static") {
398            sym = sym.static_fn();
399        }
400
401        // Check for async
402        if text.contains("async") {
403            sym = sym.async_fn();
404        }
405
406        // Extract parameters
407        if let Some(params) = node.child_by_field_name("parameters") {
408            self.extract_parameters(&params, source, &mut sym);
409        }
410
411        // Extract return type
412        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        // Set definition_start_line (before decorators/doc comments)
428        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        // Extract type
450        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        // Set definition_start_line (before decorators/doc comments)
464        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        // Extract generics
486        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        // Set definition_start_line (before decorators/doc comments)
497        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        // Extract the type value
519        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        // Set definition_start_line (before decorators/doc comments)
530        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        // Set definition_start_line (before decorators/doc comments)
558        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                    // Mark as exported
605                    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        // Parse import clause
712        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                    // Default import
728                    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 * as foo
736                    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                    // import { foo, bar as baz }
746                    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        // Track current function for nested calls
785        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                // Method call: obj.method()
813                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                // Direct function call: foo()
823                (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    /// Find the earliest line of decorators/comments before a node.
868    /// Returns the line where annotation should be inserted (before decorators/docs).
869    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        // Walk backwards through siblings to find decorators and comments
874        let mut current = node.prev_sibling();
875        while let Some(prev) = current {
876            match prev.kind() {
877                "decorator" => {
878                    // @Decorator() before class/method
879                    earliest_line = prev.start_position().row + 1;
880                    current = prev.prev_sibling();
881                }
882                "comment" => {
883                    let comment = node_text(&prev, source);
884                    // Only include JSDoc comments (/** ... */) as part of definition
885                    if comment.starts_with("/**") {
886                        earliest_line = prev.start_position().row + 1;
887                        current = prev.prev_sibling();
888                    } else {
889                        // Regular comment, stop here
890                        break;
891                    }
892                }
893                _ => break,
894            }
895        }
896
897        earliest_line
898    }
899
900    /// Find all names exported via `export { name1, name2 }` clauses
901    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        // Handle export statements with export_clause (e.g., `export { Button, Card as CardComponent }`)
914        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        // Recurse into children
924        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            // export_specifier: name, or name as alias
939            if child.kind() == "export_specifier" {
940                // Get the local name (the original identifier, not the alias)
941                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                    // Fallback: get the first identifier
946                    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        // Should have: class, constructor, greet method
1011        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        // Test that symbols exported via `export { name }` are marked as exported
1075        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        // Button should be marked as exported
1089        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        // buttonVariants should be marked as exported
1096        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        // Function starts at line 6, but definition_start_line should be line 2 (JSDoc start)
1123        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        // No decorators or JSDoc, so definition_start_line equals start_line
1138        assert_eq!(func.start_line, 2);
1139        assert_eq!(func.definition_start_line, Some(2));
1140    }
1141}