1use crate::extract::make_id;
2use crate::import_extension::resolve_js_module_path;
3use crate::types::{Edge, Node};
4use std::collections::{HashMap, HashSet};
5use std::path::{Path, PathBuf};
6
7struct SymbolDeclarationFact {
12 file_path: PathBuf,
13 name: String,
14 line: usize,
15}
16
17struct SymbolImportFact {
18 file_path: PathBuf,
19 local_name: String,
20 target_path: PathBuf,
21 imported_name: String,
22 #[allow(dead_code)]
23 line: usize,
24}
25
26struct SymbolAliasFact {
27 file_path: PathBuf,
28 alias: String,
29 target_name: String,
30 #[allow(dead_code)]
31 line: usize,
32}
33
34struct SymbolExportFact {
35 file_path: PathBuf,
36 exported_name: String,
37 #[allow(dead_code)]
38 line: usize,
39 local_name: Option<String>,
40 target_path: Option<PathBuf>,
41 target_name: Option<String>,
42}
43
44struct StarExportFact {
45 file_path: PathBuf,
46 target_path: PathBuf,
47 #[allow(dead_code)]
48 line: usize,
49}
50
51struct SymbolUseFact {
52 file_path: PathBuf,
53 source_id: String,
54 local_name: String,
55 relation: String,
56 #[allow(dead_code)]
57 context: String,
58 #[allow(dead_code)]
59 line: usize,
60}
61
62#[derive(Default)]
63struct SymbolResolutionFacts {
64 declarations: Vec<SymbolDeclarationFact>,
65 imports: Vec<SymbolImportFact>,
66 aliases: Vec<SymbolAliasFact>,
67 exports: Vec<SymbolExportFact>,
68 star_exports: Vec<StarExportFact>,
69 uses: Vec<SymbolUseFact>,
70}
71
72pub fn js_file_stem(rel_path: &str) -> String {
77 let path = Path::new(rel_path);
78 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
79 let parent = path
80 .parent()
81 .and_then(|p| p.file_name())
82 .and_then(|n| n.to_str())
83 .unwrap_or("");
84 if !parent.is_empty() && parent != "." {
85 format!("{}.{}", parent, stem)
86 } else {
87 stem.to_string()
88 }
89}
90
91fn walk_nodes(node: tree_sitter::Node) -> Vec<tree_sitter::Node> {
96 let mut stack = vec![node];
97 let mut result = Vec::new();
98 while let Some(n) = stack.pop() {
99 result.push(n);
100 for i in (0..n.child_count()).rev() {
101 if let Some(child) = n.child(i) {
102 stack.push(child);
103 }
104 }
105 }
106 result
107}
108
109fn node_text<'a>(node: tree_sitter::Node<'_>, source: &'a [u8]) -> &'a str {
110 std::str::from_utf8(&source[node.start_byte()..node.end_byte()]).unwrap_or("")
111}
112
113fn string_content(node: tree_sitter::Node<'_>, source: &[u8]) -> Option<String> {
114 let text = node_text(node, source);
115 Some(
116 text.trim_matches(|c| c == '\'' || c == '"' || c == '`')
117 .to_string(),
118 )
119}
120
121fn js_module_specifier(node: tree_sitter::Node<'_>, source: &[u8]) -> Option<String> {
122 if let Some(src) = node.child_by_field_name("source") {
123 return string_content(src, source);
124 }
125 for i in 0..node.child_count() {
126 if let Some(child) = node.child(i) {
127 if child.kind() == "string" {
128 return string_content(child, source);
129 }
130 }
131 }
132 None
133}
134
135fn js_named_specifiers(
136 node: tree_sitter::Node<'_>,
137 source: &[u8],
138 specifier_type: &str,
139) -> Vec<(String, String)> {
140 let mut pairs = Vec::new();
141 for n in walk_nodes(node) {
142 if n.kind() != specifier_type {
143 continue;
144 }
145 let name_node = match n.child_by_field_name("name") {
146 Some(nn) => nn,
147 None => continue,
148 };
149 let name = node_text(name_node, source).to_string();
150 let alias_node = n.child_by_field_name("alias");
151 let alias = if let Some(a) = alias_node {
152 node_text(a, source).to_string()
153 } else {
154 name.clone()
155 };
156 if !name.is_empty() && !alias.is_empty() {
157 pairs.push((name, alias));
158 }
159 }
160 pairs
161}
162
163fn js_lexical_aliases(node: tree_sitter::Node<'_>, source: &[u8]) -> Vec<(String, String)> {
164 if node.kind() != "lexical_declaration" {
165 return vec![];
166 }
167 let mut aliases = Vec::new();
168 for i in 0..node.child_count() {
169 if let Some(child) = node.child(i) {
170 if child.kind() != "variable_declarator" {
171 continue;
172 }
173 let name_node = child.child_by_field_name("name");
174 let value_node = child.child_by_field_name("value");
175 if let (Some(name_n), Some(val_n)) = (name_node, value_node) {
176 if val_n.kind() == "identifier" || val_n.kind() == "type_identifier" {
177 aliases.push((
178 node_text(name_n, source).to_string(),
179 node_text(val_n, source).to_string(),
180 ));
181 }
182 }
183 }
184 }
185 aliases
186}
187
188fn js_export_is_star(node: tree_sitter::Node<'_>) -> bool {
189 for i in 0..node.child_count() {
190 if let Some(child) = node.child(i) {
191 if child.kind() == "*" {
192 return true;
193 }
194 }
195 }
196 false
197}
198
199fn js_exported_declaration_names(node: tree_sitter::Node<'_>, source: &[u8]) -> Vec<String> {
200 let decl = match node.child_by_field_name("declaration") {
201 Some(d) => d,
202 None => return vec![],
203 };
204 let mut names = Vec::new();
205 match decl.kind() {
206 "lexical_declaration" => {
207 for (alias, _) in js_lexical_aliases(decl, source) {
208 names.push(alias);
209 }
210 }
211 "class_declaration"
212 | "abstract_class_declaration"
213 | "interface_declaration"
214 | "type_alias_declaration"
215 | "function_declaration" => {
216 if let Some(name_node) = decl.child_by_field_name("name") {
217 let name = node_text(name_node, source).to_string();
218 if !name.is_empty() {
219 names.push(name);
220 }
221 }
222 }
223 _ => {}
224 }
225 names
226}
227
228fn ts_collect_type_refs(
229 node: tree_sitter::Node<'_>,
230 source: &[u8],
231 generic: bool,
232 out: &mut Vec<(String, String)>,
233) {
234 const PRIMITIVES: &[&str] = &[
235 "string",
236 "number",
237 "boolean",
238 "any",
239 "unknown",
240 "void",
241 "never",
242 "object",
243 "null",
244 "undefined",
245 "bigint",
246 "symbol",
247 "this",
248 "T",
249 ];
250
251 let kind = node.kind();
252 match kind {
253 "type_annotation" => {
254 for i in 0..node.child_count() {
255 if let Some(child) = node.child(i) {
256 if child.is_named() {
257 ts_collect_type_refs(child, source, generic, out);
258 }
259 }
260 }
261 }
262 "type_identifier" | "identifier" => {
263 let name = node_text(node, source);
264 if !name.is_empty() && !PRIMITIVES.contains(&name) {
265 out.push((
266 name.to_string(),
267 if generic {
268 "generic_arg".to_string()
269 } else {
270 "type".to_string()
271 },
272 ));
273 }
274 }
275 "generic_type" => {
276 if let Some(name_node) = node.child_by_field_name("name") {
277 let text = node_text(name_node, source)
278 .rsplit('.')
279 .next()
280 .unwrap_or("")
281 .to_string();
282 if !text.is_empty() && !PRIMITIVES.contains(&text.as_str()) {
283 out.push((
284 text,
285 if generic {
286 "generic_arg".to_string()
287 } else {
288 "type".to_string()
289 },
290 ));
291 }
292 }
293 for i in 0..node.child_count() {
294 if let Some(child) = node.child(i) {
295 if child.kind() == "type_arguments" {
296 for j in 0..child.child_count() {
297 if let Some(sub) = child.child(j) {
298 if sub.is_named() {
299 ts_collect_type_refs(sub, source, true, out);
300 }
301 }
302 }
303 }
304 }
305 }
306 }
307 "nested_type_identifier" => {
308 let text = node_text(node, source)
309 .rsplit('.')
310 .next()
311 .unwrap_or("")
312 .to_string();
313 if !text.is_empty() && !PRIMITIVES.contains(&text.as_str()) {
314 out.push((
315 text,
316 if generic {
317 "generic_arg".to_string()
318 } else {
319 "type".to_string()
320 },
321 ));
322 }
323 }
324 _ => {
325 if node.is_named() {
326 for i in 0..node.child_count() {
327 if let Some(child) = node.child(i) {
328 if child.is_named() {
329 ts_collect_type_refs(child, source, generic, out);
330 }
331 }
332 }
333 }
334 }
335 }
336}
337
338fn ts_heritage_clause_entries(clause_node: tree_sitter::Node<'_>, source: &[u8]) -> Vec<String> {
339 let mut out = Vec::new();
340 for i in 0..clause_node.child_count() {
341 let child = match clause_node.child(i) {
342 Some(c) => c,
343 None => continue,
344 };
345 if !child.is_named() {
346 continue;
347 }
348 match child.kind() {
349 "identifier" | "type_identifier" => {
350 let name = node_text(child, source).to_string();
351 if !name.is_empty() {
352 out.push(name);
353 }
354 }
355 "generic_type" => {
356 if let Some(name_node) = child.child_by_field_name("name") {
357 let text = node_text(name_node, source)
358 .rsplit('.')
359 .next()
360 .unwrap_or("")
361 .to_string();
362 if !text.is_empty() {
363 out.push(text);
364 }
365 }
366 }
367 "nested_type_identifier" => {
368 let text = node_text(child, source)
369 .rsplit('.')
370 .next()
371 .unwrap_or("")
372 .to_string();
373 if !text.is_empty() {
374 out.push(text);
375 }
376 }
377 _ => {}
378 }
379 }
380 out
381}
382
383fn is_js_ts_file(path: &Path) -> bool {
388 matches!(
389 path.extension().and_then(|e| e.to_str()),
390 Some("ts") | Some("tsx") | Some("js") | Some("jsx") | Some("mjs") | Some("svelte")
391 )
392}
393
394fn parse_js_ts(path: &Path, source: &[u8]) -> Option<tree_sitter::Tree> {
395 let mut parser = tree_sitter::Parser::new();
396 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
397 let lang: tree_sitter::Language = match ext {
398 "ts" | "tsx" => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
399 _ => tree_sitter_javascript::LANGUAGE.into(),
400 };
401 parser.set_language(&lang).ok()?;
402 parser.parse(source, None)
403}
404
405fn make_node(id: String, label: String, source_file: String, line: Option<usize>) -> Node {
410 let source_location = line.map(|l| format!("{}:{}", source_file, l + 1));
411 Node {
412 id,
413 label,
414 file_type: "code".to_string(),
415 source_file,
416 source_location,
417 community: None,
418 rationale: None,
419 docstring: None,
420 metadata: std::collections::HashMap::new(),
421 }
422}
423
424fn make_edge(
425 source: String,
426 target: String,
427 relation: String,
428 source_file: Option<String>,
429) -> Edge {
430 Edge {
431 source,
432 target,
433 relation,
434 confidence: "EXTRACTED".to_string(),
435 source_file,
436 weight: 1.0,
437 context: None,
438 }
439}
440
441fn extract_js_ts_file_nodes(
446 abs_path: &Path,
447 rel_path: &Path,
448 source: &[u8],
449 nodes: &mut Vec<Node>,
450 edges: &mut Vec<Edge>,
451 edge_set: &mut HashSet<(String, String, String)>,
452) {
453 let rel_str = rel_path.to_string_lossy().to_string();
454 let file_id = make_id(&[&rel_str]);
455 let file_label = abs_path
456 .file_name()
457 .and_then(|n| n.to_str())
458 .unwrap_or(&rel_str)
459 .to_string();
460
461 nodes.push(make_node(
463 file_id.clone(),
464 file_label,
465 rel_str.clone(),
466 None,
467 ));
468
469 let tree = match parse_js_ts(abs_path, source) {
470 Some(t) => t,
471 None => return,
472 };
473 let root = tree.root_node();
474 let file_stem = js_file_stem(&rel_str);
475
476 for i in 0..root.child_count() {
478 let child = match root.child(i) {
479 Some(c) => c,
480 None => continue,
481 };
482
483 match child.kind() {
484 "export_statement" => {
485 if child.child_by_field_name("declaration").is_none() {
487 let source_field = child.child_by_field_name("source");
488 if source_field.is_some() || js_export_is_star(child) {
489 } else {
491 }
493 continue;
494 }
495 let names = js_exported_declaration_names(child, source);
497 let line = child.start_position().row;
498 for name in names {
499 let sym_id = make_id(&[&file_stem, &name]);
500 nodes.push(make_node(sym_id.clone(), name, rel_str.clone(), Some(line)));
501 let ek = (file_id.clone(), sym_id.clone(), "contains".to_string());
502 if edge_set.insert(ek) {
503 edges.push(make_edge(
504 file_id.clone(),
505 sym_id,
506 "contains".to_string(),
507 Some(rel_str.clone()),
508 ));
509 }
510 if let Some(decl) = child.child_by_field_name("declaration") {
512 let decl_kind = decl.kind();
513 if decl_kind == "class_declaration"
514 || decl_kind == "abstract_class_declaration"
515 {
516 if let Some(name_node) = decl.child_by_field_name("name") {
517 let class_name = node_text(name_node, source).to_string();
518 let class_nid = make_id(&[&file_stem, &class_name]);
519 extract_class_methods(
520 decl, source, &class_nid, &rel_str, nodes, edges, edge_set,
521 );
522 }
523 }
524 }
525 }
526 }
527 "function_declaration" => {
528 let line = child.start_position().row;
529 if let Some(name_node) = child.child_by_field_name("name") {
530 let name = node_text(name_node, source).to_string();
531 if !name.is_empty() {
532 let sym_id = make_id(&[&file_stem, &name]);
533 if !nodes.iter().any(|n| n.id == sym_id) {
534 nodes.push(make_node(
535 sym_id.clone(),
536 name,
537 rel_str.clone(),
538 Some(line),
539 ));
540 let ek = (file_id.clone(), sym_id.clone(), "contains".to_string());
541 if edge_set.insert(ek) {
542 edges.push(make_edge(
543 file_id.clone(),
544 sym_id,
545 "contains".to_string(),
546 Some(rel_str.clone()),
547 ));
548 }
549 }
550 }
551 }
552 }
553 "lexical_declaration" => {
554 let line = child.start_position().row;
555 for i2 in 0..child.child_count() {
556 if let Some(declarator) = child.child(i2) {
557 if declarator.kind() != "variable_declarator" {
558 continue;
559 }
560 let name_node = declarator.child_by_field_name("name");
561 let value_node = declarator.child_by_field_name("value");
562 if let (Some(nn), Some(vn)) = (name_node, value_node) {
563 if vn.kind() == "arrow_function" || vn.kind() == "function" {
564 let name = node_text(nn, source).to_string();
565 if !name.is_empty() {
566 let sym_id = make_id(&[&file_stem, &name]);
567 if !nodes.iter().any(|n| n.id == sym_id) {
568 nodes.push(make_node(
569 sym_id.clone(),
570 name,
571 rel_str.clone(),
572 Some(line),
573 ));
574 let ek = (
575 file_id.clone(),
576 sym_id.clone(),
577 "contains".to_string(),
578 );
579 if edge_set.insert(ek) {
580 edges.push(make_edge(
581 file_id.clone(),
582 sym_id,
583 "contains".to_string(),
584 Some(rel_str.clone()),
585 ));
586 }
587 }
588 }
589 }
590 }
591 }
592 }
593 }
594 "class_declaration" | "abstract_class_declaration" => {
595 let line = child.start_position().row;
596 if let Some(name_node) = child.child_by_field_name("name") {
597 let name = node_text(name_node, source).to_string();
598 if !name.is_empty() {
599 let sym_id = make_id(&[&file_stem, &name]);
600 if !nodes.iter().any(|n| n.id == sym_id) {
601 nodes.push(make_node(
602 sym_id.clone(),
603 name,
604 rel_str.clone(),
605 Some(line),
606 ));
607 let ek = (file_id.clone(), sym_id.clone(), "contains".to_string());
608 if edge_set.insert(ek) {
609 edges.push(make_edge(
610 file_id.clone(),
611 sym_id.clone(),
612 "contains".to_string(),
613 Some(rel_str.clone()),
614 ));
615 }
616 }
617 extract_class_methods(
618 child, source, &sym_id, &rel_str, nodes, edges, edge_set,
619 );
620 }
621 }
622 }
623 _ => {}
624 }
625 }
626}
627
628fn extract_class_methods(
629 class_node: tree_sitter::Node<'_>,
630 source: &[u8],
631 class_nid: &str,
632 rel_str: &str,
633 nodes: &mut Vec<Node>,
634 edges: &mut Vec<Edge>,
635 edge_set: &mut HashSet<(String, String, String)>,
636) {
637 let body = match class_node.child_by_field_name("body") {
639 Some(b) => b,
640 None => return,
641 };
642 for i in 0..body.child_count() {
643 let member = match body.child(i) {
644 Some(m) => m,
645 None => continue,
646 };
647 match member.kind() {
648 "method_definition" | "method_signature" | "abstract_method_signature" => {
649 let line = member.start_position().row;
650 if let Some(name_node) = member.child_by_field_name("name") {
651 let method_name = node_text(name_node, source).to_string();
652 if method_name.is_empty() || method_name == "constructor" {
653 continue;
654 }
655 let method_nid = make_id(&[class_nid, &method_name]);
656 if !nodes.iter().any(|n| n.id == method_nid) {
657 nodes.push(make_node(
658 method_nid.clone(),
659 method_name,
660 rel_str.to_string(),
661 Some(line),
662 ));
663 let ek = (
664 class_nid.to_string(),
665 method_nid.clone(),
666 "contains".to_string(),
667 );
668 if edge_set.insert(ek) {
669 edges.push(make_edge(
670 class_nid.to_string(),
671 method_nid,
672 "contains".to_string(),
673 Some(rel_str.to_string()),
674 ));
675 }
676 }
677 }
678 }
679 _ => {}
680 }
681 }
682}
683
684fn collect_js_symbol_resolution_facts(
689 abs_path: &Path,
690 rel_path: &Path,
691 source: &[u8],
692 facts: &mut SymbolResolutionFacts,
693 canon_to_rel: &HashMap<PathBuf, PathBuf>,
694) {
695 let rel_str = rel_path.to_string_lossy().to_string();
696 let file_stem = js_file_stem(&rel_str);
697 let file_dir = abs_path.parent().unwrap_or(Path::new("."));
698
699 let tree = match parse_js_ts(abs_path, source) {
700 Some(t) => t,
701 None => return,
702 };
703 let root = tree.root_node();
704
705 let resolve_module = |raw: &str| -> Option<PathBuf> {
707 let resolved = resolve_js_module_path(raw, file_dir)?;
708 std::fs::canonicalize(&resolved).ok().or(Some(resolved))
709 };
710
711 for node in walk_nodes(root) {
713 match node.kind() {
714 "export_statement"
715 if node.child_by_field_name("source").is_none() && !js_export_is_star(node) => {
717 for name in js_exported_declaration_names(node, source) {
718 facts.declarations.push(SymbolDeclarationFact {
719 file_path: abs_path.to_path_buf(),
720 name,
721 line: node.start_position().row,
722 });
723 }
724 }
725 "function_declaration"
726 if node.parent().map(|p| p.kind()) == Some("program") => {
728 if let Some(name_node) = node.child_by_field_name("name") {
729 let name = node_text(name_node, source).to_string();
730 if !name.is_empty() {
731 facts.declarations.push(SymbolDeclarationFact {
732 file_path: abs_path.to_path_buf(),
733 name,
734 line: node.start_position().row,
735 });
736 }
737 }
738 }
739 "lexical_declaration"
740 if node.parent().map(|p| p.kind()) == Some("program") => {
741 for (alias, target) in js_lexical_aliases(node, source) {
743 facts.aliases.push(SymbolAliasFact {
744 file_path: abs_path.to_path_buf(),
745 alias,
746 target_name: target,
747 line: node.start_position().row,
748 });
749 }
750 }
751 "import_statement" => {
752 let line = node.start_position().row;
753 if let Some(raw) = js_module_specifier(node, source) {
754 if let Some(target_path) = resolve_module(&raw) {
755 let specifiers = js_named_specifiers(node, source, "import_specifier");
757 for (imported_name, local_name) in specifiers {
758 facts.imports.push(SymbolImportFact {
759 file_path: abs_path.to_path_buf(),
760 local_name,
761 target_path: target_path.clone(),
762 imported_name,
763 line,
764 });
765 }
766 }
767 }
768 }
769 _ => {}
770 }
771 }
772
773 for node in walk_nodes(root) {
775 if node.kind() != "export_statement" {
776 continue;
777 }
778 let line = node.start_position().row;
779 let source_field = node.child_by_field_name("source");
780
781 if let Some(src_node) = source_field {
782 let raw = match string_content(src_node, source) {
783 Some(s) => s,
784 None => continue,
785 };
786 let target_path = match resolve_module(&raw) {
787 Some(p) => p,
788 None => continue,
789 };
790
791 if js_export_is_star(node) {
792 facts.star_exports.push(StarExportFact {
793 file_path: abs_path.to_path_buf(),
794 target_path,
795 line,
796 });
797 } else {
798 let specifiers = js_named_specifiers(node, source, "export_specifier");
800 for (orig_name, exported_name) in specifiers {
801 facts.exports.push(SymbolExportFact {
802 file_path: abs_path.to_path_buf(),
803 exported_name,
804 line,
805 local_name: None,
806 target_path: Some(target_path.clone()),
807 target_name: Some(orig_name),
808 });
809 }
810 }
811 } else if node.child_by_field_name("declaration").is_none() {
812 let specifiers = js_named_specifiers(node, source, "export_specifier");
814 for (local_name, exported_name) in specifiers {
815 facts.exports.push(SymbolExportFact {
816 file_path: abs_path.to_path_buf(),
817 exported_name,
818 line,
819 local_name: Some(local_name),
820 target_path: None,
821 target_name: None,
822 });
823 }
824 } else {
825 for name in js_exported_declaration_names(node, source) {
828 facts.exports.push(SymbolExportFact {
829 file_path: abs_path.to_path_buf(),
830 exported_name: name.clone(),
831 line,
832 local_name: Some(name),
833 target_path: None,
834 target_name: None,
835 });
836 }
837 }
838 }
839
840 let mut bodies: Vec<tree_sitter::Node> = Vec::new();
842 for i in 0..root.child_count() {
843 let child = match root.child(i) {
844 Some(c) => c,
845 None => continue,
846 };
847 match child.kind() {
848 "function_declaration" => {
849 if let Some(body) = child.child_by_field_name("body") {
850 bodies.push(body);
851 }
852 }
853 "lexical_declaration" => {
854 for j in 0..child.child_count() {
855 if let Some(declarator) = child.child(j) {
856 if declarator.kind() != "variable_declarator" {
857 continue;
858 }
859 if let Some(value) = declarator.child_by_field_name("value") {
860 if value.kind() == "arrow_function" {
861 if let Some(name_n) = declarator.child_by_field_name("name") {
863 let const_name = node_text(name_n, source).to_string();
864 let source_id = make_id(&[&file_stem, &const_name]);
865 let arrow_body = value.child_by_field_name("body");
867 let body_node = arrow_body.unwrap_or(value);
868 collect_call_uses(
869 body_node, source, &source_id, abs_path, &rel_str, facts,
870 );
871 }
872 continue;
873 }
874 if value.kind() == "function" {
875 if let Some(name_n) = declarator.child_by_field_name("name") {
876 let const_name = node_text(name_n, source).to_string();
877 let source_id = make_id(&[&file_stem, &const_name]);
878 if let Some(body) = value.child_by_field_name("body") {
879 collect_call_uses(
880 body, source, &source_id, abs_path, &rel_str, facts,
881 );
882 }
883 }
884 }
885 }
886 }
887 }
888 }
889 "export_statement" => {
890 if let Some(decl) = child.child_by_field_name("declaration") {
891 match decl.kind() {
892 "function_declaration" => {
893 if let Some(name_n) = decl.child_by_field_name("name") {
894 let fn_name = node_text(name_n, source).to_string();
895 let source_id = make_id(&[&file_stem, &fn_name]);
896 if let Some(body) = decl.child_by_field_name("body") {
897 collect_call_uses(
898 body, source, &source_id, abs_path, &rel_str, facts,
899 );
900 }
901 }
902 }
903 "lexical_declaration" => {
904 for j in 0..decl.child_count() {
905 if let Some(declarator) = decl.child(j) {
906 if declarator.kind() != "variable_declarator" {
907 continue;
908 }
909 if let Some(value) = declarator.child_by_field_name("value") {
910 if value.kind() == "arrow_function"
911 || value.kind() == "function"
912 {
913 if let Some(name_n) =
914 declarator.child_by_field_name("name")
915 {
916 let const_name =
917 node_text(name_n, source).to_string();
918 let source_id = make_id(&[&file_stem, &const_name]);
919 let body_node = value
920 .child_by_field_name("body")
921 .unwrap_or(value);
922 collect_call_uses(
923 body_node, source, &source_id, abs_path,
924 &rel_str, facts,
925 );
926 }
927 }
928 }
929 }
930 }
931 }
932 _ => {}
933 }
934 }
935 }
936 _ => {}
937 }
938 }
939
940 for i in 0..root.child_count() {
942 let child = match root.child(i) {
943 Some(c) => c,
944 None => continue,
945 };
946 let class_decl = match child.kind() {
947 "class_declaration" | "abstract_class_declaration" | "interface_declaration" => {
948 Some(child)
949 }
950 "export_statement" => {
951 let decl = child.child_by_field_name("declaration");
952 decl.filter(|d| {
953 matches!(
954 d.kind(),
955 "class_declaration"
956 | "abstract_class_declaration"
957 | "interface_declaration"
958 )
959 })
960 }
961 _ => None,
962 };
963 if let Some(class_node) = class_decl {
964 let class_name = class_node
965 .child_by_field_name("name")
966 .map(|n| node_text(n, source).to_string())
967 .unwrap_or_default();
968 if class_name.is_empty() {
969 continue;
970 }
971 let class_nid = make_id(&[&file_stem, &class_name]);
972
973 for j in 0..class_node.child_count() {
975 if let Some(heritage_node) = class_node.child(j) {
976 match heritage_node.kind() {
977 "class_heritage" => {
978 for k in 0..heritage_node.child_count() {
980 if let Some(clause) = heritage_node.child(k) {
981 match clause.kind() {
982 "extends_clause" => {
983 for name in ts_heritage_clause_entries(clause, source) {
984 facts.uses.push(SymbolUseFact {
985 file_path: abs_path.to_path_buf(),
986 source_id: class_nid.clone(),
987 local_name: name,
988 relation: "inherits".to_string(),
989 context: "extends".to_string(),
990 line: clause.start_position().row,
991 });
992 }
993 }
994 "implements_clause" => {
995 for name in ts_heritage_clause_entries(clause, source) {
996 facts.uses.push(SymbolUseFact {
997 file_path: abs_path.to_path_buf(),
998 source_id: class_nid.clone(),
999 local_name: name,
1000 relation: "implements".to_string(),
1001 context: "implements".to_string(),
1002 line: clause.start_position().row,
1003 });
1004 }
1005 }
1006 _ => {}
1007 }
1008 }
1009 }
1010 }
1011 "extends_clause" => {
1012 for name in ts_heritage_clause_entries(heritage_node, source) {
1013 facts.uses.push(SymbolUseFact {
1014 file_path: abs_path.to_path_buf(),
1015 source_id: class_nid.clone(),
1016 local_name: name,
1017 relation: "inherits".to_string(),
1018 context: "extends".to_string(),
1019 line: heritage_node.start_position().row,
1020 });
1021 }
1022 }
1023 "implements_clause" => {
1024 for name in ts_heritage_clause_entries(heritage_node, source) {
1025 facts.uses.push(SymbolUseFact {
1026 file_path: abs_path.to_path_buf(),
1027 source_id: class_nid.clone(),
1028 local_name: name,
1029 relation: "implements".to_string(),
1030 context: "implements".to_string(),
1031 line: heritage_node.start_position().row,
1032 });
1033 }
1034 }
1035 _ => {}
1036 }
1037 }
1038 }
1039
1040 if let Some(body) = class_node.child_by_field_name("body") {
1042 for k in 0..body.child_count() {
1043 let member = match body.child(k) {
1044 Some(m) => m,
1045 None => continue,
1046 };
1047 match member.kind() {
1048 "method_definition" | "method_signature" | "abstract_method_signature" => {
1049 let method_name = member
1050 .child_by_field_name("name")
1051 .map(|n| node_text(n, source).to_string())
1052 .unwrap_or_default();
1053 if method_name.is_empty() || method_name == "constructor" {
1054 continue;
1055 }
1056 let method_nid = make_id(&[&class_nid, &method_name]);
1057
1058 if let Some(params) = member.child_by_field_name("parameters") {
1060 for m in walk_nodes(params) {
1061 match m.kind() {
1062 "required_parameter" | "optional_parameter" => {
1063 if let Some(type_ann) = m.child_by_field_name("type") {
1064 let mut refs = Vec::new();
1065 ts_collect_type_refs(
1066 type_ann, source, false, &mut refs,
1067 );
1068 for (type_name, context) in refs {
1069 facts.uses.push(SymbolUseFact {
1070 file_path: abs_path.to_path_buf(),
1071 source_id: method_nid.clone(),
1072 local_name: type_name,
1073 relation: "references".to_string(),
1074 context: if context == "generic_arg" {
1075 "generic_arg".to_string()
1076 } else {
1077 "parameter_type".to_string()
1078 },
1079 line: m.start_position().row,
1080 });
1081 }
1082 }
1083 }
1084 _ => {}
1085 }
1086 }
1087 }
1088
1089 if let Some(ret_type) = member.child_by_field_name("return_type") {
1091 let mut refs = Vec::new();
1092 ts_collect_type_refs(ret_type, source, false, &mut refs);
1093 for (type_name, context) in refs {
1094 facts.uses.push(SymbolUseFact {
1095 file_path: abs_path.to_path_buf(),
1096 source_id: method_nid.clone(),
1097 local_name: type_name,
1098 relation: "references".to_string(),
1099 context: if context == "generic_arg" {
1100 "generic_arg".to_string()
1101 } else {
1102 "return_type".to_string()
1103 },
1104 line: member.start_position().row,
1105 });
1106 }
1107 }
1108 }
1109 _ => {}
1110 }
1111 }
1112 }
1113 }
1114 }
1115
1116 let _ = canon_to_rel; }
1118
1119fn collect_call_uses(
1120 body: tree_sitter::Node<'_>,
1121 source: &[u8],
1122 source_id: &str,
1123 file_path: &Path,
1124 _rel_str: &str,
1125 facts: &mut SymbolResolutionFacts,
1126) {
1127 for node in walk_nodes(body) {
1128 if node.kind() != "call_expression" {
1129 continue;
1130 }
1131 if let Some(callee) = node.child_by_field_name("function") {
1132 if callee.kind() == "identifier" {
1133 let name = node_text(callee, source).to_string();
1134 if !name.is_empty() {
1135 facts.uses.push(SymbolUseFact {
1136 file_path: file_path.to_path_buf(),
1137 source_id: source_id.to_string(),
1138 local_name: name,
1139 relation: "calls".to_string(),
1140 context: "call".to_string(),
1141 line: node.start_position().row,
1142 });
1143 }
1144 }
1145 }
1146 }
1147}
1148
1149fn resolve_exported_origin(
1154 target_path: &Path,
1155 imported_name: &str,
1156 named_exports: &HashMap<PathBuf, HashMap<String, (PathBuf, String)>>,
1157 star_exports: &HashMap<PathBuf, Vec<PathBuf>>,
1158 symbol_nodes: &HashMap<(PathBuf, String), String>,
1159 seen: &mut HashSet<(PathBuf, String)>,
1160) -> (PathBuf, String) {
1161 let key = (target_path.to_path_buf(), imported_name.to_string());
1162 if seen.contains(&key) {
1163 return key;
1164 }
1165 seen.insert(key.clone());
1166
1167 if let Some(exports) = named_exports.get(target_path) {
1168 if let Some((origin_path, origin_name)) = exports.get(imported_name) {
1169 return resolve_exported_origin(
1170 origin_path,
1171 origin_name,
1172 named_exports,
1173 star_exports,
1174 symbol_nodes,
1175 seen,
1176 );
1177 }
1178 }
1179
1180 let empty_vec: Vec<PathBuf> = Vec::new();
1181 for star_target in star_exports.get(target_path).unwrap_or(&empty_vec) {
1182 let star_key = (star_target.clone(), imported_name.to_string());
1183 if symbol_nodes.contains_key(&star_key) {
1184 return star_key;
1185 }
1186 let resolved = resolve_exported_origin(
1187 star_target,
1188 imported_name,
1189 named_exports,
1190 star_exports,
1191 symbol_nodes,
1192 seen,
1193 );
1194 if symbol_nodes.contains_key(&resolved) {
1195 return resolved;
1196 }
1197 }
1198
1199 key
1200}
1201
1202fn apply_js_symbol_resolution_facts(
1207 facts: &SymbolResolutionFacts,
1208 nodes: &mut Vec<Node>,
1209 edges: &mut Vec<Edge>,
1210 edge_set: &mut HashSet<(String, String, String)>,
1211 canon_to_rel: &HashMap<PathBuf, PathBuf>,
1212) {
1213 let mut symbol_nodes: HashMap<(PathBuf, String), String> = HashMap::new();
1215 for node in nodes.iter() {
1216 if node.file_type != "code" {
1217 continue;
1218 }
1219 let source_file_path = PathBuf::from(&node.source_file);
1223 let canon_path = canon_to_rel
1225 .iter()
1226 .find(|(_, rel)| *rel == &source_file_path)
1227 .map(|(canon, _)| canon.clone());
1228 if let Some(canon) = canon_path {
1229 let label = node
1231 .label
1232 .trim_start_matches('.')
1233 .trim_end_matches("()")
1234 .to_string();
1235 if !label.is_empty() {
1236 symbol_nodes.insert((canon, label), node.id.clone());
1237 }
1238 }
1239 }
1240
1241 let mut file_id_by_canon: HashMap<PathBuf, String> = HashMap::new();
1243 for (canon, rel) in canon_to_rel.iter() {
1244 let rel_str = rel.to_string_lossy().to_string();
1245 let file_id = make_id(&[&rel_str]);
1246 file_id_by_canon.insert(canon.clone(), file_id);
1247 }
1248
1249 let ensure_symbol_node = |canon_path: &PathBuf,
1252 name: &str,
1253 line: usize,
1254 symbol_nodes: &mut HashMap<(PathBuf, String), String>,
1255 nodes: &mut Vec<Node>| {
1256 let key = (canon_path.clone(), name.to_string());
1257 if let Some(id) = symbol_nodes.get(&key) {
1258 return id.clone();
1259 }
1260 let rel_path = match canon_to_rel.get(canon_path) {
1261 Some(r) => r,
1262 None => return String::new(),
1263 };
1264 let rel_str = rel_path.to_string_lossy().to_string();
1265 let stem = js_file_stem(&rel_str);
1266 let nid = make_id(&[&stem, name]);
1267 if !nodes.iter().any(|n| n.id == nid) {
1268 nodes.push(make_node(
1269 nid.clone(),
1270 name.to_string(),
1271 rel_str,
1272 Some(line),
1273 ));
1274 }
1275 symbol_nodes.insert(key, nid.clone());
1276 nid
1277 };
1278
1279 for decl in &facts.declarations {
1281 ensure_symbol_node(
1282 &decl.file_path,
1283 &decl.name,
1284 decl.line,
1285 &mut symbol_nodes,
1286 nodes,
1287 );
1288 }
1289
1290 let mut local_aliases_by_file: HashMap<PathBuf, HashMap<String, (PathBuf, String)>> =
1292 HashMap::new();
1293 for imp in &facts.imports {
1294 local_aliases_by_file
1295 .entry(imp.file_path.clone())
1296 .or_default()
1297 .insert(
1298 imp.local_name.clone(),
1299 (imp.target_path.clone(), imp.imported_name.clone()),
1300 );
1301 }
1302
1303 for alias in &facts.aliases {
1305 let local_aliases = match local_aliases_by_file.get(&alias.file_path) {
1306 Some(m) => m,
1307 None => continue,
1308 };
1309 if let Some(origin) = local_aliases.get(&alias.target_name).cloned() {
1310 local_aliases_by_file
1311 .entry(alias.file_path.clone())
1312 .or_default()
1313 .insert(alias.alias.clone(), origin);
1314 }
1315 }
1316
1317 let mut named_exports_by_file: HashMap<PathBuf, HashMap<String, (PathBuf, String)>> =
1319 HashMap::new();
1320 for exp in &facts.exports {
1321 let origin =
1322 if let (Some(target_path), Some(target_name)) = (&exp.target_path, &exp.target_name) {
1323 (target_path.clone(), target_name.clone())
1325 } else if let Some(local_name) = &exp.local_name {
1326 let local_aliases = local_aliases_by_file.get(&exp.file_path);
1329 if let Some(aliases) = local_aliases {
1330 if let Some((origin_path, origin_name)) = aliases.get(local_name) {
1331 (origin_path.clone(), origin_name.clone())
1332 } else {
1333 (exp.file_path.clone(), local_name.clone())
1335 }
1336 } else {
1337 (exp.file_path.clone(), local_name.clone())
1338 }
1339 } else {
1340 continue;
1341 };
1342
1343 named_exports_by_file
1344 .entry(exp.file_path.clone())
1345 .or_default()
1346 .insert(exp.exported_name.clone(), origin);
1347 }
1348
1349 let mut star_exports_by_file: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
1351 for se in &facts.star_exports {
1352 star_exports_by_file
1353 .entry(se.file_path.clone())
1354 .or_default()
1355 .push(se.target_path.clone());
1356 }
1357
1358 for se in &facts.star_exports {
1361 if let (Some(src_id), Some(tgt_id)) = (
1362 file_id_by_canon.get(&se.file_path),
1363 file_id_by_canon.get(&se.target_path),
1364 ) {
1365 let ek = (src_id.clone(), tgt_id.clone(), "re_exports".to_string());
1366 if edge_set.insert(ek) {
1367 edges.push(make_edge(
1368 src_id.clone(),
1369 tgt_id.clone(),
1370 "re_exports".to_string(),
1371 None,
1372 ));
1373 }
1374 }
1375 }
1376
1377 for (file_path, exports) in &named_exports_by_file {
1379 let src_id = match file_id_by_canon.get(file_path) {
1380 Some(id) => id.clone(),
1381 None => continue,
1382 };
1383 for (origin_path, _) in exports.values() {
1384 if origin_path != file_path {
1385 if let Some(tgt_id) = file_id_by_canon.get(origin_path) {
1386 let ek = (src_id.clone(), tgt_id.clone(), "re_exports".to_string());
1387 if edge_set.insert(ek) {
1388 edges.push(make_edge(
1389 src_id.clone(),
1390 tgt_id.clone(),
1391 "re_exports".to_string(),
1392 None,
1393 ));
1394 }
1395 }
1396 }
1397 }
1398 }
1399
1400 for imp in &facts.imports {
1402 let src_file_id = match file_id_by_canon.get(&imp.file_path) {
1403 Some(id) => id.clone(),
1404 None => continue,
1405 };
1406 let mut seen = HashSet::new();
1407 let (origin_path, origin_name) = resolve_exported_origin(
1408 &imp.target_path,
1409 &imp.imported_name,
1410 &named_exports_by_file,
1411 &star_exports_by_file,
1412 &symbol_nodes,
1413 &mut seen,
1414 );
1415
1416 ensure_symbol_node(
1418 &origin_path,
1419 &origin_name,
1420 imp.line,
1421 &mut symbol_nodes,
1422 nodes,
1423 );
1424
1425 if let Some(tgt_sym_id) = symbol_nodes.get(&(origin_path.clone(), origin_name.clone())) {
1426 let tgt_id = tgt_sym_id.clone();
1427 let ek = (src_file_id.clone(), tgt_id.clone(), "imports".to_string());
1428 if edge_set.insert(ek) {
1429 edges.push(make_edge(
1430 src_file_id.clone(),
1431 tgt_id,
1432 "imports".to_string(),
1433 None,
1434 ));
1435 }
1436 }
1437 }
1438
1439 for use_fact in &facts.uses {
1441 let src_id = use_fact.source_id.clone();
1442 let local_name = &use_fact.local_name;
1443
1444 let (origin_path, origin_name) =
1446 if let Some(aliases) = local_aliases_by_file.get(&use_fact.file_path) {
1447 if let Some((target_path, imported_name)) = aliases.get(local_name) {
1448 let mut seen = HashSet::new();
1449 resolve_exported_origin(
1450 target_path,
1451 imported_name,
1452 &named_exports_by_file,
1453 &star_exports_by_file,
1454 &symbol_nodes,
1455 &mut seen,
1456 )
1457 } else {
1458 (use_fact.file_path.clone(), local_name.clone())
1460 }
1461 } else {
1462 (use_fact.file_path.clone(), local_name.clone())
1463 };
1464
1465 ensure_symbol_node(
1467 &origin_path,
1468 &origin_name,
1469 use_fact.line,
1470 &mut symbol_nodes,
1471 nodes,
1472 );
1473
1474 if let Some(tgt_id) = symbol_nodes.get(&(origin_path, origin_name)) {
1475 let tgt = tgt_id.clone();
1476 if src_id == tgt {
1478 continue;
1479 }
1480 let ek = (src_id.clone(), tgt.clone(), use_fact.relation.clone());
1481 if edge_set.insert(ek) {
1482 edges.push(make_edge(src_id, tgt, use_fact.relation.clone(), None));
1483 }
1484 }
1485 }
1486}
1487
1488pub fn extract_js_files(paths: &[PathBuf], root: &Path) -> (Vec<Node>, Vec<Edge>) {
1493 let mut canon_to_rel: HashMap<PathBuf, PathBuf> = HashMap::new();
1495 for path in paths {
1496 if !is_js_ts_file(path) {
1497 continue;
1498 }
1499 let canon = std::fs::canonicalize(path)
1500 .ok()
1501 .unwrap_or_else(|| path.clone());
1502 let rel = path
1503 .strip_prefix(root)
1504 .map(|r| r.to_path_buf())
1505 .unwrap_or_else(|_| path.clone());
1506 canon_to_rel.insert(canon, rel);
1507 }
1508
1509 let mut nodes: Vec<Node> = Vec::new();
1510 let mut edges: Vec<Edge> = Vec::new();
1511 let mut edge_set: HashSet<(String, String, String)> = HashSet::new();
1512
1513 for (canon, rel) in &canon_to_rel {
1515 let source = match std::fs::read(canon) {
1516 Ok(s) => s,
1517 Err(_) => continue,
1518 };
1519 extract_js_ts_file_nodes(canon, rel, &source, &mut nodes, &mut edges, &mut edge_set);
1520 }
1521
1522 let mut facts = SymbolResolutionFacts::default();
1524 for (canon, rel) in &canon_to_rel {
1525 let source = match std::fs::read(canon) {
1526 Ok(s) => s,
1527 Err(_) => continue,
1528 };
1529 collect_js_symbol_resolution_facts(canon, rel, &source, &mut facts, &canon_to_rel);
1530 }
1531
1532 apply_js_symbol_resolution_facts(&facts, &mut nodes, &mut edges, &mut edge_set, &canon_to_rel);
1534
1535 (nodes, edges)
1536}
1537
1538#[cfg(test)]
1543mod tests {
1544 use super::*;
1545 use std::fs;
1546 use tempfile::TempDir;
1547
1548 fn tmp() -> TempDir {
1549 tempfile::TempDir::new().unwrap()
1550 }
1551
1552 fn write(dir: &Path, name: &str, content: &str) -> PathBuf {
1553 let path = dir.join(name);
1554 if let Some(parent) = path.parent() {
1555 fs::create_dir_all(parent).unwrap();
1556 }
1557 fs::write(&path, content).unwrap();
1558 path
1559 }
1560
1561 fn run(dir: &Path, paths: &[PathBuf]) -> (Vec<Node>, Vec<Edge>) {
1562 extract_js_files(paths, dir)
1563 }
1564
1565 fn make_test_id(s: &str) -> String {
1566 s.chars()
1567 .map(|c| {
1568 if c.is_alphanumeric() || c == '_' {
1569 c
1570 } else {
1571 '_'
1572 }
1573 })
1574 .collect::<String>()
1575 .to_lowercase()
1576 }
1577
1578 fn test_file_stem(rel_path: &str) -> String {
1579 js_file_stem(rel_path)
1580 }
1581
1582 fn has_node(nodes: &[Node], id: &str) -> bool {
1583 let nid = make_test_id(id);
1584 nodes.iter().any(|n| n.id == nid)
1585 }
1586
1587 fn has_edge(edges: &[Edge], source: &str, target: &str, relation: &str) -> bool {
1588 let src = make_test_id(source);
1589 let tgt = make_test_id(target);
1590 edges
1591 .iter()
1592 .any(|e| e.source == src && e.target == tgt && e.relation == relation)
1593 }
1594
1595 fn has_symbol_edge(
1597 edges: &[Edge],
1598 source_file: &str,
1599 target_file: &str,
1600 symbol: &str,
1601 relation: &str,
1602 ) -> bool {
1603 let src = make_test_id(source_file);
1604 let stem = test_file_stem(target_file);
1605 let tgt = make_test_id(&format!("{}_{}", stem, symbol));
1606 edges
1607 .iter()
1608 .any(|e| e.source == src && e.target == tgt && e.relation == relation)
1609 }
1610
1611 fn has_symbol_to_symbol_edge(
1612 edges: &[Edge],
1613 src_file: &str,
1614 src_sym: &str,
1615 tgt_file: &str,
1616 tgt_sym: &str,
1617 relation: &str,
1618 ) -> bool {
1619 let src_stem = test_file_stem(src_file);
1620 let tgt_stem = test_file_stem(tgt_file);
1621 let src = make_test_id(&format!("{}_{}", src_stem, src_sym));
1622 let tgt = make_test_id(&format!("{}_{}", tgt_stem, tgt_sym));
1623 edges
1624 .iter()
1625 .any(|e| e.source == src && e.target == tgt && e.relation == relation)
1626 }
1627
1628 #[test]
1630 fn test_ts_exported_function_node() {
1631 let dir = tmp();
1632 write(
1633 dir.path(),
1634 "src/lib/foo.ts",
1635 "export function Foo() { return 42; }",
1636 );
1637 let paths = vec![dir.path().join("src/lib/foo.ts")];
1638 let (nodes, _edges) = run(dir.path(), &paths);
1639 assert!(
1641 has_node(&nodes, "src/lib/foo.ts"),
1642 "file node missing; nodes: {:?}",
1643 nodes.iter().map(|n| &n.id).collect::<Vec<_>>()
1644 );
1645 let sym_id = make_test_id(&format!("{}_Foo", test_file_stem("src/lib/foo.ts")));
1647 assert!(
1648 nodes.iter().any(|n| n.id == sym_id),
1649 "symbol node missing: {}; nodes: {:?}",
1650 sym_id,
1651 nodes.iter().map(|n| &n.id).collect::<Vec<_>>()
1652 );
1653 }
1654
1655 #[test]
1657 fn test_js_file_stem_with_parent() {
1658 assert_eq!(test_file_stem("src/lib/foo.ts"), "lib.foo");
1659 assert_eq!(test_file_stem("src/routes/page.ts"), "routes.page");
1660 assert_eq!(test_file_stem("foo.ts"), "foo");
1661 }
1662
1663 #[test]
1665 fn test_ts_named_import_to_origin() {
1666 let dir = tmp();
1667 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
1668 write(
1669 dir.path(),
1670 "src/routes/page.ts",
1671 "import { Foo } from '../lib/foo';\nexport function handler() { return Foo(); }",
1672 );
1673 let paths = vec![
1674 dir.path().join("src/lib/foo.ts"),
1675 dir.path().join("src/routes/page.ts"),
1676 ];
1677 let (_, edges) = run(dir.path(), &paths);
1678 assert!(
1679 has_symbol_edge(
1680 &edges,
1681 "src/routes/page.ts",
1682 "src/lib/foo.ts",
1683 "Foo",
1684 "imports"
1685 ),
1686 "imports edge missing; edges: {:?}",
1687 edges
1688 .iter()
1689 .map(|e| (&e.source, &e.target, &e.relation))
1690 .collect::<Vec<_>>()
1691 );
1692 }
1693
1694 #[test]
1696 fn test_ts_named_import_through_barrel() {
1697 let dir = tmp();
1698 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
1699 write(
1700 dir.path(),
1701 "src/lib/index.ts",
1702 "export { Foo } from './foo';",
1703 );
1704 write(
1705 dir.path(),
1706 "src/routes/page.ts",
1707 "import { Foo } from '../lib/index';",
1708 );
1709 let paths = vec![
1710 dir.path().join("src/lib/foo.ts"),
1711 dir.path().join("src/lib/index.ts"),
1712 dir.path().join("src/routes/page.ts"),
1713 ];
1714 let (_, edges) = run(dir.path(), &paths);
1715 assert!(
1716 has_symbol_edge(
1717 &edges,
1718 "src/routes/page.ts",
1719 "src/lib/foo.ts",
1720 "Foo",
1721 "imports"
1722 ),
1723 "barrel import edge missing; edges: {:?}",
1724 edges
1725 .iter()
1726 .map(|e| (&e.source, &e.target, &e.relation))
1727 .collect::<Vec<_>>()
1728 );
1729 }
1730
1731 #[test]
1733 fn test_ts_star_export_barrel() {
1734 let dir = tmp();
1735 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
1736 write(dir.path(), "src/lib/index.ts", "export * from './foo';");
1737 write(
1738 dir.path(),
1739 "src/routes/page.ts",
1740 "import { Foo } from '../lib/index';",
1741 );
1742 let paths = vec![
1743 dir.path().join("src/lib/foo.ts"),
1744 dir.path().join("src/lib/index.ts"),
1745 dir.path().join("src/routes/page.ts"),
1746 ];
1747 let (_, edges) = run(dir.path(), &paths);
1748 assert!(
1749 has_symbol_edge(
1750 &edges,
1751 "src/routes/page.ts",
1752 "src/lib/foo.ts",
1753 "Foo",
1754 "imports"
1755 ),
1756 "star barrel import edge missing; edges: {:?}",
1757 edges
1758 .iter()
1759 .map(|e| (&e.source, &e.target, &e.relation))
1760 .collect::<Vec<_>>()
1761 );
1762 }
1763
1764 #[test]
1766 fn test_ts_barrel_re_exports_edge() {
1767 let dir = tmp();
1768 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
1769 write(
1770 dir.path(),
1771 "src/lib/index.ts",
1772 "export { Foo } from './foo';",
1773 );
1774 let paths = vec![
1775 dir.path().join("src/lib/foo.ts"),
1776 dir.path().join("src/lib/index.ts"),
1777 ];
1778 let (_, edges) = run(dir.path(), &paths);
1779 assert!(
1780 has_edge(&edges, "src/lib/index.ts", "src/lib/foo.ts", "re_exports"),
1781 "re_exports edge missing; edges: {:?}",
1782 edges
1783 .iter()
1784 .map(|e| (&e.source, &e.target, &e.relation))
1785 .collect::<Vec<_>>()
1786 );
1787 }
1788
1789 #[test]
1791 fn test_ts_star_re_exports_edge() {
1792 let dir = tmp();
1793 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
1794 write(dir.path(), "src/lib/index.ts", "export * from './foo';");
1795 let paths = vec![
1796 dir.path().join("src/lib/foo.ts"),
1797 dir.path().join("src/lib/index.ts"),
1798 ];
1799 let (_, edges) = run(dir.path(), &paths);
1800 assert!(
1801 has_edge(&edges, "src/lib/index.ts", "src/lib/foo.ts", "re_exports"),
1802 "star re_exports edge missing; edges: {:?}",
1803 edges
1804 .iter()
1805 .map(|e| (&e.source, &e.target, &e.relation))
1806 .collect::<Vec<_>>()
1807 );
1808 }
1809
1810 #[test]
1812 fn test_ts_class_inherits() {
1813 let dir = tmp();
1814 write(dir.path(), "src/lib/base.ts", "export class Base {}");
1815 write(
1816 dir.path(),
1817 "src/lib/impl.ts",
1818 "import { Base } from './base';\nexport class DataProcessor extends Base {}",
1819 );
1820 let paths = vec![
1821 dir.path().join("src/lib/base.ts"),
1822 dir.path().join("src/lib/impl.ts"),
1823 ];
1824 let (_, edges) = run(dir.path(), &paths);
1825 assert!(
1826 has_symbol_to_symbol_edge(
1827 &edges,
1828 "src/lib/impl.ts",
1829 "DataProcessor",
1830 "src/lib/base.ts",
1831 "Base",
1832 "inherits"
1833 ),
1834 "inherits edge missing; edges: {:?}",
1835 edges
1836 .iter()
1837 .map(|e| (&e.source, &e.target, &e.relation))
1838 .collect::<Vec<_>>()
1839 );
1840 }
1841
1842 #[test]
1844 fn test_ts_class_implements() {
1845 let dir = tmp();
1846 write(
1847 dir.path(),
1848 "src/lib/iface.ts",
1849 "export interface IProcessor {}",
1850 );
1851 write(
1852 dir.path(),
1853 "src/lib/impl.ts",
1854 "import { IProcessor } from './iface';\nexport class DataProcessor implements IProcessor {}",
1855 );
1856 let paths = vec![
1857 dir.path().join("src/lib/iface.ts"),
1858 dir.path().join("src/lib/impl.ts"),
1859 ];
1860 let (_, edges) = run(dir.path(), &paths);
1861 assert!(
1862 has_symbol_to_symbol_edge(
1863 &edges,
1864 "src/lib/impl.ts",
1865 "DataProcessor",
1866 "src/lib/iface.ts",
1867 "IProcessor",
1868 "implements"
1869 ),
1870 "implements edge missing; edges: {:?}",
1871 edges
1872 .iter()
1873 .map(|e| (&e.source, &e.target, &e.relation))
1874 .collect::<Vec<_>>()
1875 );
1876 }
1877
1878 #[test]
1880 fn test_ts_method_parameter_type_ref() {
1881 let dir = tmp();
1882 write(dir.path(), "src/lib/types.ts", "export interface Config {}");
1883 write(
1884 dir.path(),
1885 "src/lib/impl.ts",
1886 "import { Config } from './types';\nexport class DataProcessor {\n run(cfg: Config): void {}\n}",
1887 );
1888 let paths = vec![
1889 dir.path().join("src/lib/types.ts"),
1890 dir.path().join("src/lib/impl.ts"),
1891 ];
1892 let (_, edges) = run(dir.path(), &paths);
1893 let method_id = make_test_id("lib_impl_DataProcessor_run");
1895 let cfg_id = make_test_id(&format!("{}_Config", test_file_stem("src/lib/types.ts")));
1896 assert!(
1897 edges
1898 .iter()
1899 .any(|e| e.source == method_id && e.target == cfg_id && e.relation == "references"),
1900 "references edge missing; edges: {:?}",
1901 edges
1902 .iter()
1903 .map(|e| (&e.source, &e.target, &e.relation))
1904 .collect::<Vec<_>>()
1905 );
1906 }
1907
1908 #[test]
1910 fn test_ts_method_return_type_ref() {
1911 let dir = tmp();
1912 write(dir.path(), "src/lib/types.ts", "export interface Result {}");
1913 write(
1914 dir.path(),
1915 "src/lib/impl.ts",
1916 "import { Result } from './types';\nexport class DataProcessor {\n run(): Result {}\n}",
1917 );
1918 let paths = vec![
1919 dir.path().join("src/lib/types.ts"),
1920 dir.path().join("src/lib/impl.ts"),
1921 ];
1922 let (_, edges) = run(dir.path(), &paths);
1923 let method_id = make_test_id("lib_impl_DataProcessor_run");
1924 let result_id = make_test_id(&format!("{}_Result", test_file_stem("src/lib/types.ts")));
1925 assert!(
1926 edges.iter().any(|e| e.source == method_id
1927 && e.target == result_id
1928 && e.relation == "references"),
1929 "return type references edge missing; edges: {:?}",
1930 edges
1931 .iter()
1932 .map(|e| (&e.source, &e.target, &e.relation))
1933 .collect::<Vec<_>>()
1934 );
1935 }
1936
1937 #[test]
1939 fn test_ts_arrow_function_call_through_barrel_targets_origin_symbol() {
1940 let dir = tmp();
1941 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
1942 write(
1943 dir.path(),
1944 "src/lib/index.ts",
1945 "export { Foo } from './foo';",
1946 );
1947 write(
1948 dir.path(),
1949 "src/routes/page.ts",
1950 "import { Foo } from '../lib/index';\nconst X = () => Foo();",
1951 );
1952 let paths = vec![
1953 dir.path().join("src/lib/foo.ts"),
1954 dir.path().join("src/lib/index.ts"),
1955 dir.path().join("src/routes/page.ts"),
1956 ];
1957 let (_, edges) = run(dir.path(), &paths);
1958 assert!(
1959 has_symbol_to_symbol_edge(
1960 &edges,
1961 "src/routes/page.ts",
1962 "X",
1963 "src/lib/foo.ts",
1964 "Foo",
1965 "calls"
1966 ),
1967 "calls edge missing; edges: {:?}",
1968 edges
1969 .iter()
1970 .map(|e| (&e.source, &e.target, &e.relation))
1971 .collect::<Vec<_>>()
1972 );
1973 }
1974
1975 #[test]
1977 fn test_ts_function_call_direct_import() {
1978 let dir = tmp();
1979 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
1980 write(
1981 dir.path(),
1982 "src/routes/page.ts",
1983 "import { Foo } from '../lib/foo';\nexport function handler() { return Foo(); }",
1984 );
1985 let paths = vec![
1986 dir.path().join("src/lib/foo.ts"),
1987 dir.path().join("src/routes/page.ts"),
1988 ];
1989 let (_, edges) = run(dir.path(), &paths);
1990 assert!(
1991 has_symbol_to_symbol_edge(
1992 &edges,
1993 "src/routes/page.ts",
1994 "handler",
1995 "src/lib/foo.ts",
1996 "Foo",
1997 "calls"
1998 ),
1999 "calls edge missing; edges: {:?}",
2000 edges
2001 .iter()
2002 .map(|e| (&e.source, &e.target, &e.relation))
2003 .collect::<Vec<_>>()
2004 );
2005 }
2006
2007 #[test]
2009 fn test_ts_function_call_through_star_barrel() {
2010 let dir = tmp();
2011 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
2012 write(dir.path(), "src/lib/index.ts", "export * from './foo';");
2013 write(
2014 dir.path(),
2015 "src/routes/page.ts",
2016 "import { Foo } from '../lib/index';\nexport function handler() { return Foo(); }",
2017 );
2018 let paths = vec![
2019 dir.path().join("src/lib/foo.ts"),
2020 dir.path().join("src/lib/index.ts"),
2021 dir.path().join("src/routes/page.ts"),
2022 ];
2023 let (_, edges) = run(dir.path(), &paths);
2024 assert!(
2025 has_symbol_to_symbol_edge(
2026 &edges,
2027 "src/routes/page.ts",
2028 "handler",
2029 "src/lib/foo.ts",
2030 "Foo",
2031 "calls"
2032 ),
2033 "calls edge missing; edges: {:?}",
2034 edges
2035 .iter()
2036 .map(|e| (&e.source, &e.target, &e.relation))
2037 .collect::<Vec<_>>()
2038 );
2039 }
2040
2041 #[test]
2043 fn test_ts_svelte_ts_import() {
2044 let dir = tmp();
2045 write(
2046 dir.path(),
2047 "src/lib/hooks/is-mobile.svelte.ts",
2048 "export function isMobile() { return false; }",
2049 );
2050 write(
2051 dir.path(),
2052 "src/routes/page.ts",
2053 "import { isMobile } from '../lib/hooks/is-mobile.svelte';",
2054 );
2055 let paths = vec![
2056 dir.path().join("src/lib/hooks/is-mobile.svelte.ts"),
2057 dir.path().join("src/routes/page.ts"),
2058 ];
2059 let (_, edges) = run(dir.path(), &paths);
2060 let stem = test_file_stem("src/lib/hooks/is-mobile.svelte.ts");
2068 let sym_id = make_test_id(&format!("{}_isMobile", stem));
2069 let src_id = make_test_id("src/routes/page.ts");
2070 assert!(
2071 edges
2072 .iter()
2073 .any(|e| e.source == src_id && e.target == sym_id && e.relation == "imports"),
2074 "svelte.ts import edge missing; stem={}, sym_id={}, edges: {:?}",
2075 stem,
2076 sym_id,
2077 edges
2078 .iter()
2079 .map(|e| (&e.source, &e.target, &e.relation))
2080 .collect::<Vec<_>>()
2081 );
2082 }
2083
2084 #[test]
2086 fn test_ts_multiple_exports() {
2087 let dir = tmp();
2088 write(
2089 dir.path(),
2090 "src/lib/foo.ts",
2091 "export function Foo() {}\nexport function Bar() {}",
2092 );
2093 let paths = vec![dir.path().join("src/lib/foo.ts")];
2094 let (nodes, _) = run(dir.path(), &paths);
2095 let foo_id = make_test_id(&format!("{}_Foo", test_file_stem("src/lib/foo.ts")));
2096 let bar_id = make_test_id(&format!("{}_Bar", test_file_stem("src/lib/foo.ts")));
2097 assert!(nodes.iter().any(|n| n.id == foo_id), "Foo node missing");
2098 assert!(nodes.iter().any(|n| n.id == bar_id), "Bar node missing");
2099 }
2100
2101 #[test]
2103 fn test_ts_deep_chain_barrel() {
2104 let dir = tmp();
2105 write(dir.path(), "src/lib/foo.ts", "export function Foo() {}");
2106 write(
2107 dir.path(),
2108 "src/lib/index.ts",
2109 "export { Foo } from './foo';",
2110 );
2111 write(
2112 dir.path(),
2113 "src/routes/index.ts",
2114 "export { Foo } from '../lib/index';",
2115 );
2116 write(
2117 dir.path(),
2118 "src/app/main.ts",
2119 "import { Foo } from '../routes/index';",
2120 );
2121 let paths = vec![
2122 dir.path().join("src/lib/foo.ts"),
2123 dir.path().join("src/lib/index.ts"),
2124 dir.path().join("src/routes/index.ts"),
2125 dir.path().join("src/app/main.ts"),
2126 ];
2127 let (_, edges) = run(dir.path(), &paths);
2128 assert!(
2129 has_symbol_edge(
2130 &edges,
2131 "src/app/main.ts",
2132 "src/lib/foo.ts",
2133 "Foo",
2134 "imports"
2135 ),
2136 "deep chain barrel imports edge missing; edges: {:?}",
2137 edges
2138 .iter()
2139 .map(|e| (&e.source, &e.target, &e.relation))
2140 .collect::<Vec<_>>()
2141 );
2142 }
2143
2144 #[test]
2146 fn test_ts_interface_declaration_node() {
2147 let dir = tmp();
2148 write(
2149 dir.path(),
2150 "src/lib/types.ts",
2151 "export interface Config { key: string; }",
2152 );
2153 let paths = vec![dir.path().join("src/lib/types.ts")];
2154 let (nodes, _) = run(dir.path(), &paths);
2155 let sym_id = make_test_id(&format!("{}_Config", test_file_stem("src/lib/types.ts")));
2156 assert!(
2157 nodes.iter().any(|n| n.id == sym_id),
2158 "Config interface node missing; nodes: {:?}",
2159 nodes.iter().map(|n| &n.id).collect::<Vec<_>>()
2160 );
2161 }
2162
2163 #[test]
2165 fn test_ts_type_alias_node() {
2166 let dir = tmp();
2167 write(
2168 dir.path(),
2169 "src/lib/types.ts",
2170 "export type MyType = string | number;",
2171 );
2172 let paths = vec![dir.path().join("src/lib/types.ts")];
2173 let (nodes, _) = run(dir.path(), &paths);
2174 let sym_id = make_test_id(&format!("{}_MyType", test_file_stem("src/lib/types.ts")));
2175 assert!(
2176 nodes.iter().any(|n| n.id == sym_id),
2177 "MyType alias node missing; nodes: {:?}",
2178 nodes.iter().map(|n| &n.id).collect::<Vec<_>>()
2179 );
2180 }
2181
2182 #[test]
2184 fn test_js_import_resolution() {
2185 let dir = tmp();
2186 write(dir.path(), "src/lib/foo.js", "export function Foo() {}");
2187 write(
2188 dir.path(),
2189 "src/routes/page.js",
2190 "import { Foo } from '../lib/foo.js';\nexport function handler() { return Foo(); }",
2191 );
2192 let paths = vec![
2193 dir.path().join("src/lib/foo.js"),
2194 dir.path().join("src/routes/page.js"),
2195 ];
2196 let (_, edges) = run(dir.path(), &paths);
2197 assert!(
2198 has_symbol_edge(
2199 &edges,
2200 "src/routes/page.js",
2201 "src/lib/foo.js",
2202 "Foo",
2203 "imports"
2204 ),
2205 "JS imports edge missing; edges: {:?}",
2206 edges
2207 .iter()
2208 .map(|e| (&e.source, &e.target, &e.relation))
2209 .collect::<Vec<_>>()
2210 );
2211 }
2212
2213 #[test]
2215 fn test_ts_method_nodes_created() {
2216 let dir = tmp();
2217 write(
2218 dir.path(),
2219 "src/lib/impl.ts",
2220 "export class DataProcessor {\n run(): void {}\n stop(): void {}\n}",
2221 );
2222 let paths = vec![dir.path().join("src/lib/impl.ts")];
2223 let (nodes, _) = run(dir.path(), &paths);
2224 let class_nid = make_test_id(&format!(
2225 "{}_DataProcessor",
2226 test_file_stem("src/lib/impl.ts")
2227 ));
2228 let run_id = make_test_id(&format!("{}_run", class_nid));
2229 let stop_id = make_test_id(&format!("{}_stop", class_nid));
2230 assert!(
2231 nodes.iter().any(|n| n.id == run_id),
2232 "run method node missing; nodes: {:?}",
2233 nodes.iter().map(|n| &n.id).collect::<Vec<_>>()
2234 );
2235 assert!(
2236 nodes.iter().any(|n| n.id == stop_id),
2237 "stop method node missing; nodes: {:?}",
2238 nodes.iter().map(|n| &n.id).collect::<Vec<_>>()
2239 );
2240 }
2241}