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
13pub 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 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 _ => {
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 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 let exported_names = collect_exported_names(&root, source_bytes);
87
88 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
106fn 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 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 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 extract_expression_statement(node, source, path, parent_id, output, language);
199 }
200 _ => {}
201 }
202}
203
204fn 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 if let Some(params) = node.child_by_field_name("parameters") {
261 extract_parameters(¶ms, source, path, func_id, output, language);
262 }
263
264 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 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(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 if let Some(params) = node.child_by_field_name("parameters") {
320 extract_parameters(¶ms, source, path, func_id, output, language);
321 } else if let Some(param) = node.child_by_field_name("parameter") {
322 let param_name = node_text(¶m, 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(¶m),
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 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 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
349fn 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 if let Some(heritage) = node.child_by_field_name("heritage") {
393 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 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 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(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 if let Some(params) = node.child_by_field_name("parameters") {
532 extract_parameters(¶ms, source, path, method_id, output, language);
533 }
534
535 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 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 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 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 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
613fn 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 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 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
724fn 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
767fn 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 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
831fn 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 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 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
901fn 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 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 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 alias = Some(node_text(&clause_child, source));
937 }
938 "named_imports" => {
939 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 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 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
994fn 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
1034fn 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 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 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
1092fn 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(¶m, 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 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
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(¶m),
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 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
1165fn extract_decorators(
1168 node: &Node,
1169 source: &[u8],
1170 path: &Path,
1171 target_id: SymbolId,
1172 output: &mut ParseOutput,
1173 language: Language,
1174) {
1175 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
1196fn 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 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
1224fn 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 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
1279fn method_visibility(node: &Node, source: &[u8]) -> Visibility {
1282 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 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 let prev = node.prev_sibling()?;
1358 if prev.kind() == "comment" {
1359 let text = node_text(&prev, source);
1360 if text.starts_with("/**") {
1361 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 #[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 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 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 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 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}