Skip to main content

graphy_parser/
typescript.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4use graphy_core::{
5    EdgeKind, EdgeMetadata, GirEdge, GirNode, Language, NodeKind, ParseOutput, SymbolId,
6    Visibility,
7};
8use tree_sitter::{Node, Parser};
9
10use crate::frontend::LanguageFrontend;
11use crate::helpers::{is_noise_method_call, node_span, node_text};
12
13/// Frontend for TypeScript (.ts, .tsx) and JavaScript (.js, .jsx, .mjs, .cjs) files.
14pub struct TypeScriptFrontend;
15
16impl TypeScriptFrontend {
17    pub fn new() -> Self {
18        Self
19    }
20}
21
22impl LanguageFrontend for TypeScriptFrontend {
23    fn parse(&self, path: &Path, source: &str) -> Result<ParseOutput> {
24        let mut parser = Parser::new();
25
26        let ext = path
27            .extension()
28            .and_then(|e| e.to_str())
29            .unwrap_or("");
30
31        // Pick the right tree-sitter grammar for the file extension
32        match ext {
33            "ts" => {
34                parser
35                    .set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())
36                    .context("Failed to set TypeScript language")?;
37            }
38            "tsx" => {
39                parser
40                    .set_language(&tree_sitter_typescript::LANGUAGE_TSX.into())
41                    .context("Failed to set TSX language")?;
42            }
43            // JS, JSX, MJS, CJS all use the JavaScript grammar
44            _ => {
45                parser
46                    .set_language(&tree_sitter_javascript::LANGUAGE.into())
47                    .context("Failed to set JavaScript language")?;
48            }
49        }
50
51        let tree = parser
52            .parse(source, None)
53            .context("tree-sitter parse returned None")?;
54
55        let root = tree.root_node();
56        let mut output = ParseOutput::new();
57        let source_bytes = source.as_bytes();
58
59        let language = match ext {
60            "ts" | "tsx" => Language::TypeScript,
61            _ => Language::JavaScript,
62        };
63
64        // Create the file node
65        let file_node = GirNode {
66            id: SymbolId::new(path, path.to_string_lossy().as_ref(), NodeKind::File, 0),
67            name: path
68                .file_stem()
69                .map(|s| s.to_string_lossy().into_owned())
70                .unwrap_or_else(|| path.to_string_lossy().into_owned()),
71            kind: NodeKind::File,
72            file_path: path.to_path_buf(),
73            span: node_span(&root),
74            visibility: Visibility::Public,
75            language,
76            signature: None,
77            complexity: None,
78            confidence: 1.0,
79            doc: None,
80            coverage: None,
81        };
82        let file_id = file_node.id;
83        output.add_node(file_node);
84
85        // Track which top-level names are exported (for visibility determination)
86        let exported_names = collect_exported_names(&root, source_bytes);
87
88        // Walk top-level children
89        let mut cursor = root.walk();
90        for child in root.children(&mut cursor) {
91            extract_node(
92                &child,
93                source_bytes,
94                path,
95                file_id,
96                &mut output,
97                language,
98                &exported_names,
99            );
100        }
101
102        Ok(output)
103    }
104}
105
106/// Collect names that are explicitly exported at the top level.
107fn collect_exported_names(root: &Node, source: &[u8]) -> Vec<String> {
108    let mut names = Vec::new();
109    let mut cursor = root.walk();
110    for child in root.children(&mut cursor) {
111        if child.kind() == "export_statement" {
112            // `export function foo`, `export class Foo`, `export default ...`
113            let mut inner_cursor = child.walk();
114            for inner in child.children(&mut inner_cursor) {
115                match inner.kind() {
116                    "function_declaration" | "class_declaration"
117                    | "interface_declaration" | "enum_declaration"
118                    | "type_alias_declaration" => {
119                        if let Some(name_node) = inner.child_by_field_name("name") {
120                            names.push(node_text(&name_node, source));
121                        }
122                    }
123                    "lexical_declaration" | "variable_declaration" => {
124                        collect_variable_names(&inner, source, &mut names);
125                    }
126                    _ => {}
127                }
128            }
129            // `export { x, y }` — named exports
130            let mut inner_cursor2 = child.walk();
131            for inner in child.children(&mut inner_cursor2) {
132                if inner.kind() == "export_clause" {
133                    let mut clause_cursor = inner.walk();
134                    for spec in inner.children(&mut clause_cursor) {
135                        if spec.kind() == "export_specifier" {
136                            if let Some(name_node) = spec.child_by_field_name("name") {
137                                names.push(node_text(&name_node, source));
138                            }
139                        }
140                    }
141                }
142            }
143        }
144    }
145    names
146}
147
148fn collect_variable_names(node: &Node, source: &[u8], names: &mut Vec<String>) {
149    let mut cursor = node.walk();
150    for child in node.children(&mut cursor) {
151        if child.kind() == "variable_declarator" {
152            if let Some(name_node) = child.child_by_field_name("name") {
153                names.push(node_text(&name_node, source));
154            }
155        }
156    }
157}
158
159fn extract_node(
160    node: &Node,
161    source: &[u8],
162    path: &Path,
163    parent_id: SymbolId,
164    output: &mut ParseOutput,
165    language: Language,
166    exported_names: &[String],
167) {
168    match node.kind() {
169        "function_declaration" => {
170            extract_function(node, source, path, parent_id, output, language, false, exported_names);
171        }
172        "generator_function_declaration" => {
173            extract_function(node, source, path, parent_id, output, language, false, exported_names);
174        }
175        "class_declaration" => {
176            extract_class(node, source, path, parent_id, output, language, exported_names);
177        }
178        "interface_declaration" => {
179            extract_interface(node, source, path, parent_id, output, language, exported_names);
180        }
181        "type_alias_declaration" => {
182            extract_type_alias(node, source, path, parent_id, output, language, exported_names);
183        }
184        "enum_declaration" => {
185            extract_enum(node, source, path, parent_id, output, language, exported_names);
186        }
187        "lexical_declaration" | "variable_declaration" => {
188            extract_variable_declaration(node, source, path, parent_id, output, language, exported_names);
189        }
190        "import_statement" => {
191            extract_import(node, source, path, parent_id, output, language);
192        }
193        "export_statement" => {
194            extract_export(node, source, path, parent_id, output, language, exported_names);
195        }
196        "expression_statement" => {
197            // Handle `module.exports = ...` or top-level expressions with require()
198            extract_expression_statement(node, source, path, parent_id, output, language);
199        }
200        _ => {}
201    }
202}
203
204// ── Functions ───────────────────────────────────────────────
205
206fn extract_function(
207    node: &Node,
208    source: &[u8],
209    path: &Path,
210    parent_id: SymbolId,
211    output: &mut ParseOutput,
212    language: Language,
213    is_method: bool,
214    exported_names: &[String],
215) {
216    let Some(name_node) = node.child_by_field_name("name") else {
217        return;
218    };
219    let name = node_text(&name_node, source);
220    let span = node_span(node);
221
222    let kind = if is_method {
223        if name == "constructor" {
224            NodeKind::Constructor
225        } else {
226            NodeKind::Method
227        }
228    } else {
229        NodeKind::Function
230    };
231
232    let visibility = if exported_names.contains(&name) {
233        Visibility::Public
234    } else {
235        Visibility::Internal
236    };
237
238    let sig = build_function_signature(node, source, &name);
239    let doc = extract_jsdoc(node, source);
240
241    let func_node = GirNode {
242        id: SymbolId::new(path, &name, kind, span.start_line),
243        name: name.clone(),
244        kind,
245        file_path: path.to_path_buf(),
246        span,
247        visibility,
248        language,
249        signature: Some(sig),
250        complexity: None,
251        confidence: 1.0,
252        doc,
253        coverage: None,
254    };
255    let func_id = func_node.id;
256    output.add_node(func_node);
257    output.add_edge(parent_id, func_id, GirEdge::new(EdgeKind::Contains));
258
259    // Extract parameters
260    if let Some(params) = node.child_by_field_name("parameters") {
261        extract_parameters(&params, source, path, func_id, output, language);
262    }
263
264    // Extract return type annotation
265    if let Some(ret) = node.child_by_field_name("return_type") {
266        extract_return_type(&ret, source, path, func_id, output, language);
267    }
268
269    // Walk body for calls
270    if let Some(body) = node.child_by_field_name("body") {
271        extract_calls_from_body(&body, source, path, func_id, output, language);
272    }
273
274    // Extract decorators
275    extract_decorators(node, source, path, func_id, output, language);
276}
277
278fn extract_arrow_function(
279    node: &Node,
280    name: &str,
281    source: &[u8],
282    path: &Path,
283    parent_id: SymbolId,
284    output: &mut ParseOutput,
285    language: Language,
286    exported_names: &[String],
287) {
288    let span = node_span(node);
289    let kind = NodeKind::Function;
290
291    let visibility = if exported_names.contains(&name.to_string()) {
292        Visibility::Public
293    } else {
294        Visibility::Internal
295    };
296
297    let sig = build_arrow_signature(node, source, name);
298    let doc = extract_jsdoc(node, source);
299
300    let func_node = GirNode {
301        id: SymbolId::new(path, name, kind, span.start_line),
302        name: name.to_string(),
303        kind,
304        file_path: path.to_path_buf(),
305        span,
306        visibility,
307        language,
308        signature: Some(sig),
309        complexity: None,
310        confidence: 1.0,
311        doc,
312        coverage: None,
313    };
314    let func_id = func_node.id;
315    output.add_node(func_node);
316    output.add_edge(parent_id, func_id, GirEdge::new(EdgeKind::Contains));
317
318    // Extract parameters
319    if let Some(params) = node.child_by_field_name("parameters") {
320        extract_parameters(&params, source, path, func_id, output, language);
321    } else if let Some(param) = node.child_by_field_name("parameter") {
322        // Single parameter arrow function: x => x + 1
323        let param_name = node_text(&param, source);
324        if !param_name.is_empty() {
325            let param_node = GirNode::new(
326                param_name,
327                NodeKind::Parameter,
328                path.to_path_buf(),
329                node_span(&param),
330                language,
331            );
332            let param_id = param_node.id;
333            output.add_node(param_node);
334            output.add_edge(func_id, param_id, GirEdge::new(EdgeKind::Contains));
335        }
336    }
337
338    // Extract return type
339    if let Some(ret) = node.child_by_field_name("return_type") {
340        extract_return_type(&ret, source, path, func_id, output, language);
341    }
342
343    // Walk body for calls
344    if let Some(body) = node.child_by_field_name("body") {
345        extract_calls_from_body(&body, source, path, func_id, output, language);
346    }
347}
348
349// ── Classes ─────────────────────────────────────────────────
350
351fn extract_class(
352    node: &Node,
353    source: &[u8],
354    path: &Path,
355    parent_id: SymbolId,
356    output: &mut ParseOutput,
357    language: Language,
358    exported_names: &[String],
359) {
360    let Some(name_node) = node.child_by_field_name("name") else {
361        return;
362    };
363    let name = node_text(&name_node, source);
364    let span = node_span(node);
365    let doc = extract_jsdoc(node, source);
366
367    let visibility = if exported_names.contains(&name) {
368        Visibility::Public
369    } else {
370        Visibility::Internal
371    };
372
373    let class_node = GirNode {
374        id: SymbolId::new(path, &name, NodeKind::Class, span.start_line),
375        name: name.clone(),
376        kind: NodeKind::Class,
377        file_path: path.to_path_buf(),
378        span,
379        visibility,
380        language,
381        signature: Some(format!("class {name}")),
382        complexity: None,
383        confidence: 1.0,
384        doc,
385        coverage: None,
386    };
387    let class_id = class_node.id;
388    output.add_node(class_node);
389    output.add_edge(parent_id, class_id, GirEdge::new(EdgeKind::Contains));
390
391    // Extract superclass (extends)
392    if let Some(heritage) = node.child_by_field_name("heritage") {
393        // The heritage clause node may directly contain the superclass name
394        let heritage_text = node_text(&heritage, source);
395        if !heritage_text.is_empty() {
396            let base_node = GirNode::new(
397                heritage_text.clone(),
398                NodeKind::Class,
399                path.to_path_buf(),
400                node_span(&heritage),
401                language,
402            );
403            let base_id = base_node.id;
404            output.add_node(base_node);
405            output.add_edge(
406                class_id,
407                base_id,
408                GirEdge::new(EdgeKind::Inherits)
409                    .with_metadata(EdgeMetadata::Inheritance { depth: 1 }),
410            );
411        }
412    }
413
414    // Also check class_heritage for extends/implements via walking children
415    let mut cursor = node.walk();
416    for child in node.children(&mut cursor) {
417        if child.kind() == "class_heritage" {
418            let mut heritage_cursor = child.walk();
419            for heritage_child in child.children(&mut heritage_cursor) {
420                if heritage_child.kind() == "extends_clause" {
421                    if let Some(value) = heritage_child.child(1) {
422                        let base_name = node_text(&value, source);
423                        let base_node = GirNode::new(
424                            base_name,
425                            NodeKind::Class,
426                            path.to_path_buf(),
427                            node_span(&value),
428                            language,
429                        );
430                        let base_id = base_node.id;
431                        output.add_node(base_node);
432                        output.add_edge(
433                            class_id,
434                            base_id,
435                            GirEdge::new(EdgeKind::Inherits)
436                                .with_metadata(EdgeMetadata::Inheritance { depth: 1 }),
437                        );
438                    }
439                }
440                if heritage_child.kind() == "implements_clause" {
441                    let mut impl_cursor = heritage_child.walk();
442                    for impl_child in heritage_child.children(&mut impl_cursor) {
443                        if impl_child.kind() == "type_identifier"
444                            || impl_child.kind() == "generic_type"
445                        {
446                            let iface_name = node_text(&impl_child, source);
447                            let iface_node = GirNode::new(
448                                iface_name,
449                                NodeKind::Interface,
450                                path.to_path_buf(),
451                                node_span(&impl_child),
452                                language,
453                            );
454                            let iface_id = iface_node.id;
455                            output.add_node(iface_node);
456                            output.add_edge(
457                                class_id,
458                                iface_id,
459                                GirEdge::new(EdgeKind::Implements),
460                            );
461                        }
462                    }
463                }
464            }
465        }
466    }
467
468    // Walk class body for methods, properties, fields
469    if let Some(body) = node.child_by_field_name("body") {
470        let mut body_cursor = body.walk();
471        for child in body.children(&mut body_cursor) {
472            match child.kind() {
473                "method_definition" => {
474                    extract_method(&child, source, path, class_id, output, language);
475                }
476                "public_field_definition" | "field_definition" => {
477                    extract_class_field(&child, source, path, class_id, output, language);
478                }
479                _ => {}
480            }
481        }
482    }
483
484    // Extract decorators on the class
485    extract_decorators(node, source, path, class_id, output, language);
486}
487
488fn extract_method(
489    node: &Node,
490    source: &[u8],
491    path: &Path,
492    class_id: SymbolId,
493    output: &mut ParseOutput,
494    language: Language,
495) {
496    let Some(name_node) = node.child_by_field_name("name") else {
497        return;
498    };
499    let name = node_text(&name_node, source);
500    let span = node_span(node);
501
502    let kind = if name == "constructor" {
503        NodeKind::Constructor
504    } else {
505        NodeKind::Method
506    };
507
508    let visibility = method_visibility(node, source);
509    let sig = build_function_signature(node, source, &name);
510    let doc = extract_jsdoc(node, source);
511
512    let method_node = GirNode {
513        id: SymbolId::new(path, &name, kind, span.start_line),
514        name: name.clone(),
515        kind,
516        file_path: path.to_path_buf(),
517        span,
518        visibility,
519        language,
520        signature: Some(sig),
521        complexity: None,
522        confidence: 1.0,
523        doc,
524        coverage: None,
525    };
526    let method_id = method_node.id;
527    output.add_node(method_node);
528    output.add_edge(class_id, method_id, GirEdge::new(EdgeKind::Contains));
529
530    // Parameters
531    if let Some(params) = node.child_by_field_name("parameters") {
532        extract_parameters(&params, source, path, method_id, output, language);
533    }
534
535    // Return type
536    if let Some(ret) = node.child_by_field_name("return_type") {
537        extract_return_type(&ret, source, path, method_id, output, language);
538    }
539
540    // Body calls
541    if let Some(body) = node.child_by_field_name("body") {
542        extract_calls_from_body(&body, source, path, method_id, output, language);
543    }
544
545    // Decorators
546    extract_decorators(node, source, path, method_id, output, language);
547}
548
549fn extract_class_field(
550    node: &Node,
551    source: &[u8],
552    path: &Path,
553    class_id: SymbolId,
554    output: &mut ParseOutput,
555    language: Language,
556) {
557    let Some(name_node) = node.child_by_field_name("name") else {
558        // fallback: try first named child
559        if let Some(first) = node.named_child(0) {
560            let name = node_text(&first, source);
561            if !name.is_empty() {
562                let field_node = GirNode::new(
563                    name,
564                    NodeKind::Field,
565                    path.to_path_buf(),
566                    node_span(node),
567                    language,
568                );
569                let field_id = field_node.id;
570                output.add_node(field_node);
571                output.add_edge(class_id, field_id, GirEdge::new(EdgeKind::Contains));
572            }
573        }
574        return;
575    };
576    let name = node_text(&name_node, source);
577    let span = node_span(node);
578
579    let field_node = GirNode {
580        id: SymbolId::new(path, &name, NodeKind::Field, span.start_line),
581        name,
582        kind: NodeKind::Field,
583        file_path: path.to_path_buf(),
584        span,
585        visibility: method_visibility(node, source),
586        language,
587        signature: None,
588        complexity: None,
589        confidence: 1.0,
590        doc: None,
591        coverage: None,
592    };
593    let field_id = field_node.id;
594    output.add_node(field_node);
595    output.add_edge(class_id, field_id, GirEdge::new(EdgeKind::Contains));
596
597    // Extract field type annotation
598    if let Some(type_ann) = node.child_by_field_name("type") {
599        let type_name = node_text(&type_ann, source);
600        let type_node = GirNode::new(
601            type_name,
602            NodeKind::TypeAlias,
603            path.to_path_buf(),
604            node_span(&type_ann),
605            language,
606        );
607        let type_id = type_node.id;
608        output.add_node(type_node);
609        output.add_edge(field_id, type_id, GirEdge::new(EdgeKind::FieldType));
610    }
611}
612
613// ── Interfaces ──────────────────────────────────────────────
614
615fn extract_interface(
616    node: &Node,
617    source: &[u8],
618    path: &Path,
619    parent_id: SymbolId,
620    output: &mut ParseOutput,
621    language: Language,
622    exported_names: &[String],
623) {
624    let Some(name_node) = node.child_by_field_name("name") else {
625        return;
626    };
627    let name = node_text(&name_node, source);
628    let span = node_span(node);
629    let doc = extract_jsdoc(node, source);
630
631    let visibility = if exported_names.contains(&name) {
632        Visibility::Public
633    } else {
634        Visibility::Internal
635    };
636
637    let iface_node = GirNode {
638        id: SymbolId::new(path, &name, NodeKind::Interface, span.start_line),
639        name: name.clone(),
640        kind: NodeKind::Interface,
641        file_path: path.to_path_buf(),
642        span,
643        visibility,
644        language,
645        signature: Some(format!("interface {name}")),
646        complexity: None,
647        confidence: 1.0,
648        doc,
649        coverage: None,
650    };
651    let iface_id = iface_node.id;
652    output.add_node(iface_node);
653    output.add_edge(parent_id, iface_id, GirEdge::new(EdgeKind::Contains));
654
655    // Walk interface body for property signatures and method signatures
656    if let Some(body) = node.child_by_field_name("body") {
657        let mut cursor = body.walk();
658        for child in body.children(&mut cursor) {
659            match child.kind() {
660                "property_signature" | "public_field_definition" => {
661                    if let Some(pname) = child.child_by_field_name("name") {
662                        let prop_name = node_text(&pname, source);
663                        let prop_node = GirNode::new(
664                            prop_name,
665                            NodeKind::Property,
666                            path.to_path_buf(),
667                            node_span(&child),
668                            language,
669                        );
670                        let prop_id = prop_node.id;
671                        output.add_node(prop_node);
672                        output.add_edge(iface_id, prop_id, GirEdge::new(EdgeKind::Contains));
673                    }
674                }
675                "method_signature" => {
676                    if let Some(mname) = child.child_by_field_name("name") {
677                        let method_name = node_text(&mname, source);
678                        let method_node = GirNode::new(
679                            method_name,
680                            NodeKind::Method,
681                            path.to_path_buf(),
682                            node_span(&child),
683                            language,
684                        );
685                        let method_id = method_node.id;
686                        output.add_node(method_node);
687                        output.add_edge(iface_id, method_id, GirEdge::new(EdgeKind::Contains));
688                    }
689                }
690                _ => {}
691            }
692        }
693    }
694
695    // extends clause for interfaces
696    let mut cursor = node.walk();
697    for child in node.children(&mut cursor) {
698        if child.kind() == "extends_type_clause" || child.kind() == "extends_clause" {
699            let mut ext_cursor = child.walk();
700            for ext_child in child.children(&mut ext_cursor) {
701                if ext_child.kind() == "type_identifier" || ext_child.kind() == "generic_type" {
702                    let base_name = node_text(&ext_child, source);
703                    let base_node = GirNode::new(
704                        base_name,
705                        NodeKind::Interface,
706                        path.to_path_buf(),
707                        node_span(&ext_child),
708                        language,
709                    );
710                    let base_id = base_node.id;
711                    output.add_node(base_node);
712                    output.add_edge(
713                        iface_id,
714                        base_id,
715                        GirEdge::new(EdgeKind::Inherits)
716                            .with_metadata(EdgeMetadata::Inheritance { depth: 1 }),
717                    );
718                }
719            }
720        }
721    }
722}
723
724// ── Type Aliases ────────────────────────────────────────────
725
726fn extract_type_alias(
727    node: &Node,
728    source: &[u8],
729    path: &Path,
730    parent_id: SymbolId,
731    output: &mut ParseOutput,
732    language: Language,
733    exported_names: &[String],
734) {
735    let Some(name_node) = node.child_by_field_name("name") else {
736        return;
737    };
738    let name = node_text(&name_node, source);
739    let span = node_span(node);
740    let full_text = node_text(node, source);
741
742    let visibility = if exported_names.contains(&name) {
743        Visibility::Public
744    } else {
745        Visibility::Internal
746    };
747
748    let type_node = GirNode {
749        id: SymbolId::new(path, &name, NodeKind::TypeAlias, span.start_line),
750        name: name.clone(),
751        kind: NodeKind::TypeAlias,
752        file_path: path.to_path_buf(),
753        span,
754        visibility,
755        language,
756        signature: Some(full_text),
757        complexity: None,
758        confidence: 1.0,
759        doc: extract_jsdoc(node, source),
760        coverage: None,
761    };
762    let type_id = type_node.id;
763    output.add_node(type_node);
764    output.add_edge(parent_id, type_id, GirEdge::new(EdgeKind::Contains));
765}
766
767// ── Enums ───────────────────────────────────────────────────
768
769fn extract_enum(
770    node: &Node,
771    source: &[u8],
772    path: &Path,
773    parent_id: SymbolId,
774    output: &mut ParseOutput,
775    language: Language,
776    exported_names: &[String],
777) {
778    let Some(name_node) = node.child_by_field_name("name") else {
779        return;
780    };
781    let name = node_text(&name_node, source);
782    let span = node_span(node);
783
784    let visibility = if exported_names.contains(&name) {
785        Visibility::Public
786    } else {
787        Visibility::Internal
788    };
789
790    let enum_node = GirNode {
791        id: SymbolId::new(path, &name, NodeKind::Enum, span.start_line),
792        name: name.clone(),
793        kind: NodeKind::Enum,
794        file_path: path.to_path_buf(),
795        span,
796        visibility,
797        language,
798        signature: Some(format!("enum {name}")),
799        complexity: None,
800        confidence: 1.0,
801        doc: extract_jsdoc(node, source),
802        coverage: None,
803    };
804    let enum_id = enum_node.id;
805    output.add_node(enum_node);
806    output.add_edge(parent_id, enum_id, GirEdge::new(EdgeKind::Contains));
807
808    // Extract enum members
809    if let Some(body) = node.child_by_field_name("body") {
810        let mut cursor = body.walk();
811        for child in body.children(&mut cursor) {
812            if child.kind() == "enum_member" || child.kind() == "property_identifier" {
813                if let Some(member_name_node) = child.child_by_field_name("name") {
814                    let member_name = node_text(&member_name_node, source);
815                    let variant_node = GirNode::new(
816                        member_name,
817                        NodeKind::EnumVariant,
818                        path.to_path_buf(),
819                        node_span(&child),
820                        language,
821                    );
822                    let variant_id = variant_node.id;
823                    output.add_node(variant_node);
824                    output.add_edge(enum_id, variant_id, GirEdge::new(EdgeKind::Contains));
825                }
826            }
827        }
828    }
829}
830
831// ── Variables ───────────────────────────────────────────────
832
833fn extract_variable_declaration(
834    node: &Node,
835    source: &[u8],
836    path: &Path,
837    parent_id: SymbolId,
838    output: &mut ParseOutput,
839    language: Language,
840    exported_names: &[String],
841) {
842    // Determine if this is `const`
843    let decl_text = node_text(node, source);
844    let is_const = decl_text.starts_with("const ");
845
846    let mut cursor = node.walk();
847    for child in node.children(&mut cursor) {
848        if child.kind() == "variable_declarator" {
849            let Some(name_node) = child.child_by_field_name("name") else {
850                continue;
851            };
852            let name = node_text(&name_node, source);
853            if name.is_empty() {
854                continue;
855            }
856
857            // Check if the value is an arrow function or function expression
858            if let Some(value) = child.child_by_field_name("value") {
859                if value.kind() == "arrow_function" || value.kind() == "function" || value.kind() == "function_expression" {
860                    extract_arrow_function(
861                        &value, &name, source, path, parent_id, output, language, exported_names,
862                    );
863                    continue;
864                }
865            }
866
867            let kind = if is_const {
868                NodeKind::Constant
869            } else {
870                NodeKind::Variable
871            };
872
873            let span = node_span(&child);
874            let visibility = if exported_names.contains(&name) {
875                Visibility::Public
876            } else {
877                Visibility::Internal
878            };
879
880            let var_node = GirNode {
881                id: SymbolId::new(path, &name, kind, span.start_line),
882                name,
883                kind,
884                file_path: path.to_path_buf(),
885                span,
886                visibility,
887                language,
888                signature: None,
889                complexity: None,
890                confidence: 1.0,
891                doc: None,
892                coverage: None,
893            };
894            let var_id = var_node.id;
895            output.add_node(var_node);
896            output.add_edge(parent_id, var_id, GirEdge::new(EdgeKind::Contains));
897        }
898    }
899}
900
901// ── Imports ─────────────────────────────────────────────────
902
903fn extract_import(
904    node: &Node,
905    source: &[u8],
906    path: &Path,
907    parent_id: SymbolId,
908    output: &mut ParseOutput,
909    language: Language,
910) {
911    let text = node_text(node, source);
912    let span = node_span(node);
913
914    // Extract the module source
915    let module_name = node
916        .child_by_field_name("source")
917        .map(|n| {
918            let t = node_text(&n, source);
919            t.trim_matches('\'').trim_matches('"').to_string()
920        })
921        .unwrap_or_default();
922
923    // Collect imported items
924    let mut items = Vec::new();
925    let mut alias = None;
926
927    let mut cursor = node.walk();
928    for child in node.children(&mut cursor) {
929        match child.kind() {
930            "import_clause" => {
931                let mut clause_cursor = child.walk();
932                for clause_child in child.children(&mut clause_cursor) {
933                    match clause_child.kind() {
934                        "identifier" => {
935                            // `import foo from 'bar'` — default import
936                            alias = Some(node_text(&clause_child, source));
937                        }
938                        "named_imports" => {
939                            // `import { x, y } from 'bar'`
940                            let mut named_cursor = clause_child.walk();
941                            for spec in clause_child.children(&mut named_cursor) {
942                                if spec.kind() == "import_specifier" {
943                                    if let Some(name_node) = spec.child_by_field_name("name") {
944                                        items.push(node_text(&name_node, source));
945                                    }
946                                }
947                            }
948                        }
949                        "namespace_import" => {
950                            // `import * as foo from 'bar'`
951                            if let Some(name_node) = clause_child.child_by_field_name("name") {
952                                alias = Some(format!("* as {}", node_text(&name_node, source)));
953                            } else if clause_child.child_count() >= 3 {
954                                // Fallback: try the last child which should be the identifier
955                                if let Some(last) = clause_child.child(clause_child.child_count() - 1) {
956                                    alias = Some(format!("* as {}", node_text(&last, source)));
957                                }
958                            }
959                        }
960                        _ => {}
961                    }
962                }
963            }
964            _ => {}
965        }
966    }
967
968    let import_node = GirNode {
969        id: SymbolId::new(path, &text, NodeKind::Import, span.start_line),
970        name: module_name.clone(),
971        kind: NodeKind::Import,
972        file_path: path.to_path_buf(),
973        span,
974        visibility: Visibility::Internal,
975        language,
976        signature: Some(text),
977        complexity: None,
978        confidence: 1.0,
979        doc: None,
980        coverage: None,
981    };
982    let import_id = import_node.id;
983    output.add_node(import_node);
984    output.add_edge(
985        parent_id,
986        import_id,
987        GirEdge::new(EdgeKind::ImportsFrom).with_metadata(EdgeMetadata::Import {
988            alias,
989            items,
990        }),
991    );
992}
993
994// ── Exports ─────────────────────────────────────────────────
995
996fn extract_export(
997    node: &Node,
998    source: &[u8],
999    path: &Path,
1000    parent_id: SymbolId,
1001    output: &mut ParseOutput,
1002    language: Language,
1003    exported_names: &[String],
1004) {
1005    let mut cursor = node.walk();
1006    for child in node.children(&mut cursor) {
1007        match child.kind() {
1008            "function_declaration" => {
1009                extract_function(&child, source, path, parent_id, output, language, false, exported_names);
1010            }
1011            "generator_function_declaration" => {
1012                extract_function(&child, source, path, parent_id, output, language, false, exported_names);
1013            }
1014            "class_declaration" => {
1015                extract_class(&child, source, path, parent_id, output, language, exported_names);
1016            }
1017            "interface_declaration" => {
1018                extract_interface(&child, source, path, parent_id, output, language, exported_names);
1019            }
1020            "type_alias_declaration" => {
1021                extract_type_alias(&child, source, path, parent_id, output, language, exported_names);
1022            }
1023            "enum_declaration" => {
1024                extract_enum(&child, source, path, parent_id, output, language, exported_names);
1025            }
1026            "lexical_declaration" | "variable_declaration" => {
1027                extract_variable_declaration(&child, source, path, parent_id, output, language, exported_names);
1028            }
1029            _ => {}
1030        }
1031    }
1032}
1033
1034// ── Expression statement (module.exports, require) ──────────
1035
1036fn extract_expression_statement(
1037    node: &Node,
1038    source: &[u8],
1039    path: &Path,
1040    parent_id: SymbolId,
1041    output: &mut ParseOutput,
1042    language: Language,
1043) {
1044    let text = node_text(node, source);
1045
1046    // Handle `module.exports = ...` as an export
1047    if text.starts_with("module.exports") {
1048        let span = node_span(node);
1049        let import_node = GirNode {
1050            id: SymbolId::new(path, "module.exports", NodeKind::Variable, span.start_line),
1051            name: "module.exports".to_string(),
1052            kind: NodeKind::Variable,
1053            file_path: path.to_path_buf(),
1054            span,
1055            visibility: Visibility::Public,
1056            language,
1057            signature: Some(text.clone()),
1058            complexity: None,
1059            confidence: 1.0,
1060            doc: None,
1061            coverage: None,
1062        };
1063        let var_id = import_node.id;
1064        output.add_node(import_node);
1065        output.add_edge(parent_id, var_id, GirEdge::new(EdgeKind::Contains));
1066    }
1067
1068    // Handle `const x = require('y')` — already handled in variable_declaration,
1069    // but standalone require() calls are treated as imports
1070    if text.contains("require(") && !text.starts_with("const ") && !text.starts_with("let ") && !text.starts_with("var ") {
1071        let span = node_span(node);
1072        let import_node = GirNode {
1073            id: SymbolId::new(path, &text, NodeKind::Import, span.start_line),
1074            name: text.clone(),
1075            kind: NodeKind::Import,
1076            file_path: path.to_path_buf(),
1077            span,
1078            visibility: Visibility::Internal,
1079            language,
1080            signature: Some(text),
1081            complexity: None,
1082            confidence: 0.8,
1083            doc: None,
1084            coverage: None,
1085        };
1086        let import_id = import_node.id;
1087        output.add_node(import_node);
1088        output.add_edge(parent_id, import_id, GirEdge::new(EdgeKind::Contains));
1089    }
1090}
1091
1092// ── Parameters ──────────────────────────────────────────────
1093
1094fn extract_parameters(
1095    params_node: &Node,
1096    source: &[u8],
1097    path: &Path,
1098    func_id: SymbolId,
1099    output: &mut ParseOutput,
1100    language: Language,
1101) {
1102    let mut cursor = params_node.walk();
1103    for param in params_node.children(&mut cursor) {
1104        let name = match param.kind() {
1105            "identifier" => node_text(&param, source),
1106            "required_parameter" | "optional_parameter" => {
1107                param
1108                    .child_by_field_name("pattern")
1109                    .or_else(|| param.child_by_field_name("name"))
1110                    .or_else(|| param.child(0))
1111                    .map(|n| node_text(&n, source))
1112                    .unwrap_or_default()
1113            }
1114            "rest_pattern" => {
1115                // ...args
1116                param
1117                    .child(1)
1118                    .or_else(|| param.child(0))
1119                    .map(|n| node_text(&n, source))
1120                    .unwrap_or_default()
1121            }
1122            "assignment_pattern" => {
1123                // param = defaultValue
1124                param
1125                    .child_by_field_name("left")
1126                    .map(|n| node_text(&n, source))
1127                    .unwrap_or_default()
1128            }
1129            "formal_parameters" => continue,
1130            _ => continue,
1131        };
1132
1133        if name.is_empty() || name == "," || name == "(" || name == ")" {
1134            continue;
1135        }
1136
1137        let param_node = GirNode::new(
1138            name,
1139            NodeKind::Parameter,
1140            path.to_path_buf(),
1141            node_span(&param),
1142            language,
1143        );
1144        let param_id = param_node.id;
1145        output.add_node(param_node);
1146        output.add_edge(func_id, param_id, GirEdge::new(EdgeKind::Contains));
1147
1148        // Extract type annotation
1149        if let Some(type_ann) = param.child_by_field_name("type") {
1150            let type_name = node_text(&type_ann, source);
1151            let tn = GirNode::new(
1152                type_name,
1153                NodeKind::TypeAlias,
1154                path.to_path_buf(),
1155                node_span(&type_ann),
1156                language,
1157            );
1158            let type_id = tn.id;
1159            output.add_node(tn);
1160            output.add_edge(param_id, type_id, GirEdge::new(EdgeKind::ParamType));
1161        }
1162    }
1163}
1164
1165// ── Decorators ──────────────────────────────────────────────
1166
1167fn extract_decorators(
1168    node: &Node,
1169    source: &[u8],
1170    path: &Path,
1171    target_id: SymbolId,
1172    output: &mut ParseOutput,
1173    language: Language,
1174) {
1175    // Decorators appear as preceding siblings or children of the node
1176    // In tree-sitter-typescript, decorators are child nodes of type "decorator"
1177    let mut cursor = node.walk();
1178    for child in node.children(&mut cursor) {
1179        if child.kind() == "decorator" {
1180            let dec_text = node_text(&child, source);
1181            let dec_name = dec_text.trim_start_matches('@').trim().to_string();
1182            let dec_node = GirNode::new(
1183                dec_name,
1184                NodeKind::Decorator,
1185                path.to_path_buf(),
1186                node_span(&child),
1187                language,
1188            );
1189            let dec_id = dec_node.id;
1190            output.add_node(dec_node);
1191            output.add_edge(target_id, dec_id, GirEdge::new(EdgeKind::AnnotatedWith));
1192        }
1193    }
1194}
1195
1196// ── Return types ────────────────────────────────────────────
1197
1198fn extract_return_type(
1199    ret_node: &Node,
1200    source: &[u8],
1201    path: &Path,
1202    func_id: SymbolId,
1203    output: &mut ParseOutput,
1204    language: Language,
1205) {
1206    let type_name = node_text(ret_node, source);
1207    // Strip leading `: ` if present (type_annotation nodes may include the colon)
1208    let clean_name = type_name.trim_start_matches(':').trim().to_string();
1209    if clean_name.is_empty() {
1210        return;
1211    }
1212    let type_node = GirNode::new(
1213        clean_name,
1214        NodeKind::TypeAlias,
1215        path.to_path_buf(),
1216        node_span(ret_node),
1217        language,
1218    );
1219    let type_id = type_node.id;
1220    output.add_node(type_node);
1221    output.add_edge(func_id, type_id, GirEdge::new(EdgeKind::ReturnsType));
1222}
1223
1224// ── Call extraction ─────────────────────────────────────────
1225
1226fn extract_calls_from_body(
1227    body: &Node,
1228    source: &[u8],
1229    path: &Path,
1230    func_id: SymbolId,
1231    output: &mut ParseOutput,
1232    language: Language,
1233) {
1234    let mut stack = vec![*body];
1235    while let Some(node) = stack.pop() {
1236        if node.kind() == "call_expression" {
1237            if let Some(func_node) = node.child_by_field_name("function") {
1238                let call_name = node_text(&func_node, source);
1239
1240                if !is_noise_builtin(&call_name) && !is_noise_method_call(&call_name) {
1241                    let call_target = GirNode::new(
1242                        call_name.clone(),
1243                        NodeKind::Function,
1244                        path.to_path_buf(),
1245                        node_span(&func_node),
1246                        language,
1247                    );
1248                    let target_id = call_target.id;
1249                    output.add_node(call_target);
1250
1251                    let is_dynamic = call_name.contains('.');
1252                    let confidence = if is_dynamic { 0.7 } else { 0.9 };
1253                    output.add_edge(
1254                        func_id,
1255                        target_id,
1256                        GirEdge::new(EdgeKind::Calls)
1257                            .with_confidence(confidence)
1258                            .with_metadata(EdgeMetadata::Call { is_dynamic }),
1259                    );
1260                }
1261            }
1262        }
1263
1264        // Don't recurse into nested function/class definitions
1265        let dominated = matches!(
1266            node.kind(),
1267            "function_declaration" | "function" | "function_expression"
1268                | "arrow_function" | "class_declaration" | "class"
1269        );
1270        if !dominated || node == *body {
1271            let mut cursor = node.walk();
1272            for child in node.children(&mut cursor) {
1273                stack.push(child);
1274            }
1275        }
1276    }
1277}
1278
1279// ── Helpers ─────────────────────────────────────────────────
1280
1281fn method_visibility(node: &Node, source: &[u8]) -> Visibility {
1282    // Check for access modifiers: private, protected, public
1283    let text = node_text(node, source);
1284    if text.starts_with("private ") || text.starts_with("private\t") {
1285        Visibility::Private
1286    } else if text.starts_with("protected ") || text.starts_with("protected\t") {
1287        Visibility::Internal
1288    } else if text.starts_with("public ") || text.starts_with("public\t") {
1289        Visibility::Public
1290    } else {
1291        // Also check child nodes for accessibility modifiers
1292        let mut cursor = node.walk();
1293        for child in node.children(&mut cursor) {
1294            if child.kind() == "accessibility_modifier" {
1295                let modifier = node_text(&child, source);
1296                return match modifier.as_str() {
1297                    "private" => Visibility::Private,
1298                    "protected" => Visibility::Internal,
1299                    "public" => Visibility::Public,
1300                    _ => Visibility::Public,
1301                };
1302            }
1303        }
1304        Visibility::Public
1305    }
1306}
1307
1308fn build_function_signature(node: &Node, source: &[u8], name: &str) -> String {
1309    let params = node
1310        .child_by_field_name("parameters")
1311        .map(|p| node_text(&p, source))
1312        .unwrap_or_else(|| "()".to_string());
1313
1314    let ret = node
1315        .child_by_field_name("return_type")
1316        .map(|r| {
1317            let t = node_text(&r, source);
1318            if t.starts_with(':') {
1319                t
1320            } else {
1321                format!(": {t}")
1322            }
1323        })
1324        .unwrap_or_default();
1325
1326    format!("function {name}{params}{ret}")
1327}
1328
1329fn build_arrow_signature(node: &Node, source: &[u8], name: &str) -> String {
1330    let params = node
1331        .child_by_field_name("parameters")
1332        .map(|p| node_text(&p, source))
1333        .or_else(|| {
1334            node.child_by_field_name("parameter")
1335                .map(|p| format!("({})", node_text(&p, source)))
1336        })
1337        .unwrap_or_else(|| "()".to_string());
1338
1339    let ret = node
1340        .child_by_field_name("return_type")
1341        .map(|r| {
1342            let t = node_text(&r, source);
1343            if t.starts_with(':') {
1344                t
1345            } else {
1346                format!(": {t}")
1347            }
1348        })
1349        .unwrap_or_default();
1350
1351    format!("const {name} = {params}{ret} => ...")
1352}
1353
1354fn extract_jsdoc(node: &Node, source: &[u8]) -> Option<String> {
1355    // JSDoc is typically a comment node preceding the declaration.
1356    // In tree-sitter, comments are siblings. Check the previous sibling.
1357    let prev = node.prev_sibling()?;
1358    if prev.kind() == "comment" {
1359        let text = node_text(&prev, source);
1360        if text.starts_with("/**") {
1361            // Strip /** and */ and leading * on each line
1362            let cleaned = text
1363                .trim_start_matches("/**")
1364                .trim_end_matches("*/")
1365                .lines()
1366                .map(|line| line.trim().trim_start_matches('*').trim())
1367                .filter(|line| !line.is_empty())
1368                .collect::<Vec<_>>()
1369                .join("\n");
1370            if !cleaned.is_empty() {
1371                return Some(cleaned);
1372            }
1373        }
1374    }
1375    None
1376}
1377
1378fn is_noise_builtin(name: &str) -> bool {
1379    matches!(
1380        name,
1381        "console.log"
1382            | "console.error"
1383            | "console.warn"
1384            | "console.info"
1385            | "console.debug"
1386            | "JSON.stringify"
1387            | "JSON.parse"
1388            | "parseInt"
1389            | "parseFloat"
1390            | "isNaN"
1391            | "isFinite"
1392            | "String"
1393            | "Number"
1394            | "Boolean"
1395            | "Array"
1396            | "Object"
1397            | "Math.floor"
1398            | "Math.ceil"
1399            | "Math.round"
1400            | "Math.max"
1401            | "Math.min"
1402    )
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408    use graphy_core::NodeKind;
1409
1410    #[test]
1411    fn parse_simple_function() {
1412        let source = r#"
1413function greet(name: string): string {
1414    return `Hello, ${name}!`;
1415}
1416"#;
1417        let output = TypeScriptFrontend::new()
1418            .parse(Path::new("test.ts"), source)
1419            .unwrap();
1420
1421        let funcs: Vec<_> = output
1422            .nodes
1423            .iter()
1424            .filter(|n| n.kind == NodeKind::Function)
1425            .collect();
1426        assert_eq!(funcs.len(), 1);
1427        assert_eq!(funcs[0].name, "greet");
1428    }
1429
1430    #[test]
1431    fn parse_arrow_function() {
1432        let source = r#"
1433const add = (a: number, b: number): number => a + b;
1434"#;
1435        let output = TypeScriptFrontend::new()
1436            .parse(Path::new("test.ts"), source)
1437            .unwrap();
1438
1439        let funcs: Vec<_> = output
1440            .nodes
1441            .iter()
1442            .filter(|n| n.kind == NodeKind::Function)
1443            .collect();
1444        assert_eq!(funcs.len(), 1);
1445        assert_eq!(funcs[0].name, "add");
1446    }
1447
1448    #[test]
1449    fn parse_class_with_methods() {
1450        let source = r#"
1451class Dog extends Animal {
1452    name: string;
1453
1454    constructor(name: string) {
1455        super(name);
1456        this.name = name;
1457    }
1458
1459    bark(): string {
1460        return "Woof!";
1461    }
1462}
1463"#;
1464        let output = TypeScriptFrontend::new()
1465            .parse(Path::new("test.ts"), source)
1466            .unwrap();
1467
1468        let classes: Vec<_> = output
1469            .nodes
1470            .iter()
1471            .filter(|n| n.kind == NodeKind::Class)
1472            .collect();
1473        assert!(classes.iter().any(|c| c.name == "Dog"));
1474
1475        let constructors: Vec<_> = output
1476            .nodes
1477            .iter()
1478            .filter(|n| n.kind == NodeKind::Constructor)
1479            .collect();
1480        assert_eq!(constructors.len(), 1);
1481
1482        let methods: Vec<_> = output
1483            .nodes
1484            .iter()
1485            .filter(|n| n.kind == NodeKind::Method)
1486            .collect();
1487        assert_eq!(methods.len(), 1);
1488        assert_eq!(methods[0].name, "bark");
1489    }
1490
1491    #[test]
1492    fn parse_interface() {
1493        let source = r#"
1494interface Greetable {
1495    name: string;
1496    greet(): string;
1497}
1498"#;
1499        let output = TypeScriptFrontend::new()
1500            .parse(Path::new("test.ts"), source)
1501            .unwrap();
1502
1503        let ifaces: Vec<_> = output
1504            .nodes
1505            .iter()
1506            .filter(|n| n.kind == NodeKind::Interface)
1507            .collect();
1508        assert_eq!(ifaces.len(), 1);
1509        assert_eq!(ifaces[0].name, "Greetable");
1510    }
1511
1512    #[test]
1513    fn parse_enum() {
1514        let source = r#"
1515enum Direction {
1516    Up,
1517    Down,
1518    Left,
1519    Right,
1520}
1521"#;
1522        let output = TypeScriptFrontend::new()
1523            .parse(Path::new("test.ts"), source)
1524            .unwrap();
1525
1526        let enums: Vec<_> = output
1527            .nodes
1528            .iter()
1529            .filter(|n| n.kind == NodeKind::Enum)
1530            .collect();
1531        assert_eq!(enums.len(), 1);
1532        assert_eq!(enums[0].name, "Direction");
1533    }
1534
1535    #[test]
1536    fn parse_imports() {
1537        let source = r#"
1538import { readFile } from 'fs';
1539import path from 'path';
1540import * as http from 'http';
1541"#;
1542        let output = TypeScriptFrontend::new()
1543            .parse(Path::new("test.ts"), source)
1544            .unwrap();
1545
1546        let imports: Vec<_> = output
1547            .nodes
1548            .iter()
1549            .filter(|n| n.kind == NodeKind::Import)
1550            .collect();
1551        assert_eq!(imports.len(), 3);
1552    }
1553
1554    #[test]
1555    fn parse_exported_function_visibility() {
1556        let source = r#"
1557export function publicFn(): void {}
1558function privateFn(): void {}
1559"#;
1560        let output = TypeScriptFrontend::new()
1561            .parse(Path::new("test.ts"), source)
1562            .unwrap();
1563
1564        let funcs: Vec<_> = output
1565            .nodes
1566            .iter()
1567            .filter(|n| n.kind == NodeKind::Function)
1568            .collect();
1569        assert_eq!(funcs.len(), 2);
1570
1571        let public_fn = funcs.iter().find(|f| f.name == "publicFn").unwrap();
1572        assert_eq!(public_fn.visibility, Visibility::Public);
1573
1574        let private_fn = funcs.iter().find(|f| f.name == "privateFn").unwrap();
1575        assert_eq!(private_fn.visibility, Visibility::Internal);
1576    }
1577
1578    #[test]
1579    fn parse_javascript_file() {
1580        let source = r#"
1581function hello() {
1582    return "world";
1583}
1584const x = 42;
1585"#;
1586        let output = TypeScriptFrontend::new()
1587            .parse(Path::new("test.js"), source)
1588            .unwrap();
1589
1590        let funcs: Vec<_> = output
1591            .nodes
1592            .iter()
1593            .filter(|n| n.kind == NodeKind::Function)
1594            .collect();
1595        assert_eq!(funcs.len(), 1);
1596
1597        let consts: Vec<_> = output
1598            .nodes
1599            .iter()
1600            .filter(|n| n.kind == NodeKind::Constant)
1601            .collect();
1602        assert_eq!(consts.len(), 1);
1603    }
1604
1605    #[test]
1606    fn parse_type_alias() {
1607        let source = r#"
1608type Point = { x: number; y: number };
1609"#;
1610        let output = TypeScriptFrontend::new()
1611            .parse(Path::new("test.ts"), source)
1612            .unwrap();
1613
1614        let types: Vec<_> = output
1615            .nodes
1616            .iter()
1617            .filter(|n| n.kind == NodeKind::TypeAlias && n.name == "Point")
1618            .collect();
1619        assert_eq!(types.len(), 1);
1620    }
1621
1622    // ── Edge case tests ───────────────────────────────────
1623
1624    #[test]
1625    fn parse_empty_file() {
1626        let output = TypeScriptFrontend::new()
1627            .parse(Path::new("empty.ts"), "")
1628            .unwrap();
1629        assert!(output.nodes.iter().any(|n| n.kind == NodeKind::File));
1630    }
1631
1632    #[test]
1633    fn parse_comments_only() {
1634        let source = "// This is a comment\n/* Block comment */\n";
1635        let output = TypeScriptFrontend::new()
1636            .parse(Path::new("test.ts"), source)
1637            .unwrap();
1638        let funcs: Vec<_> = output.nodes.iter()
1639            .filter(|n| n.kind == NodeKind::Function || n.kind == NodeKind::Class)
1640            .collect();
1641        assert!(funcs.is_empty());
1642    }
1643
1644    #[test]
1645    fn parse_async_function() {
1646        let source = r#"
1647async function fetchData(url: string): Promise<Response> {
1648    return await fetch(url);
1649}
1650"#;
1651        let output = TypeScriptFrontend::new()
1652            .parse(Path::new("test.ts"), source)
1653            .unwrap();
1654        let funcs: Vec<_> = output.nodes.iter()
1655            .filter(|n| n.kind == NodeKind::Function)
1656            .collect();
1657        // fetchData is the real function; fetch() creates a phantom call target
1658        assert!(funcs.iter().any(|f| f.name == "fetchData"));
1659    }
1660
1661    #[test]
1662    fn parse_class_with_generics() {
1663        let source = r#"
1664class Container<T> {
1665    value: T;
1666    constructor(val: T) {
1667        this.value = val;
1668    }
1669    get(): T {
1670        return this.value;
1671    }
1672}
1673"#;
1674        let output = TypeScriptFrontend::new()
1675            .parse(Path::new("test.ts"), source)
1676            .unwrap();
1677        let classes: Vec<_> = output.nodes.iter()
1678            .filter(|n| n.kind == NodeKind::Class)
1679            .collect();
1680        assert_eq!(classes.len(), 1);
1681        assert_eq!(classes[0].name, "Container");
1682    }
1683
1684    #[test]
1685    fn parse_jsx_file() {
1686        let source = r#"
1687function App() {
1688    return <div>Hello</div>;
1689}
1690"#;
1691        let output = TypeScriptFrontend::new()
1692            .parse(Path::new("app.jsx"), source)
1693            .unwrap();
1694        let funcs: Vec<_> = output.nodes.iter()
1695            .filter(|n| n.kind == NodeKind::Function)
1696            .collect();
1697        assert_eq!(funcs.len(), 1);
1698        assert_eq!(funcs[0].name, "App");
1699    }
1700
1701    #[test]
1702    fn parse_tsx_file() {
1703        let source = r#"
1704interface Props {
1705    name: string;
1706}
1707
1708function Greeting({ name }: Props) {
1709    return <h1>Hello, {name}</h1>;
1710}
1711
1712export default Greeting;
1713"#;
1714        let output = TypeScriptFrontend::new()
1715            .parse(Path::new("greeting.tsx"), source)
1716            .unwrap();
1717        let ifaces: Vec<_> = output.nodes.iter()
1718            .filter(|n| n.kind == NodeKind::Interface)
1719            .collect();
1720        assert_eq!(ifaces.len(), 1);
1721        let funcs: Vec<_> = output.nodes.iter()
1722            .filter(|n| n.kind == NodeKind::Function)
1723            .collect();
1724        assert_eq!(funcs.len(), 1);
1725    }
1726
1727    #[test]
1728    fn parse_mjs_extension() {
1729        let source = "export function hello() { return 42; }\n";
1730        let output = TypeScriptFrontend::new()
1731            .parse(Path::new("module.mjs"), source)
1732            .unwrap();
1733        let funcs: Vec<_> = output.nodes.iter()
1734            .filter(|n| n.kind == NodeKind::Function)
1735            .collect();
1736        assert_eq!(funcs.len(), 1);
1737    }
1738
1739    #[test]
1740    fn parse_call_expressions() {
1741        let source = r#"
1742function main() {
1743    console.log("hello");
1744    helper();
1745}
1746"#;
1747        let output = TypeScriptFrontend::new()
1748            .parse(Path::new("test.ts"), source)
1749            .unwrap();
1750        let calls: Vec<_> = output.edges.iter()
1751            .filter(|e| e.2.kind == EdgeKind::Calls)
1752            .collect();
1753        assert!(calls.len() >= 1);
1754    }
1755
1756    #[test]
1757    fn parse_multiple_exports() {
1758        let source = r#"
1759export const PI = 3.14;
1760export function add(a: number, b: number) { return a + b; }
1761export class Calculator {}
1762"#;
1763        let output = TypeScriptFrontend::new()
1764            .parse(Path::new("test.ts"), source)
1765            .unwrap();
1766        let exported: Vec<_> = output.nodes.iter()
1767            .filter(|n| n.visibility == Visibility::Public && n.kind != NodeKind::File)
1768            .collect();
1769        assert!(exported.len() >= 2);
1770    }
1771
1772    #[test]
1773    fn parse_generic_function_with_constraints() {
1774        let source = r#"
1775function identity<T extends Serializable>(arg: T): T {
1776    return arg;
1777}
1778
1779interface Pair<K, V> {
1780    key: K;
1781    value: V;
1782}
1783
1784type Result<T, E = Error> = { ok: T } | { err: E };
1785"#;
1786        let output = TypeScriptFrontend::new()
1787            .parse(Path::new("test.ts"), source)
1788            .unwrap();
1789
1790        // Generic function should be parsed with its name
1791        let funcs: Vec<_> = output.nodes.iter()
1792            .filter(|n| n.kind == NodeKind::Function)
1793            .collect();
1794        assert_eq!(funcs.len(), 1);
1795        assert_eq!(funcs[0].name, "identity");
1796
1797        // Generic interface should be parsed
1798        let ifaces: Vec<_> = output.nodes.iter()
1799            .filter(|n| n.kind == NodeKind::Interface)
1800            .collect();
1801        assert_eq!(ifaces.len(), 1);
1802        assert_eq!(ifaces[0].name, "Pair");
1803
1804        // Generic type alias should be parsed
1805        let types: Vec<_> = output.nodes.iter()
1806            .filter(|n| n.kind == NodeKind::TypeAlias && n.name == "Result")
1807            .collect();
1808        assert_eq!(types.len(), 1);
1809    }
1810}