1use crate::error::{CodeSynapseError, Result};
6use crate::extract::path_to_file_id;
7use crate::types::{Edge, ExtractionFragment, Node, NodeId};
8use std::collections::HashMap;
9use std::path::Path;
10use tree_sitter::{Language, Node as TsNode, Parser, Query, QueryCursor, StreamingIterator};
11
12fn node_text<'a>(source: &'a [u8], node: &TsNode) -> Result<&'a str> {
13 let bytes = &source[node.start_byte()..node.end_byte()];
14 std::str::from_utf8(bytes).map_err(|e| CodeSynapseError::Parse(format!("invalid UTF-8: {}", e)))
15}
16
17fn make_file_node(path: &Path) -> (NodeId, String, Node) {
18 let file_label = path
19 .file_name()
20 .unwrap_or_default()
21 .to_string_lossy()
22 .to_string();
23 let file_id = path_to_file_id(path);
24 let node = Node {
25 id: file_id.clone(),
26 label: file_label.clone(),
27 file_type: "code".to_string(),
28 source_file: path.to_string_lossy().to_string(),
29 source_location: None,
30 community: None,
31 rationale: None,
32 docstring: None,
33 metadata: HashMap::new(),
34 };
35 (file_id, file_label, node)
36}
37
38fn add_contains_edge(
39 fragment: &mut ExtractionFragment,
40 file_id: &NodeId,
41 target_id: NodeId,
42 path: &Path,
43) {
44 fragment.edges.push(Edge {
45 source: file_id.clone(),
46 target: target_id,
47 relation: "contains".to_string(),
48 confidence: "EXTRACTED".to_string(),
49 source_file: Some(path.to_string_lossy().to_string()),
50 weight: 1.0,
51 context: None,
52 });
53}
54
55fn add_node_if_missing(fragment: &mut ExtractionFragment, node: Node) {
56 if !fragment.nodes.iter().any(|n| n.id == node.id) {
57 fragment.nodes.push(node);
58 }
59}
60
61fn run_query_named(
62 source: &[u8],
63 language: &Language,
64 query_str: &str,
65) -> Result<HashMap<String, Vec<String>>> {
66 let mut parser = Parser::new();
67 parser
68 .set_language(language)
69 .map_err(|e| CodeSynapseError::Parse(format!("failed to set language: {}", e)))?;
70
71 let tree = parser
72 .parse(source, None)
73 .ok_or_else(|| CodeSynapseError::Parse("failed to parse source".to_string()))?;
74
75 let query = Query::new(language, query_str)
76 .map_err(|e| CodeSynapseError::Parse(format!("invalid query: {}", e)))?;
77
78 let capture_names = query.capture_names().to_vec();
79 let mut cursor = QueryCursor::new();
80 let root = tree.root_node();
81
82 let mut results: HashMap<String, Vec<String>> = HashMap::new();
83 for name in &capture_names {
84 results.insert(name.to_string(), Vec::new());
85 }
86
87 let mut matches = cursor.matches(&query, root, source);
88 matches.advance();
89 while let Some(m) = matches.get() {
90 for capture in m.captures.iter() {
91 let idx = capture.index as usize;
92 if idx < capture_names.len() {
93 let name = &capture_names[idx];
94 if let Some(texts) = results.get_mut(&name[..]) {
95 if let Ok(text) = node_text(source, &capture.node) {
96 texts.push(text.to_string());
97 }
98 }
99 }
100 }
101 matches.advance();
102 }
103 Ok(results)
104}
105
106#[allow(dead_code)]
107fn run_query_matches(
108 source: &[u8],
109 language: &Language,
110 query_str: &str,
111) -> Result<Vec<HashMap<String, String>>> {
112 let mut parser = Parser::new();
113 parser
114 .set_language(language)
115 .map_err(|e| CodeSynapseError::Parse(format!("failed to set language: {}", e)))?;
116
117 let tree = parser
118 .parse(source, None)
119 .ok_or_else(|| CodeSynapseError::Parse("failed to parse source".to_string()))?;
120
121 let query = Query::new(language, query_str)
122 .map_err(|e| CodeSynapseError::Parse(format!("invalid query: {}", e)))?;
123
124 let capture_names = query.capture_names().to_vec();
125 let mut cursor = QueryCursor::new();
126 let root = tree.root_node();
127
128 let mut all_matches: Vec<HashMap<String, String>> = Vec::new();
129
130 let mut matches = cursor.matches(&query, root, source);
131 matches.advance();
132 while let Some(m) = matches.get() {
133 let mut match_map: HashMap<String, String> = HashMap::new();
134 for capture in m.captures.iter() {
135 let idx = capture.index as usize;
136 if idx < capture_names.len() {
137 let name = capture_names[idx].to_string();
138 if let Ok(text) = node_text(source, &capture.node) {
139 match_map.insert(name, text.to_string());
140 }
141 }
142 }
143 all_matches.push(match_map);
144 matches.advance();
145 }
146 Ok(all_matches)
147}
148
149type RangedMatch = (HashMap<String, String>, HashMap<String, (usize, usize)>);
150
151fn run_query_matches_ranged(
152 source: &[u8],
153 language: &Language,
154 query_str: &str,
155) -> Result<Vec<RangedMatch>> {
156 let mut parser = Parser::new();
157 parser
158 .set_language(language)
159 .map_err(|e| CodeSynapseError::Parse(format!("failed to set language: {}", e)))?;
160
161 let tree = parser
162 .parse(source, None)
163 .ok_or_else(|| CodeSynapseError::Parse("failed to parse source".to_string()))?;
164
165 let query = Query::new(language, query_str)
166 .map_err(|e| CodeSynapseError::Parse(format!("invalid query: {}", e)))?;
167
168 let capture_names = query.capture_names().to_vec();
169 let mut cursor = QueryCursor::new();
170 let root = tree.root_node();
171
172 let mut all_matches: Vec<RangedMatch> = Vec::new();
173
174 let mut matches = cursor.matches(&query, root, source);
175 matches.advance();
176 while let Some(m) = matches.get() {
177 let mut text_map: HashMap<String, String> = HashMap::new();
178 let mut range_map: HashMap<String, (usize, usize)> = HashMap::new();
179 for capture in m.captures.iter() {
180 let idx = capture.index as usize;
181 if idx < capture_names.len() {
182 let name = capture_names[idx].to_string();
183 range_map.insert(
184 name.clone(),
185 (capture.node.start_byte(), capture.node.end_byte()),
186 );
187 if let Ok(text) = node_text(source, &capture.node) {
188 text_map.insert(name, text.to_string());
189 }
190 }
191 }
192 all_matches.push((text_map, range_map));
193 matches.advance();
194 }
195 Ok(all_matches)
196}
197
198pub(super) fn run_tree_walk_docstrings(
202 source: &[u8],
203 language: &Language,
204 item_kinds: &[&str],
205) -> HashMap<String, String> {
206 let mut out: HashMap<String, String> = HashMap::new();
207
208 let mut parser = Parser::new();
209 if parser.set_language(language).is_err() {
210 return out;
211 }
212 let tree = match parser.parse(source, None) {
213 Some(t) => t,
214 None => return out,
215 };
216
217 fn walk_node(
218 node: TsNode<'_>,
219 source: &[u8],
220 item_kinds: &[&str],
221 out: &mut HashMap<String, String>,
222 ) {
223 if item_kinds.contains(&node.kind()) {
224 if let Some(name_node) = node.child_by_field_name("name") {
225 if let Ok(name) =
226 std::str::from_utf8(&source[name_node.start_byte()..name_node.end_byte()])
227 {
228 let doc = node
229 .prev_named_sibling()
230 .filter(|s| s.kind() == "comment")
231 .and_then(|s| {
232 let text =
233 std::str::from_utf8(&source[s.start_byte()..s.end_byte()]).ok()?;
234 if text.starts_with("/**") {
235 strip_docstring(text)
236 } else {
237 None
238 }
239 });
240 if let Some(d) = doc {
241 out.insert(name.trim().to_string(), d);
242 }
243 }
244 }
245 }
246 let mut cursor = node.walk();
247 if cursor.goto_first_child() {
248 loop {
249 walk_node(cursor.node(), source, item_kinds, out);
250 if !cursor.goto_next_sibling() {
251 break;
252 }
253 }
254 }
255 }
256
257 walk_node(tree.root_node(), source, item_kinds, &mut out);
258 out
259}
260
261pub(super) fn strip_docstring(raw: &str) -> Option<String> {
264 let s = raw.trim();
265
266 let inner = if s.starts_with("\"\"\"") || s.starts_with("'''") {
267 let delim = &s[..3];
268 let body = s
269 .strip_prefix(delim)
270 .and_then(|t| t.strip_suffix(delim))
271 .unwrap_or_else(|| s.strip_prefix(delim).unwrap_or(s));
272 body.to_string()
273 } else if s.starts_with("/**") {
274 let body = s
275 .strip_prefix("/**")
276 .and_then(|t| t.strip_suffix("*/"))
277 .unwrap_or_else(|| s.strip_prefix("/**").unwrap_or(s));
278 body.lines()
279 .map(|l| {
280 let t = l.trim();
281 if let Some(rest) = t.strip_prefix("* ") {
282 rest
283 } else if let Some(rest) = t.strip_prefix('*') {
284 rest
285 } else {
286 t
287 }
288 })
289 .collect::<Vec<_>>()
290 .join("\n")
291 } else if s.starts_with('"') || s.starts_with('\'') {
292 let delim = &s[..1];
293 s.strip_prefix(delim)
294 .and_then(|t| t.strip_suffix(delim))
295 .unwrap_or(s)
296 .to_string()
297 } else if s.starts_with("///") {
298 s.lines()
299 .map(|l| {
300 l.trim()
301 .strip_prefix("///")
302 .unwrap_or(l.trim())
303 .trim_start()
304 .to_string()
305 })
306 .collect::<Vec<_>>()
307 .join("\n")
308 } else {
309 s.to_string()
310 };
311
312 let lines: Vec<&str> = inner.lines().collect();
313 let min_indent = lines
314 .iter()
315 .filter(|l| !l.trim().is_empty())
316 .map(|l| l.len() - l.trim_start().len())
317 .min()
318 .unwrap_or(0);
319
320 let dedented: String = lines
321 .iter()
322 .map(|l| {
323 if l.len() >= min_indent {
324 &l[min_indent..]
325 } else {
326 l.trim()
327 }
328 })
329 .collect::<Vec<_>>()
330 .join("\n")
331 .trim()
332 .to_string();
333
334 if dedented.is_empty() {
335 None
336 } else {
337 Some(dedented)
338 }
339}
340
341pub mod astro;
342pub mod bash;
343pub mod c;
344pub mod cmake;
345pub mod cpp;
346pub mod csharp;
347pub mod csproj;
348pub mod dart;
349pub mod dm;
350pub mod elixir;
351pub mod fortran;
352pub mod go;
353pub mod groovy;
354pub mod haskell;
355pub mod java;
356pub mod javascript;
357pub mod json_package;
358pub mod julia;
359pub mod kotlin;
360pub mod lua;
361pub mod markdown;
362pub mod mcp_config;
363pub mod objc;
364pub mod pascal;
365pub mod php;
366pub mod powershell;
367pub mod python;
368pub mod racket;
369pub mod razor;
370pub mod ruby;
371pub mod rust;
372pub mod scala;
373pub mod sln;
374pub mod sql;
375pub mod svelte;
376pub mod swift;
377pub mod typescript;
378pub mod verilog;
379pub mod vue;
380pub mod zig;
381
382pub use astro::AstroExtractor;
383pub use bash::TsBashExtractor;
384pub use c::TsCExtractor;
385pub use cmake::TsCmakeExtractor;
386pub use cpp::TsCppExtractor;
387pub use csharp::TsCSharpExtractor;
388pub use csproj::CsprojExtractor;
389pub use dart::TsDartExtractor;
390pub use dm::{DmExtractor, DmfExtractor, DmiExtractor, DmmExtractor};
391pub use elixir::TsElixirExtractor;
392pub use fortran::FortranExtractor;
393pub use go::TsGoExtractor;
394pub use groovy::TsGroovyExtractor;
395pub use haskell::TsHaskellExtractor;
396pub use java::TsJavaExtractor;
397pub use javascript::TsJavaScriptExtractor;
398pub use json_package::JsonPackageExtractor;
399pub use julia::TsJuliaExtractor;
400pub use kotlin::TsKotlinExtractor;
401pub use lua::TsLuaExtractor;
402pub use markdown::MarkdownExtractor;
403pub use mcp_config::McpConfigExtractor;
404pub use objc::ObjCExtractor;
405pub use pascal::TsPascalExtractor;
406pub use php::TsPhpExtractor;
407pub use powershell::PowerShellExtractor;
408pub use python::TsPythonExtractor;
409pub use racket::TsRacketExtractor;
410pub use razor::RazorExtractor;
411pub use ruby::TsRubyExtractor;
412pub use rust::TsRustExtractor;
413pub use scala::TsScalaExtractor;
414pub use sln::SlnExtractor;
415pub use sql::TsSqlExtractor;
416pub use svelte::TsSvelteExtractor;
417pub use swift::TsSwiftExtractor;
418pub use typescript::TsTypeScriptExtractor;
419pub use verilog::VerilogExtractor;
420pub use vue::TsVueExtractor;
421pub use zig::TsZigExtractor;
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use crate::extract::{make_id, normalize_id};
427
428 #[test]
431 fn test_run_query_matches_ranged_byte_ranges() {
432 let source = b"def foo(x):\n pass\n";
433 let lang = tree_sitter_python::LANGUAGE.into();
434 let query = r#"(function_definition) @func.node"#;
435 let matches = run_query_matches_ranged(source, &lang, query).unwrap();
436 assert_eq!(matches.len(), 1);
437 let (_, ranges) = &matches[0];
438 let (start, end) = ranges["func.node"];
439 assert_eq!(start, 0);
440 assert!(end > start);
441 assert!(source[start..end].starts_with(b"def"));
442 }
443
444 #[test]
445 fn test_run_query_matches_ranged_name_and_node() {
446 let source = b"def bar():\n return 1\n";
447 let lang = tree_sitter_python::LANGUAGE.into();
448 let query = r#"(function_definition name: (identifier) @fn.name) @fn.node"#;
449 let matches = run_query_matches_ranged(source, &lang, query).unwrap();
450 assert_eq!(matches.len(), 1);
451 let (texts, ranges) = &matches[0];
452 assert_eq!(texts["fn.name"], "bar");
453 let (node_start, node_end) = ranges["fn.node"];
454 let (name_start, name_end) = ranges["fn.name"];
455 assert!(node_start <= name_start);
456 assert!(name_end <= node_end);
457 }
458
459 #[test]
462 fn test_python_fn_source_location_set() {
463 let source = b"def foo(x):\n pass\n";
464 let path = Path::new("test.py");
465 let result = TsPythonExtractor::extract(source, path).unwrap();
466 let fn_node = result.nodes.iter().find(|n| n.label == "foo()").unwrap();
467 assert!(fn_node.source_location.is_some());
468 let loc = fn_node.source_location.as_ref().unwrap();
469 let parts: Vec<&str> = loc.split(':').collect();
470 assert_eq!(parts.len(), 2);
471 let start: usize = parts[0].parse().unwrap();
472 let end: usize = parts[1].parse().unwrap();
473 assert_eq!(start, 0);
474 assert!(end > start);
475 assert!(source[start..end].starts_with(b"def"));
476 }
477
478 #[test]
479 fn test_python_file_node_source_location_none() {
480 let source = b"def foo(x):\n pass\n";
481 let path = Path::new("mymod.py");
482 let result = TsPythonExtractor::extract(source, path).unwrap();
483 let file_node = result.nodes.iter().find(|n| n.label == "mymod.py").unwrap();
484 assert!(file_node.source_location.is_none());
485 }
486
487 #[test]
488 fn test_python_class_source_location_set() {
489 let source = b"class Foo:\n pass\n";
490 let path = Path::new("test.py");
491 let result = TsPythonExtractor::extract(source, path).unwrap();
492 let class_node = result.nodes.iter().find(|n| n.label == "Foo").unwrap();
493 assert!(class_node.source_location.is_some());
494 let loc = class_node.source_location.as_ref().unwrap();
495 let (start, end) = {
496 let parts: Vec<&str> = loc.split(':').collect();
497 let s: usize = parts[0].parse().unwrap();
498 let e: usize = parts[1].parse().unwrap();
499 (s, e)
500 };
501 assert_eq!(start, 0);
502 assert!(end > start);
503 }
504
505 #[test]
508 fn test_ts_extract_python_class() {
509 let source = b"class Foo(Bar):\n pass\n";
510 let path = Path::new("test.py");
511 let result = TsPythonExtractor::extract(source, path).unwrap();
512
513 let foo_node = result.nodes.iter().find(|n| n.label == "Foo");
514 assert!(foo_node.is_some(), "expected Foo node");
515 assert!(result.nodes.len() >= 2, "expected at least 2 nodes");
516
517 let contains_edge = result.edges.iter().find(|e| e.relation == "contains");
518 assert!(contains_edge.is_some(), "expected contains edge");
519
520 let inherits_edge = result.edges.iter().find(|e| e.relation == "inherits");
521 assert!(inherits_edge.is_some(), "expected inherits edge");
522 }
523
524 #[test]
525 fn test_ts_extract_python_function() {
526 let source = b"def foo(x: int) -> str:\n pass\n";
527 let path = Path::new("test.py");
528 let result = TsPythonExtractor::extract(source, path).unwrap();
529
530 let fn_node = result.nodes.iter().find(|n| n.label == "foo()");
531 assert!(fn_node.is_some(), "expected foo() node");
532 }
533
534 #[test]
535 fn test_ts_extract_python_import() {
536 let source = b"import os\n";
537 let path = Path::new("test.py");
538 let result = TsPythonExtractor::extract(source, path).unwrap();
539
540 let import_edge = result.edges.iter().find(|e| e.relation == "imports");
541 assert!(import_edge.is_some(), "expected imports edge");
542 }
543
544 #[test]
545 fn test_ts_extract_python_from_import() {
546 let source = b"from .helper import transform\n";
547 let path = Path::new("test.py");
548 let result = TsPythonExtractor::extract(source, path).unwrap();
549
550 let import_edge = result.edges.iter().find(|e| e.relation == "imports");
551 assert!(import_edge.is_some(), "expected imports edge");
552 }
553
554 #[test]
555 fn test_ts_extract_recursion_limit() {
556 let source = b"# deeply nested\n";
557 let path = Path::new("test.py");
558 let result = TsPythonExtractor::extract(source, path).unwrap();
559 assert!(!result.nodes.is_empty(), "should at least have file node");
560 }
561
562 #[test]
563 fn test_ts_extract_syntax_error() {
564 let source = b"def foo( bar : \n";
565 let path = Path::new("test.py");
566 let result = TsPythonExtractor::extract(source, path);
567 assert!(result.is_ok(), "should gracefully handle syntax errors");
568 }
569
570 #[test]
571 fn test_python_class_file_type() {
572 let source = b"class Authenticator:\n pass\n";
573 let path = Path::new("auth.py");
574 let result = TsPythonExtractor::extract(source, path).unwrap();
575 let node = result
576 .nodes
577 .iter()
578 .find(|n| n.label == "Authenticator")
579 .unwrap();
580 assert_eq!(node.file_type, "class");
581 }
582
583 #[test]
584 fn test_python_module_function_file_type() {
585 let source = b"def authenticate(req):\n pass\n";
586 let path = Path::new("auth.py");
587 let result = TsPythonExtractor::extract(source, path).unwrap();
588 let node = result
589 .nodes
590 .iter()
591 .find(|n| n.label == "authenticate()")
592 .unwrap();
593 assert_eq!(node.file_type, "function");
594 }
595
596 #[test]
597 fn test_python_method_file_type() {
598 let source = b"class Auth:\n def login(self):\n pass\n";
599 let path = Path::new("auth.py");
600 let result = TsPythonExtractor::extract(source, path).unwrap();
601 let node = result.nodes.iter().find(|n| n.label == "login()").unwrap();
602 assert_eq!(node.file_type, "method");
603 }
604
605 #[test]
606 fn test_ts_make_id() {
607 assert_eq!(make_id(&["foo", "bar"]), "foo_bar");
608 assert_eq!(make_id(&["Foo", "Bar"]), "foo_bar");
609 assert_eq!(make_id(&["foo-bar"]), "foo_bar");
610 }
611
612 #[test]
613 fn test_ts_normalize_id() {
614 assert_eq!(normalize_id("Foo-Bar"), "foo_bar");
615 assert_eq!(
616 normalize_id("Session ValidateToken"),
617 "session_validatetoken"
618 );
619 }
620
621 #[test]
624 fn test_ts_extract_js_class() {
625 let source = b"class Foo extends Bar {}";
626 let path = Path::new("class_def.js");
627 let result = TsJavaScriptExtractor::extract(source, path).unwrap();
628
629 let foo_node = result.nodes.iter().find(|n| n.label == "Foo");
630 assert!(foo_node.is_some(), "expected Foo node");
631
632 let inherits_edge = result.edges.iter().find(|e| e.relation == "inherits");
633 assert!(inherits_edge.is_some(), "expected inherits edge");
634
635 let bar_node = result.nodes.iter().find(|n| n.label == "Bar");
636 assert!(bar_node.is_some(), "expected Bar node");
637 }
638
639 #[test]
640 fn test_ts_extract_js_import() {
641 let source = b"import { x } from './mod'";
642 let path = Path::new("imports.js");
643 let result = TsJavaScriptExtractor::extract(source, path).unwrap();
644
645 let imports_from_edge = result.edges.iter().find(|e| e.relation == "imports_from");
646 assert!(imports_from_edge.is_some(), "expected imports_from edge");
647
648 let imports_edge = result
649 .edges
650 .iter()
651 .filter(|e| e.relation == "imports")
652 .count();
653 assert!(imports_edge > 0, "expected at least one imports edge");
654 }
655
656 #[test]
657 fn test_ts_extract_js_dynamic_import() {
658 let source = b"const mod = await import('./mod')";
659 let path = Path::new("imports.js");
660 let result = TsJavaScriptExtractor::extract(source, path).unwrap();
661
662 let imports_from_edge = result.edges.iter().find(|e| e.relation == "imports_from");
663 assert!(imports_from_edge.is_some(), "expected imports_from edge");
664 }
665
666 #[test]
667 fn test_ts_extract_js_require() {
668 let source = b"const m = require('./mod')";
669 let path = Path::new("imports.js");
670 let result = TsJavaScriptExtractor::extract(source, path).unwrap();
671
672 let imports_from_edge = result.edges.iter().find(|e| e.relation == "imports_from");
673 assert!(imports_from_edge.is_some(), "expected imports_from edge");
674 }
675
676 #[test]
679 fn test_ts_interface_file_type() {
680 let source = b"interface AuthProvider {\n login(): void\n}";
681 let path = Path::new("auth.ts");
682 let result = TsTypeScriptExtractor::extract(source, path).unwrap();
683 let node = result
684 .nodes
685 .iter()
686 .find(|n| n.label == "AuthProvider")
687 .unwrap();
688 assert_eq!(node.file_type, "interface");
689 }
690
691 #[test]
692 fn test_ts_type_alias_file_type() {
693 let source = b"type UserId = User;";
694 let path = Path::new("types.ts");
695 let result = TsTypeScriptExtractor::extract(source, path).unwrap();
696 let node = result.nodes.iter().find(|n| n.label == "UserId").unwrap();
697 assert_eq!(node.file_type, "class");
698 }
699
700 #[test]
701 fn test_ts_function_declaration_file_type() {
702 let source = b"function authenticate(req: Request): boolean { return true; }";
703 let path = Path::new("auth.ts");
704 let result = TsTypeScriptExtractor::extract(source, path).unwrap();
705 let node = result
706 .nodes
707 .iter()
708 .find(|n| n.label == "authenticate()")
709 .unwrap();
710 assert_eq!(node.file_type, "function");
711 }
712
713 #[test]
714 fn test_ts_method_definition_file_type() {
715 let source = b"class AuthService {\n login() {}\n}";
716 let path = Path::new("auth.ts");
717 let result = TsTypeScriptExtractor::extract(source, path).unwrap();
718 let node = result.nodes.iter().find(|n| n.label == "login()").unwrap();
719 assert_eq!(node.file_type, "method");
720 }
721
722 #[test]
723 fn test_ts_arrow_function_file_type() {
724 let source = b"const validate = (token: string) => token.length > 0;";
725 let path = Path::new("auth.ts");
726 let result = TsTypeScriptExtractor::extract(source, path).unwrap();
727 let node = result
728 .nodes
729 .iter()
730 .find(|n| n.label == "validate()")
731 .unwrap();
732 assert_eq!(node.file_type, "function");
733 }
734
735 #[test]
736 fn test_ts_extract_ts_interface() {
737 let source = b"interface Foo {\n bar(): void\n}";
738 let path = Path::new("interface.ts");
739 let result = TsTypeScriptExtractor::extract(source, path).unwrap();
740
741 let iface_node = result.nodes.iter().find(|n| n.label == "Foo");
742 assert!(iface_node.is_some(), "expected Foo interface node");
743 if let Some(n) = iface_node {
744 assert_eq!(
745 n.metadata.get("kind").map(|s| s.as_str()),
746 Some("interface")
747 );
748 }
749 }
750
751 #[test]
752 fn test_ts_extract_ts_type_alias() {
753 let source = b"type Foo = Bar<string>";
754 let path = Path::new("interface.ts");
755 let result = TsTypeScriptExtractor::extract(source, path).unwrap();
756
757 let alias_node = result.nodes.iter().find(|n| n.label == "Foo");
758 assert!(alias_node.is_some(), "expected Foo type alias node");
759
760 let type_ref_edge = result.edges.iter().find(|e| e.relation == "type_ref");
761 assert!(type_ref_edge.is_some(), "expected type_ref edge");
762 }
763
764 #[test]
767 fn test_go_struct_file_type() {
768 let source = b"type Conn struct { addr string }";
769 let path = Path::new("conn.go");
770 let result = TsGoExtractor::extract(source, path).unwrap();
771 let node = result.nodes.iter().find(|n| n.label == "Conn").unwrap();
772 assert_eq!(node.file_type, "struct");
773 }
774
775 #[test]
776 fn test_go_function_file_type() {
777 let source = b"func HandleRequest() {}";
778 let path = Path::new("handler.go");
779 let result = TsGoExtractor::extract(source, path).unwrap();
780 let node = result
781 .nodes
782 .iter()
783 .find(|n| n.label == "HandleRequest()")
784 .unwrap();
785 assert_eq!(node.file_type, "function");
786 }
787
788 #[test]
789 fn test_go_method_file_type() {
790 let source = b"type Engine struct{}\nfunc (e *Engine) ServeHTTP() {}";
791 let path = Path::new("engine.go");
792 let result = TsGoExtractor::extract(source, path).unwrap();
793 let node = result
794 .nodes
795 .iter()
796 .find(|n| n.label == "ServeHTTP()")
797 .unwrap();
798 assert_eq!(node.file_type, "method");
799 }
800
801 #[test]
802 fn test_ts_extract_go_struct() {
803 let source = b"type Foo struct {\n\tBar string\n}";
804 let path = Path::new("struct.go");
805 let result = TsGoExtractor::extract(source, path).unwrap();
806
807 let struct_node = result.nodes.iter().find(|n| n.label == "Foo");
808 assert!(struct_node.is_some(), "expected Foo struct node");
809 }
810
811 #[test]
812 fn test_go_struct_source_location() {
813 let source = b"type Foo struct {\n\tBar string\n}";
814 let path = Path::new("struct.go");
815 let result = TsGoExtractor::extract(source, path).unwrap();
816 let struct_node = result.nodes.iter().find(|n| n.label == "Foo").unwrap();
817 assert!(
818 struct_node.source_location.is_some(),
819 "expected source_location on struct node"
820 );
821 let loc = struct_node.source_location.as_ref().unwrap();
822 let parts: Vec<&str> = loc.split(':').collect();
823 assert_eq!(parts.len(), 2);
824 let start: usize = parts[0].parse().unwrap();
825 let end: usize = parts[1].parse().unwrap();
826 assert!(end > start);
827 }
828
829 #[test]
830 fn test_go_top_level_function_extracted() {
831 let source = b"func HandleRequest(w http.ResponseWriter, r *http.Request) {}";
832 let path = Path::new("handler.go");
833 let result = TsGoExtractor::extract(source, path).unwrap();
834 let fn_node = result.nodes.iter().find(|n| n.label == "HandleRequest()");
835 assert!(fn_node.is_some(), "expected HandleRequest() node");
836 assert!(
837 fn_node.unwrap().source_location.is_some(),
838 "expected source_location on function"
839 );
840 }
841
842 #[test]
843 fn test_go_pointer_receiver_method_extracted() {
844 let source = b"package gin\ntype Engine struct{}\nfunc (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {}";
845 let path = Path::new("gin.go");
846 let result = TsGoExtractor::extract(source, path).unwrap();
847 let method_node = result.nodes.iter().find(|n| n.label == "ServeHTTP()");
848 assert!(method_node.is_some(), "expected ServeHTTP() node");
849 let nodes: HashMap<String, String> = result
850 .nodes
851 .iter()
852 .map(|n| (n.id.clone(), n.label.clone()))
853 .collect();
854 let has_edge = result.edges.iter().any(|e| {
855 e.relation == "contains"
856 && nodes.get(&e.source).map(|s| s == "Engine").unwrap_or(false)
857 && nodes
858 .get(&e.target)
859 .map(|t| t == "ServeHTTP()")
860 .unwrap_or(false)
861 });
862 assert!(has_edge, "expected Engine -contains-> ServeHTTP() edge");
863 }
864
865 #[test]
866 fn test_go_value_receiver_method_extracted() {
867 let source =
868 b"package gin\ntype Router struct{}\nfunc (r Router) Use(middleware ...HandlerFunc) {}";
869 let path = Path::new("router.go");
870 let result = TsGoExtractor::extract(source, path).unwrap();
871 let method_node = result.nodes.iter().find(|n| n.label == "Use()");
872 assert!(method_node.is_some(), "expected Use() node");
873 let nodes: HashMap<String, String> = result
874 .nodes
875 .iter()
876 .map(|n| (n.id.clone(), n.label.clone()))
877 .collect();
878 let has_edge = result.edges.iter().any(|e| {
879 e.relation == "contains"
880 && nodes.get(&e.source).map(|s| s == "Router").unwrap_or(false)
881 && nodes.get(&e.target).map(|t| t == "Use()").unwrap_or(false)
882 });
883 assert!(has_edge, "expected Router -contains-> Use() edge");
884 }
885
886 #[test]
889 fn test_rust_struct_file_type() {
890 let source = b"struct Foo { bar: u32 }";
891 let path = Path::new("lib.rs");
892 let result = TsRustExtractor::extract(source, path).unwrap();
893 let node = result.nodes.iter().find(|n| n.label == "Foo").unwrap();
894 assert_eq!(node.file_type, "struct");
895 }
896
897 #[test]
898 fn test_rust_function_file_type() {
899 let source = b"fn process(x: u32) -> u32 { x }";
900 let path = Path::new("lib.rs");
901 let result = TsRustExtractor::extract(source, path).unwrap();
902 let node = result
903 .nodes
904 .iter()
905 .find(|n| n.label == "process()")
906 .unwrap();
907 assert_eq!(node.file_type, "function");
908 }
909
910 #[test]
911 fn test_ts_extract_rs_struct() {
912 let source = b"struct Foo<T> {\n bar: T,\n}";
913 let path = Path::new("generic.rs");
914 let result = TsRustExtractor::extract(source, path).unwrap();
915
916 let struct_node = result.nodes.iter().find(|n| n.label == "Foo");
917 assert!(struct_node.is_some(), "expected Foo struct node");
918
919 let generic_edge = result.edges.iter().find(|e| e.relation == "generic");
920 assert!(generic_edge.is_some(), "expected generic edge");
921 }
922
923 #[test]
924 fn test_ts_extract_rs_import() {
925 let source = b"use crate::mod::Foo;";
926 let path = Path::new("generic.rs");
927 let result = TsRustExtractor::extract(source, path).unwrap();
928
929 let imports = result
930 .edges
931 .iter()
932 .filter(|e| e.relation == "imports")
933 .count();
934 assert!(imports > 0, "expected at least one imports edge");
935 }
936
937 #[test]
938 fn test_ts_extract_rs_function() {
939 let source = b"fn foo(x: u32) -> u32 {\n x + 1\n}\n";
940 let path = Path::new("lib.rs");
941 let result = TsRustExtractor::extract(source, path).unwrap();
942
943 let fn_node = result.nodes.iter().find(|n| n.label == "foo()");
944 assert!(fn_node.is_some(), "expected foo() function node");
945 }
946
947 #[test]
948 fn test_ts_extract_rs_function_source_location() {
949 let source = b"fn bar() {\n let x = 1;\n}\n";
950 let path = Path::new("lib.rs");
951 let result = TsRustExtractor::extract(source, path).unwrap();
952
953 let fn_node = result.nodes.iter().find(|n| n.label == "bar()").unwrap();
954 assert!(
955 fn_node.source_location.is_some(),
956 "expected source_location on function node"
957 );
958 let loc = fn_node.source_location.as_ref().unwrap();
959 let parts: Vec<&str> = loc.split(':').collect();
960 assert_eq!(parts.len(), 2);
961 let start: usize = parts[0].parse().unwrap();
962 let end: usize = parts[1].parse().unwrap();
963 assert!(end > start);
964 assert!(source[start..end].starts_with(b"fn"));
965 }
966
967 #[test]
968 fn test_ts_extract_rs_impl_method_contains_edge() {
969 let source = b"struct Foo;\nimpl Foo {\n fn new() -> Self { Foo }\n}\n";
970 let path = Path::new("lib.rs");
971 let result = TsRustExtractor::extract(source, path).unwrap();
972
973 let fn_node = result.nodes.iter().find(|n| n.label == "new()");
974 assert!(fn_node.is_some(), "expected new() method node");
975
976 let nodes: HashMap<String, String> = result
977 .nodes
978 .iter()
979 .map(|n| (n.id.clone(), n.label.clone()))
980 .collect();
981 let impl_contains = result.edges.iter().any(|e| {
982 e.relation == "contains"
983 && nodes.get(&e.source).map(|s| s == "Foo").unwrap_or(false)
984 && nodes.get(&e.target).map(|t| t == "new()").unwrap_or(false)
985 });
986 assert!(impl_contains, "expected Foo -contains-> new() edge");
987 }
988
989 #[test]
992 fn test_ts_extract_java_method() {
993 let source = b"@Override\nvoid foo() {\n}\n";
994 let path = Path::new("method.java");
995 let result = TsJavaExtractor::extract(source, path).unwrap();
996
997 let method_node = result.nodes.iter().find(|n| n.label == "foo()");
998 assert!(method_node.is_some(), "expected foo() method node");
999
1000 let annot_node = result.nodes.iter().find(|n| n.label == "@Override");
1001 assert!(annot_node.is_some(), "expected @Override annotation node");
1002 }
1003
1004 #[test]
1005 fn test_ts_extract_java_import() {
1006 let source = b"import java.util.List;\n";
1007 let path = Path::new("method.java");
1008 let result = TsJavaExtractor::extract(source, path).unwrap();
1009
1010 let import_edge = result.edges.iter().find(|e| e.relation == "imports");
1011 assert!(import_edge.is_some(), "expected imports edge");
1012 }
1013
1014 #[test]
1015 fn test_java_class_file_type() {
1016 let source = b"class UserService {\n}\n";
1017 let path = Path::new("UserService.java");
1018 let result = TsJavaExtractor::extract(source, path).unwrap();
1019 let node = result
1020 .nodes
1021 .iter()
1022 .find(|n| n.label == "UserService")
1023 .unwrap();
1024 assert_eq!(node.file_type, "class");
1025 }
1026
1027 #[test]
1028 fn test_java_method_file_type() {
1029 let source = b"class Svc {\n void handle() {}\n}\n";
1030 let path = Path::new("Svc.java");
1031 let result = TsJavaExtractor::extract(source, path).unwrap();
1032 let node = result.nodes.iter().find(|n| n.label == "handle()").unwrap();
1033 assert_eq!(node.file_type, "method");
1034 }
1035
1036 #[test]
1037 fn test_java_interface_file_type() {
1038 let source = b"interface AuthProvider {\n boolean login();\n}\n";
1039 let path = Path::new("AuthProvider.java");
1040 let result = TsJavaExtractor::extract(source, path).unwrap();
1041 let node = result
1042 .nodes
1043 .iter()
1044 .find(|n| n.label == "AuthProvider")
1045 .unwrap();
1046 assert_eq!(node.file_type, "interface");
1047 }
1048
1049 #[test]
1052 fn test_ts_extract_c_function() {
1053 let source = b"int foo(int x) {\n return bar(x);\n}\n";
1054 let path = Path::new("func.c");
1055 let result = TsCExtractor::extract(source, path).unwrap();
1056
1057 let fn_node = result.nodes.iter().find(|n| n.label == "foo()");
1058 assert!(fn_node.is_some(), "expected foo() function node");
1059 }
1060
1061 #[test]
1062 fn test_ts_extract_c_include() {
1063 let source = b"#include \"helper.h\"\n";
1064 let path = Path::new("func.c");
1065 let result = TsCExtractor::extract(source, path).unwrap();
1066
1067 let include_edge = result.edges.iter().find(|e| e.relation == "imports");
1068 assert!(include_edge.is_some(), "expected imports edge for include");
1069 }
1070
1071 #[test]
1074 fn test_ts_extract_cpp_method() {
1075 let source = b"class Foo {\n void bar();\n};\n";
1076 let path = Path::new("class.cpp");
1077 let result = TsCppExtractor::extract(source, path).unwrap();
1078
1079 let class_node = result.nodes.iter().find(|n| n.label == "Foo");
1080 assert!(class_node.is_some(), "expected Foo class node");
1081
1082 let method_node = result.nodes.iter().find(|n| n.label == "bar()");
1083 assert!(method_node.is_some(), "expected bar() method node");
1084 }
1085
1086 #[test]
1089 fn test_ts_extract_csharp_class() {
1090 let source = b"class Foo : IBar, Baz { }\n";
1091 let path = Path::new("class.cs");
1092 let result = TsCSharpExtractor::extract(source, path).unwrap();
1093
1094 let class_node = result.nodes.iter().find(|n| n.label == "Foo");
1095 assert!(class_node.is_some(), "expected Foo class node");
1096
1097 let inherits_edge = result.edges.iter().find(|e| e.relation == "inherits");
1098 assert!(inherits_edge.is_some(), "expected inherits edge");
1099 }
1100
1101 #[test]
1102 fn test_ts_extract_csharp_namespace() {
1103 let source = b"namespace X {\n class Y { }\n}\n";
1104 let path = Path::new("class.cs");
1105 let result = TsCSharpExtractor::extract(source, path).unwrap();
1106
1107 let ns_node = result.nodes.iter().find(|n| n.label == "X namespace");
1108 assert!(ns_node.is_some(), "expected X namespace node");
1109
1110 let contains_ns = result.edges.iter().any(|e| e.relation == "contains");
1111 assert!(contains_ns, "expected contains edges");
1112 }
1113
1114 #[test]
1115 fn test_csharp_class_file_type() {
1116 let source = b"class UserService { }\n";
1117 let path = Path::new("UserService.cs");
1118 let result = TsCSharpExtractor::extract(source, path).unwrap();
1119 let node = result
1120 .nodes
1121 .iter()
1122 .find(|n| n.label == "UserService")
1123 .unwrap();
1124 assert_eq!(node.file_type, "class");
1125 }
1126
1127 #[test]
1128 fn test_csharp_method_file_type() {
1129 let source = b"class Svc {\n void Handle() {}\n}\n";
1130 let path = Path::new("Svc.cs");
1131 let result = TsCSharpExtractor::extract(source, path).unwrap();
1132 let node = result.nodes.iter().find(|n| n.label == "Handle()").unwrap();
1133 assert_eq!(node.file_type, "method");
1134 }
1135
1136 #[test]
1137 fn test_csharp_interface_file_type() {
1138 let source = b"interface IAuthProvider { }\n";
1139 let path = Path::new("IAuthProvider.cs");
1140 let result = TsCSharpExtractor::extract(source, path).unwrap();
1141 let node = result
1142 .nodes
1143 .iter()
1144 .find(|n| n.label == "IAuthProvider")
1145 .unwrap();
1146 assert_eq!(node.file_type, "interface");
1147 }
1148
1149 #[test]
1150 fn test_csharp_namespace_file_type() {
1151 let source = b"namespace MyApp { }\n";
1152 let path = Path::new("MyApp.cs");
1153 let result = TsCSharpExtractor::extract(source, path).unwrap();
1154 let node = result
1155 .nodes
1156 .iter()
1157 .find(|n| n.label == "MyApp namespace")
1158 .unwrap();
1159 assert_eq!(node.file_type, "namespace");
1160 }
1161
1162 #[test]
1165 fn test_ts_extract_kotlin_class() {
1166 let source = b"class Foo : Bar()\n";
1167 let path = Path::new("class.kt");
1168 let result = TsKotlinExtractor::extract(source, path).unwrap();
1169
1170 let class_node = result.nodes.iter().find(|n| n.label == "Foo");
1171 assert!(class_node.is_some(), "expected Foo class node");
1172
1173 let inherits_edge = result.edges.iter().find(|e| e.relation == "inherits");
1174 assert!(inherits_edge.is_some(), "expected inherits edge");
1175 }
1176
1177 #[test]
1180 fn test_ts_extract_swift_struct() {
1181 let source = b"struct Foo: Bar, Baz {}\n";
1182 let path = Path::new("struct.swift");
1183 let result = TsSwiftExtractor::extract(source, path).unwrap();
1184
1185 let struct_node = result.nodes.iter().find(|n| n.label == "Foo");
1186 assert!(struct_node.is_some(), "expected Foo struct node");
1187
1188 let inherits_edges = result
1189 .edges
1190 .iter()
1191 .filter(|e| e.relation == "inherits")
1192 .count();
1193 assert_eq!(
1194 inherits_edges, 2,
1195 "expected 2 inherits edges for Bar and Baz"
1196 );
1197 }
1198
1199 #[test]
1202 fn test_ts_extract_php_class() {
1203 let source = b"class Foo extends Bar {}\n";
1204 let path = Path::new("class.php");
1205 let result = TsPhpExtractor::extract(source, path).unwrap();
1206
1207 let class_node = result.nodes.iter().find(|n| n.label == "Foo");
1208 assert!(class_node.is_some(), "expected Foo class node");
1209
1210 let inherits_edge = result.edges.iter().find(|e| e.relation == "inherits");
1211 assert!(inherits_edge.is_some(), "expected inherits edge");
1212 }
1213
1214 #[test]
1217 fn test_ts_extract_ruby_class() {
1218 let source = b"class Foo < Bar\nend\n";
1219 let path = Path::new("class.rb");
1220 let result = TsRubyExtractor::extract(source, path).unwrap();
1221
1222 let class_node = result.nodes.iter().find(|n| n.label == "Foo");
1223 assert!(class_node.is_some(), "expected Foo class node");
1224
1225 let inherits_edge = result.edges.iter().find(|e| e.relation == "inherits");
1226 assert!(inherits_edge.is_some(), "expected inherits edge");
1227 }
1228
1229 #[test]
1230 fn test_ruby_class_source_location() {
1231 let source = b"class Foo < Bar\nend\n";
1232 let path = Path::new("class.rb");
1233 let result = TsRubyExtractor::extract(source, path).unwrap();
1234 let class_node = result.nodes.iter().find(|n| n.label == "Foo").unwrap();
1235 assert!(
1236 class_node.source_location.is_some(),
1237 "expected source_location on class node"
1238 );
1239 let loc = class_node.source_location.as_ref().unwrap();
1240 let parts: Vec<&str> = loc.split(':').collect();
1241 assert_eq!(parts.len(), 2);
1242 let start: usize = parts[0].parse().unwrap();
1243 let end: usize = parts[1].parse().unwrap();
1244 assert!(end > start);
1245 }
1246
1247 #[test]
1248 fn test_ruby_instance_method_extracted() {
1249 let source = b"class UsersController\n def create\n end\nend\n";
1250 let path = Path::new("users_controller.rb");
1251 let result = TsRubyExtractor::extract(source, path).unwrap();
1252 let method_node = result.nodes.iter().find(|n| n.label == "create()");
1253 assert!(method_node.is_some(), "expected create() node");
1254 assert!(
1255 method_node.unwrap().source_location.is_some(),
1256 "expected source_location on method"
1257 );
1258 }
1259
1260 #[test]
1261 fn test_ruby_class_method_extracted() {
1262 let source = b"class User\n def self.authenticate(token)\n end\nend\n";
1263 let path = Path::new("user.rb");
1264 let result = TsRubyExtractor::extract(source, path).unwrap();
1265 let method_node = result
1266 .nodes
1267 .iter()
1268 .find(|n| n.label == "self.authenticate()");
1269 assert!(method_node.is_some(), "expected self.authenticate() node");
1270 }
1271
1272 #[test]
1273 fn test_ruby_class_contains_method_edge() {
1274 let source = b"class ArticlesController\n def index\n end\nend\n";
1275 let path = Path::new("articles_controller.rb");
1276 let result = TsRubyExtractor::extract(source, path).unwrap();
1277 let nodes: HashMap<String, String> = result
1278 .nodes
1279 .iter()
1280 .map(|n| (n.id.clone(), n.label.clone()))
1281 .collect();
1282 let has_edge = result.edges.iter().any(|e| {
1283 e.relation == "contains"
1284 && nodes
1285 .get(&e.source)
1286 .map(|s| s == "ArticlesController")
1287 .unwrap_or(false)
1288 && nodes
1289 .get(&e.target)
1290 .map(|t| t == "index()")
1291 .unwrap_or(false)
1292 });
1293 assert!(
1294 has_edge,
1295 "expected ArticlesController -contains-> index() edge"
1296 );
1297 }
1298
1299 #[test]
1300 fn test_ruby_class_contains_singleton_method_edge() {
1301 let source = b"class User\n def self.find_by_token(t)\n end\nend\n";
1302 let path = Path::new("user.rb");
1303 let result = TsRubyExtractor::extract(source, path).unwrap();
1304 let nodes: HashMap<String, String> = result
1305 .nodes
1306 .iter()
1307 .map(|n| (n.id.clone(), n.label.clone()))
1308 .collect();
1309 let has_edge = result.edges.iter().any(|e| {
1310 e.relation == "contains"
1311 && nodes.get(&e.source).map(|s| s == "User").unwrap_or(false)
1312 && nodes
1313 .get(&e.target)
1314 .map(|t| t == "self.find_by_token()")
1315 .unwrap_or(false)
1316 });
1317 assert!(
1318 has_edge,
1319 "expected User -contains-> self.find_by_token() edge"
1320 );
1321 }
1322
1323 #[test]
1324 fn test_ruby_class_file_type() {
1325 let source = b"class AuthController\nend\n";
1326 let path = Path::new("auth_controller.rb");
1327 let result = TsRubyExtractor::extract(source, path).unwrap();
1328 let node = result
1329 .nodes
1330 .iter()
1331 .find(|n| n.label == "AuthController")
1332 .unwrap();
1333 assert_eq!(node.file_type, "class");
1334 }
1335
1336 #[test]
1337 fn test_ruby_instance_method_file_type() {
1338 let source = b"class User\n def authenticate\n end\nend\n";
1339 let path = Path::new("user.rb");
1340 let result = TsRubyExtractor::extract(source, path).unwrap();
1341 let node = result
1342 .nodes
1343 .iter()
1344 .find(|n| n.label == "authenticate()")
1345 .unwrap();
1346 assert_eq!(node.file_type, "method");
1347 }
1348
1349 #[test]
1350 fn test_ruby_singleton_method_file_type() {
1351 let source = b"class User\n def self.find_by_email(email)\n end\nend\n";
1352 let path = Path::new("user.rb");
1353 let result = TsRubyExtractor::extract(source, path).unwrap();
1354 let node = result
1355 .nodes
1356 .iter()
1357 .find(|n| n.label == "self.find_by_email()")
1358 .unwrap();
1359 assert_eq!(node.file_type, "method");
1360 }
1361
1362 #[test]
1365 fn test_ts_extract_sql_table() {
1366 let source = b"CREATE TABLE foo (id INT)\n";
1367 let path = Path::new("schema.sql");
1368 let result = TsSqlExtractor::extract(source, path).unwrap();
1369
1370 let table_node = result.nodes.iter().find(|n| n.label == "foo");
1371 assert!(table_node.is_some(), "expected foo table node");
1372 }
1373
1374 #[test]
1375 fn test_ts_extract_sql_foreign_key() {
1376 let source = b"ALTER TABLE foo ADD FOREIGN KEY (a) REFERENCES bar(b)\n";
1377 let path = Path::new("schema.sql");
1378 let result = TsSqlExtractor::extract(source, path).unwrap();
1379
1380 let ref_edge = result.edges.iter().find(|e| e.relation == "references");
1381 assert!(ref_edge.is_some(), "expected references edge");
1382 }
1383
1384 #[test]
1387 fn test_ts_extract_vue_sfc() {
1388 let source = b"<script>\nexport default {\n name: 'MyComponent'\n}\n</script>\n<template>\n <div>{{ message }}</div>\n</template>\n";
1389 let path = Path::new("component.vue");
1390 let result = TsVueExtractor::extract(source, path).unwrap();
1391
1392 let comp_node = result.nodes.iter().find(|n| n.label == "MyComponent");
1393 assert!(comp_node.is_some(), "expected MyComponent node");
1394 }
1395
1396 #[test]
1399 fn test_ts_extract_svelte_sfc() {
1400 let source = b"<script>\n let count = $state(0)\n</script>\n<h1>{count}</h1>\n";
1401 let path = Path::new("component.svelte");
1402 let result = TsSvelteExtractor::extract(source, path).unwrap();
1403
1404 let state_node = result.nodes.iter().find(|n| n.label == "count");
1405 assert!(state_node.is_some(), "expected count state node");
1406 }
1407
1408 #[test]
1411 fn test_ts_extract_bash_function() {
1412 let source = b"function foo() {\n bar\n}\n";
1413 let path = Path::new("func.sh");
1414 let result = TsBashExtractor::extract(source, path).unwrap();
1415
1416 let fn_node = result.nodes.iter().find(|n| n.label == "foo()");
1417 assert!(fn_node.is_some(), "expected foo() function node");
1418 }
1419
1420 #[test]
1421 fn test_ts_extract_bash_source() {
1422 let source = b"source ./lib.sh\n";
1423 let path = Path::new("func.sh");
1424 let result = TsBashExtractor::extract(source, path).unwrap();
1425
1426 let imports_from_edge = result.edges.iter().find(|e| e.relation == "imports_from");
1427 assert!(imports_from_edge.is_some(), "expected imports_from edge");
1428 }
1429
1430 #[test]
1433 fn test_ts_extract_json_package() {
1434 let source = b"{\n \"name\": \"test-package\",\n \"dependencies\": {\n \"express\": \"^4.18.0\",\n \"lodash\": \"^4.17.21\"\n }\n}\n";
1435 let path = Path::new("package.json");
1436 let result = JsonPackageExtractor::extract(source, path).unwrap();
1437
1438 let dep_nodes = result
1439 .nodes
1440 .iter()
1441 .filter(|n| n.label == "express" || n.label == "lodash")
1442 .count();
1443 assert_eq!(dep_nodes, 2, "expected express and lodash dep nodes");
1444 }
1445
1446 #[test]
1449 fn test_ts_extract_mcp_config() {
1450 let source = b"{\n \"servers\": [\n {\n \"name\": \"test-server\",\n \"command\": \"python\",\n \"env\": {\n \"API_KEY\": \"env:API_KEY\"\n }\n }\n ]\n}\n";
1451 let path = Path::new("mcp.json");
1452 let result = McpConfigExtractor::extract(source, path).unwrap();
1453
1454 let server_node = result.nodes.iter().find(|n| n.label == "test-server");
1455 assert!(server_node.is_some(), "expected test-server server node");
1456 }
1457
1458 #[test]
1461 fn test_ts_extract_cmake_function() {
1462 let source = b"function(my_func)\n message(hello)\nendfunction()\n";
1463 let path = Path::new("CMakeLists.txt");
1464 let result = TsCmakeExtractor::extract(source, path).unwrap();
1465 let fn_node = result.nodes.iter().find(|n| n.label == "my_func()");
1466 assert!(fn_node.is_some(), "expected my_func() function node");
1467 }
1468
1469 #[test]
1472 fn test_ts_extract_dart_class() {
1473 let source = b"class MyClass { }\n";
1474 let path = Path::new("main.dart");
1475 let result = TsDartExtractor::extract(source, path).unwrap();
1476 let class_node = result.nodes.iter().find(|n| n.label == "MyClass");
1477 assert!(class_node.is_some(), "expected MyClass class node");
1478 }
1479
1480 #[test]
1481 fn test_ts_extract_dart_method() {
1482 let source = b"class MyClass {\n void myMethod() {}\n}\n";
1483 let path = Path::new("main.dart");
1484 let result = TsDartExtractor::extract(source, path).unwrap();
1485 let method_node = result.nodes.iter().find(|n| n.label == "myMethod()");
1486 assert!(method_node.is_some(), "expected myMethod() method node");
1487 }
1488
1489 #[test]
1492 fn test_ts_extract_groovy_class() {
1493 let source = b"class MyGroovyClass {\n def myMethod() { }\n}\n";
1494 let path = Path::new("MyGroovyClass.groovy");
1495 let result = TsGroovyExtractor::extract(source, path).unwrap();
1496 let class_node = result.nodes.iter().find(|n| n.label == "MyGroovyClass");
1497 assert!(class_node.is_some(), "expected MyGroovyClass class node");
1498 }
1499
1500 #[test]
1501 fn test_ts_extract_groovy_method() {
1502 let source = b"class MyGroovyClass {\n def myMethod() { }\n}\n";
1503 let path = Path::new("MyGroovyClass.groovy");
1504 let result = TsGroovyExtractor::extract(source, path).unwrap();
1505 let method_node = result.nodes.iter().find(|n| n.label == "myMethod()");
1506 assert!(method_node.is_some(), "expected myMethod() method node");
1507 }
1508
1509 #[test]
1512 fn test_ts_extract_haskell_module() {
1513 let source = b"module MyModule where\n\nmyFunc = 42\n";
1514 let path = Path::new("MyModule.hs");
1515 let result = TsHaskellExtractor::extract(source, path).unwrap();
1516 let module_node = result.nodes.iter().find(|n| n.label == "MyModule");
1517 assert!(module_node.is_some(), "expected MyModule module node");
1518 }
1519
1520 #[test]
1521 fn test_ts_extract_haskell_function() {
1522 let source = b"myFunc = 42\n";
1523 let path = Path::new("MyModule.hs");
1524 let result = TsHaskellExtractor::extract(source, path).unwrap();
1525 let func_node = result.nodes.iter().find(|n| n.label == "myFunc()");
1526 assert!(func_node.is_some(), "expected myFunc() function node");
1527 }
1528
1529 #[test]
1532 fn test_ts_extract_lua_function() {
1533 let source = b"function my_func()\nend\n";
1534 let path = Path::new("test.lua");
1535 let result = TsLuaExtractor::extract(source, path).unwrap();
1536 let fn_node = result.nodes.iter().find(|n| n.label == "my_func()");
1537 assert!(fn_node.is_some(), "expected my_func() function node");
1538 }
1539
1540 #[test]
1541 fn test_ts_extract_lua_require() {
1542 let source = b"require(\"my_module\")\n";
1543 let path = Path::new("test.lua");
1544 let result = TsLuaExtractor::extract(source, path).unwrap();
1545 let import_edge = result.edges.iter().find(|e| e.relation == "imports");
1546 assert!(import_edge.is_some(), "expected imports edge for require");
1547 }
1548
1549 #[test]
1552 fn test_ts_extract_racket_define() {
1553 let source = b"(define my-func 42)\n";
1554 let path = Path::new("test.rkt");
1555 let result = TsRacketExtractor::extract(source, path).unwrap();
1556 let fn_node = result.nodes.iter().find(|n| n.label == "my-func()");
1557 assert!(fn_node.is_some(), "expected my-func() function node");
1558 }
1559
1560 #[test]
1561 fn test_ts_extract_racket_require() {
1562 let source = b"(require racket/base)\n";
1563 let path = Path::new("test.rkt");
1564 let result = TsRacketExtractor::extract(source, path).unwrap();
1565 let import_edge = result.edges.iter().find(|e| e.relation == "imports");
1566 assert!(import_edge.is_some(), "expected imports edge for require");
1567 }
1568
1569 #[test]
1572 fn test_ts_extract_scala_class() {
1573 let source = b"class MyScalaClass {\n def myMethod(): Unit = {}\n}\n";
1574 let path = Path::new("MyScalaClass.scala");
1575 let result = TsScalaExtractor::extract(source, path).unwrap();
1576 let class_node = result.nodes.iter().find(|n| n.label == "MyScalaClass");
1577 assert!(class_node.is_some(), "expected MyScalaClass class node");
1578 }
1579
1580 #[test]
1581 fn test_ts_extract_scala_method() {
1582 let source = b"class MyScalaClass {\n def myMethod(): Unit = {}\n}\n";
1583 let path = Path::new("MyScalaClass.scala");
1584 let result = TsScalaExtractor::extract(source, path).unwrap();
1585 let method_node = result.nodes.iter().find(|n| n.label == "myMethod()");
1586 assert!(method_node.is_some(), "expected myMethod() method node");
1587 }
1588
1589 #[test]
1592 fn test_ts_extract_zig_struct() {
1593 let source = b"const MyStruct = struct {};\nfn myFunc() void {}\n";
1594 let path = Path::new("main.zig");
1595 let result = TsZigExtractor::extract(source, path).unwrap();
1596 let struct_node = result.nodes.iter().find(|n| n.label == "MyStruct");
1597 assert!(struct_node.is_some(), "expected MyStruct struct node");
1598 }
1599
1600 #[test]
1601 fn test_ts_extract_zig_function() {
1602 let source = b"const MyStruct = struct {};\nfn myFunc() void {}\n";
1603 let path = Path::new("main.zig");
1604 let result = TsZigExtractor::extract(source, path).unwrap();
1605 let func_node = result.nodes.iter().find(|n| n.label == "myFunc()");
1606 assert!(func_node.is_some(), "expected myFunc() function node");
1607 }
1608
1609 #[test]
1612 fn test_ts_extract_julia_function() {
1613 let source = b"module MyMod\nfunction compute(x)\n x * 2\nend\nend\n";
1614 let path = Path::new("MyMod.jl");
1615 let result = TsJuliaExtractor::extract(source, path).unwrap();
1616 let func_node = result.nodes.iter().find(|n| n.label == "compute()");
1617 assert!(func_node.is_some(), "expected compute() function node");
1618 }
1619
1620 #[test]
1621 fn test_ts_extract_julia_struct() {
1622 let source = b"struct Point\n x::Float64\n y::Float64\nend\n";
1623 let path = Path::new("point.jl");
1624 let result = TsJuliaExtractor::extract(source, path).unwrap();
1625 let struct_node = result.nodes.iter().find(|n| n.label == "Point");
1626 assert!(struct_node.is_some(), "expected Point struct node");
1627 }
1628
1629 #[test]
1632 fn test_ts_extract_elixir_module() {
1633 let source = b"defmodule MyApp.Repo do\n use Ecto.Repo\nend\n";
1634 let path = Path::new("repo.ex");
1635 let result = TsElixirExtractor::extract(source, path).unwrap();
1636 let mod_node = result.nodes.iter().find(|n| n.label == "MyApp.Repo");
1637 assert!(mod_node.is_some(), "expected MyApp.Repo module node");
1638 }
1639
1640 #[test]
1641 fn test_ts_extract_elixir_function() {
1642 let source = b"defmodule Calc do\n def add(a, b) do\n a + b\n end\nend\n";
1643 let path = Path::new("calc.ex");
1644 let result = TsElixirExtractor::extract(source, path).unwrap();
1645 let func_node = result.nodes.iter().find(|n| n.label == "add()");
1646 assert!(func_node.is_some(), "expected add() function node");
1647 }
1648
1649 #[test]
1652 fn test_ts_extract_pascal_procedure() {
1653 let source = b"unit MyUnit;\ninterface\nprocedure DoWork;\nimplementation\nprocedure DoWork;\nbegin\nend;\nend.\n";
1654 let path = Path::new("myunit.pas");
1655 let result = TsPascalExtractor::extract(source, path).unwrap();
1656 let proc_node = result.nodes.iter().find(|n| n.label == "DoWork");
1657 assert!(proc_node.is_some(), "expected DoWork procedure node");
1658 }
1659
1660 #[test]
1661 fn test_ts_extract_pascal_uses() {
1662 let source = b"unit MyUnit;\ninterface\nuses SysUtils, Classes;\nimplementation\nend.\n";
1663 let path = Path::new("myunit.pas");
1664 let result = TsPascalExtractor::extract(source, path).unwrap();
1665 let sysutils_node = result.nodes.iter().find(|n| n.label == "SysUtils");
1666 assert!(sysutils_node.is_some(), "expected SysUtils import node");
1667 let import_edge = result.edges.iter().find(|e| e.relation == "imports");
1668 assert!(import_edge.is_some(), "expected imports edge");
1669 }
1670
1671 #[test]
1674 fn test_markdown_headings() {
1675 let source = b"# Title\n## Section One\n### Sub\n## Section Two\n";
1676 let path = Path::new("readme.md");
1677 let result = MarkdownExtractor::extract(source, path).unwrap();
1678 assert!(
1679 result.nodes.iter().any(|n| n.label == "Title"),
1680 "h1 missing"
1681 );
1682 assert!(
1683 result.nodes.iter().any(|n| n.label == "Section One"),
1684 "h2 missing"
1685 );
1686 assert!(result.nodes.iter().any(|n| n.label == "Sub"), "h3 missing");
1687 let contains = result
1688 .edges
1689 .iter()
1690 .filter(|e| e.relation == "contains")
1691 .count();
1692 assert!(
1693 contains >= 3,
1694 "expected >=3 contains edges, got {}",
1695 contains
1696 );
1697 }
1698
1699 #[test]
1700 fn test_markdown_code_blocks() {
1701 let source = b"# Guide\n\n```rust\nfn main() {}\n```\n\n```python\nprint('hi')\n```\n";
1702 let path = Path::new("guide.md");
1703 let result = MarkdownExtractor::extract(source, path).unwrap();
1704 let code_nodes: Vec<_> = result
1705 .nodes
1706 .iter()
1707 .filter(|n| n.label.starts_with("code:"))
1708 .collect();
1709 assert_eq!(code_nodes.len(), 2, "expected 2 code block nodes");
1710 }
1711
1712 #[test]
1713 fn test_markdown_heading_nesting() {
1714 let source = b"# Top\n## Child\n# Top\n";
1715 let path = Path::new("nested.md");
1716 let result = MarkdownExtractor::extract(source, path).unwrap();
1717 let top_nodes: Vec<_> = result.nodes.iter().filter(|n| n.label == "Top").collect();
1719 assert_eq!(
1720 top_nodes.len(),
1721 2,
1722 "expected 2 Top heading nodes (deduped IDs)"
1723 );
1724 assert!(
1725 result.nodes.iter().any(|n| n.label == "Child"),
1726 "Child missing"
1727 );
1728 }
1729
1730 #[test]
1731 fn test_markdown_empty() {
1732 let source = b"just plain text, no headings or code blocks\n";
1733 let path = Path::new("plain.md");
1734 let result = MarkdownExtractor::extract(source, path).unwrap();
1735 assert_eq!(result.nodes.len(), 1);
1737 assert!(result.edges.is_empty());
1738 }
1739
1740 #[test]
1743 fn test_astro_frontmatter_imports() {
1744 let source = b"---\nimport Button from './Button.astro';\nimport { foo } from '../lib/utils';\n---\n<h1>Hello</h1>\n";
1745 let path = Path::new("page.astro");
1746 let result = AstroExtractor::extract(source, path).unwrap();
1747 let imp_edges: Vec<_> = result
1748 .edges
1749 .iter()
1750 .filter(|e| e.relation == "imports_from")
1751 .collect();
1752 assert!(
1753 imp_edges.len() >= 2,
1754 "expected >=2 import edges, got {}",
1755 imp_edges.len()
1756 );
1757 }
1758
1759 #[test]
1760 fn test_astro_script_block() {
1761 let source = b"<html>\n<script>\nimport { x } from './x.ts';\n</script>\n</html>\n";
1762 let path = Path::new("layout.astro");
1763 let result = AstroExtractor::extract(source, path).unwrap();
1764 let imp_edges: Vec<_> = result
1765 .edges
1766 .iter()
1767 .filter(|e| e.relation == "imports_from")
1768 .collect();
1769 assert!(
1770 !imp_edges.is_empty(),
1771 "expected import edge from script block"
1772 );
1773 }
1774
1775 #[test]
1778 fn test_powershell_function() {
1779 let source = b"function Get-Items {\n param($path)\n Get-ChildItem $path\n}\n";
1780 let path = Path::new("script.ps1");
1781 let result = PowerShellExtractor::extract(source, path).unwrap();
1782 let func_node = result.nodes.iter().find(|n| n.label.contains("Get-Items"));
1783 assert!(func_node.is_some(), "expected Get-Items function node");
1784 let contains = result.edges.iter().find(|e| e.relation == "contains");
1785 assert!(contains.is_some(), "expected contains edge");
1786 }
1787
1788 #[test]
1789 fn test_powershell_class() {
1790 let source =
1791 b"class Animal {\n [string]$Name\n [void] Speak() { Write-Host $this.Name }\n}\n";
1792 let path = Path::new("classes.ps1");
1793 let result = PowerShellExtractor::extract(source, path).unwrap();
1794 assert!(!result.nodes.is_empty(), "expected at least file node");
1795 let has_class = result
1797 .nodes
1798 .iter()
1799 .any(|n| n.label == "Animal" || n.file_type == "class");
1800 let has_file = result.nodes.iter().any(|n| n.file_type == "code");
1801 assert!(has_class || has_file, "expected class or file node");
1802 }
1803
1804 #[test]
1807 fn test_verilog_module() {
1808 let source =
1809 b"module adder(input a, input b, output sum);\n assign sum = a + b;\nendmodule\n";
1810 let path = Path::new("adder.v");
1811 let result = VerilogExtractor::extract(source, path).unwrap();
1812 assert!(!result.nodes.is_empty(), "expected at least file node");
1813 }
1814
1815 #[test]
1816 fn test_verilog_empty() {
1817 let source = b"// empty verilog\n";
1818 let path = Path::new("empty.v");
1819 let result = VerilogExtractor::extract(source, path).unwrap();
1820 assert_eq!(result.nodes.len(), 1, "only file node expected");
1821 }
1822
1823 #[test]
1826 fn test_fortran_subroutine() {
1827 let source =
1828 b"subroutine hello()\n implicit none\n print *, 'Hello'\nend subroutine hello\n";
1829 let path = Path::new("hello.f90");
1830 let result = FortranExtractor::extract(source, path).unwrap();
1831 assert!(!result.nodes.is_empty(), "expected at least file node");
1832 }
1833
1834 #[test]
1835 fn test_fortran_module() {
1836 let source = b"module math_utils\n implicit none\ncontains\n function square(x) result(r)\n real, intent(in) :: x\n real :: r\n r = x*x\n end function square\nend module math_utils\n";
1837 let path = Path::new("math.f90");
1838 let result = FortranExtractor::extract(source, path).unwrap();
1839 assert!(!result.nodes.is_empty(), "expected at least file node");
1840 }
1841
1842 #[test]
1845 fn test_objc_interface() {
1846 let source =
1847 b"#import <Foundation/Foundation.h>\n@interface Dog : NSObject\n- (void)bark;\n@end\n";
1848 let path = Path::new("Dog.m");
1849 let result = ObjCExtractor::extract(source, path).unwrap();
1850 assert!(!result.nodes.is_empty(), "expected at least file node");
1851 }
1852
1853 #[test]
1854 fn test_objc_empty() {
1855 let source = b"// no classes\n";
1856 let path = Path::new("empty.m");
1857 let result = ObjCExtractor::extract(source, path).unwrap();
1858 assert_eq!(result.nodes.len(), 1, "only file node expected");
1859 }
1860
1861 fn sample_path(name: &str) -> std::path::PathBuf {
1864 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1865 .join("../tests/fixtures/sample")
1866 .join(name)
1867 }
1868
1869 fn load_sample(name: &str) -> Vec<u8> {
1870 std::fs::read(sample_path(name)).unwrap_or_else(|_| panic!("fixture not found: {}", name))
1871 }
1872
1873 #[test]
1876 fn test_java_import_edges_have_import_context() {
1877 let src = load_sample("sample.java");
1878 let result = TsJavaExtractor::extract(&src, &sample_path("sample.java")).unwrap();
1879 let import_edges: Vec<_> = result
1880 .edges
1881 .iter()
1882 .filter(|e| e.relation == "imports")
1883 .collect();
1884 assert!(!import_edges.is_empty(), "expected import edges");
1885 assert!(
1886 import_edges
1887 .iter()
1888 .all(|e| e.context.as_deref() == Some("import")),
1889 "all import edges must have context='import'"
1890 );
1891 }
1892
1893 #[test]
1894 fn test_java_normalizes_inherits_and_implements() {
1895 let src = load_sample("sample.java");
1896 let path = sample_path("sample.java");
1897 let result = TsJavaExtractor::extract(&src, &path).unwrap();
1898 let nodes: std::collections::HashMap<String, String> = result
1899 .nodes
1900 .iter()
1901 .map(|n| (n.id.clone(), n.label.clone()))
1902 .collect();
1903 let inherits: Vec<_> = result
1904 .edges
1905 .iter()
1906 .filter(|e| e.relation == "inherits")
1907 .map(|e| {
1908 (
1909 nodes.get(&e.source).cloned().unwrap_or_default(),
1910 nodes.get(&e.target).cloned().unwrap_or_default(),
1911 )
1912 })
1913 .collect();
1914 let implements: Vec<_> = result
1915 .edges
1916 .iter()
1917 .filter(|e| e.relation == "implements")
1918 .map(|e| {
1919 (
1920 nodes.get(&e.source).cloned().unwrap_or_default(),
1921 nodes.get(&e.target).cloned().unwrap_or_default(),
1922 )
1923 })
1924 .collect();
1925 assert!(
1926 inherits
1927 .iter()
1928 .any(|(s, t)| s.contains("DataProcessor") && t.contains("BaseProcessor")),
1929 "expected inherits(DataProcessor, BaseProcessor), got {:?}",
1930 inherits
1931 );
1932 assert!(
1933 implements
1934 .iter()
1935 .any(|(s, t)| s.contains("DataProcessor") && t.contains("Processor")),
1936 "expected implements(DataProcessor, Processor), got {:?}",
1937 implements
1938 );
1939 }
1940
1941 #[test]
1942 fn test_java_parameter_return_generic_and_attribute_contexts() {
1943 let src = load_sample("sample.java");
1944 let path = sample_path("sample.java");
1945 let result = TsJavaExtractor::extract(&src, &path).unwrap();
1946 let nodes: std::collections::HashMap<String, String> = result
1947 .nodes
1948 .iter()
1949 .map(|n| (n.id.clone(), n.label.clone()))
1950 .collect();
1951 let refs: Vec<_> = result
1952 .edges
1953 .iter()
1954 .filter(|e| e.relation == "references")
1955 .collect();
1956 let has_param = refs.iter().any(|e| {
1957 nodes
1958 .get(&e.source)
1959 .map(|s| s.contains("build"))
1960 .unwrap_or(false)
1961 && nodes
1962 .get(&e.target)
1963 .map(|t| t == "HttpClient")
1964 .unwrap_or(false)
1965 && e.context.as_deref() == Some("parameter_type")
1966 });
1967 assert!(
1968 has_param,
1969 "expected build→HttpClient with parameter_type context"
1970 );
1971 let has_ret = refs.iter().any(|e| {
1972 nodes
1973 .get(&e.source)
1974 .map(|s| s.contains("build"))
1975 .unwrap_or(false)
1976 && nodes.get(&e.target).map(|t| t == "Result").unwrap_or(false)
1977 && e.context.as_deref() == Some("return_type")
1978 });
1979 assert!(has_ret, "expected build→Result with return_type context");
1980 let has_generic = refs.iter().any(|e| {
1981 nodes
1982 .get(&e.source)
1983 .map(|s| s.contains("build"))
1984 .unwrap_or(false)
1985 && nodes
1986 .get(&e.target)
1987 .map(|t| t == "DataProcessor")
1988 .unwrap_or(false)
1989 && e.context.as_deref() == Some("generic_arg")
1990 });
1991 assert!(
1992 has_generic,
1993 "expected build→DataProcessor with generic_arg context"
1994 );
1995 let has_attrib = refs.iter().any(|e| {
1996 nodes
1997 .get(&e.source)
1998 .map(|s| s.contains("build"))
1999 .unwrap_or(false)
2000 && nodes
2001 .get(&e.target)
2002 .map(|t| t == "Override")
2003 .unwrap_or(false)
2004 && e.context.as_deref() == Some("attribute")
2005 });
2006 assert!(has_attrib, "expected build→Override with attribute context");
2007 }
2008
2009 #[test]
2012 fn test_c_import_edges_have_import_context() {
2013 let src = load_sample("sample.c");
2014 let path = sample_path("sample.c");
2015 let result = TsCExtractor::extract(&src, &path).unwrap();
2016 let import_edges: Vec<_> = result
2017 .edges
2018 .iter()
2019 .filter(|e| e.relation == "imports")
2020 .collect();
2021 assert!(!import_edges.is_empty(), "expected import edges");
2022 assert!(
2023 import_edges
2024 .iter()
2025 .all(|e| e.context.as_deref() == Some("import")),
2026 "all import edges must have context='import'"
2027 );
2028 }
2029
2030 #[test]
2031 fn test_c_call_edges_have_call_context() {
2032 let src = load_sample("sample.c");
2033 let path = sample_path("sample.c");
2034 let result = TsCExtractor::extract(&src, &path).unwrap();
2035 let call_edges: Vec<_> = result
2036 .edges
2037 .iter()
2038 .filter(|e| e.relation == "calls")
2039 .collect();
2040 assert!(!call_edges.is_empty(), "expected call edges");
2041 assert!(
2042 call_edges
2043 .iter()
2044 .all(|e| e.context.as_deref() == Some("call")),
2045 "all call edges must have context='call'"
2046 );
2047 }
2048
2049 #[test]
2052 fn test_cpp_import_edges_have_import_context() {
2053 let src = load_sample("sample.cpp");
2054 let path = sample_path("sample.cpp");
2055 let result = TsCppExtractor::extract(&src, &path).unwrap();
2056 let import_edges: Vec<_> = result
2057 .edges
2058 .iter()
2059 .filter(|e| e.relation == "imports")
2060 .collect();
2061 assert!(!import_edges.is_empty(), "expected import edges");
2062 assert!(
2063 import_edges
2064 .iter()
2065 .all(|e| e.context.as_deref() == Some("import")),
2066 "all import edges must have context='import'"
2067 );
2068 }
2069
2070 #[test]
2071 fn test_cpp_class_inherits_edge() {
2072 let src = load_sample("sample.cpp");
2073 let path = sample_path("sample.cpp");
2074 let result = TsCppExtractor::extract(&src, &path).unwrap();
2075 let nodes: std::collections::HashMap<String, String> = result
2076 .nodes
2077 .iter()
2078 .map(|n| (n.id.clone(), n.label.clone()))
2079 .collect();
2080 let found = result.edges.iter().any(|e| {
2081 e.relation == "inherits"
2082 && nodes
2083 .get(&e.source)
2084 .map(|s| s.contains("AuthedHttpClient"))
2085 .unwrap_or(false)
2086 && nodes
2087 .get(&e.target)
2088 .map(|t| t.contains("HttpClient"))
2089 .unwrap_or(false)
2090 });
2091 assert!(
2092 found,
2093 "AuthedHttpClient should have inherits edge to HttpClient"
2094 );
2095 }
2096
2097 #[test]
2098 fn test_cpp_struct_inherits_edge() {
2099 let src = load_sample("sample.cpp");
2100 let path = sample_path("sample.cpp");
2101 let result = TsCppExtractor::extract(&src, &path).unwrap();
2102 let nodes: std::collections::HashMap<String, String> = result
2103 .nodes
2104 .iter()
2105 .map(|n| (n.id.clone(), n.label.clone()))
2106 .collect();
2107 let found = result.edges.iter().any(|e| {
2108 e.relation == "inherits"
2109 && nodes
2110 .get(&e.source)
2111 .map(|s| s.contains("RetryingHttpClient"))
2112 .unwrap_or(false)
2113 && nodes
2114 .get(&e.target)
2115 .map(|t| t.contains("HttpClient"))
2116 .unwrap_or(false)
2117 });
2118 assert!(
2119 found,
2120 "RetryingHttpClient should have inherits edge to HttpClient"
2121 );
2122 }
2123
2124 #[test]
2127 fn test_csharp_import_edges_have_import_context() {
2128 let src = load_sample("sample.cs");
2129 let path = sample_path("sample.cs");
2130 let result = TsCSharpExtractor::extract(&src, &path).unwrap();
2131 let import_edges: Vec<_> = result
2132 .edges
2133 .iter()
2134 .filter(|e| e.relation == "imports")
2135 .collect();
2136 assert!(!import_edges.is_empty(), "expected import edges");
2137 assert!(
2138 import_edges
2139 .iter()
2140 .all(|e| e.context.as_deref() == Some("import")),
2141 "all import edges must have context='import'"
2142 );
2143 }
2144
2145 #[test]
2146 fn test_csharp_inherits_edge() {
2147 let src = load_sample("sample.cs");
2148 let path = sample_path("sample.cs");
2149 let result = TsCSharpExtractor::extract(&src, &path).unwrap();
2150 let inherits: Vec<_> = result
2151 .edges
2152 .iter()
2153 .filter(|e| e.relation == "inherits")
2154 .collect();
2155 assert!(!inherits.is_empty(), "expected at least one inherits edge");
2156 }
2157
2158 #[test]
2159 fn test_csharp_implements_iprocessor() {
2160 let src = load_sample("sample.cs");
2161 let path = sample_path("sample.cs");
2162 let result = TsCSharpExtractor::extract(&src, &path).unwrap();
2163 let nodes: std::collections::HashMap<String, String> = result
2164 .nodes
2165 .iter()
2166 .map(|n| (n.id.clone(), n.label.clone()))
2167 .collect();
2168 let found = result.edges.iter().any(|e| {
2169 e.relation == "implements"
2170 && nodes
2171 .get(&e.source)
2172 .map(|s| s.contains("DataProcessor"))
2173 .unwrap_or(false)
2174 && nodes
2175 .get(&e.target)
2176 .map(|t| t.contains("IProcessor"))
2177 .unwrap_or(false)
2178 });
2179 assert!(
2180 found,
2181 "DataProcessor should have implements edge to IProcessor"
2182 );
2183 }
2184
2185 #[test]
2186 fn test_csharp_splits_inherits_and_implements_edges() {
2187 let src = load_sample("sample.cs");
2188 let path = sample_path("sample.cs");
2189 let result = TsCSharpExtractor::extract(&src, &path).unwrap();
2190 let nodes: std::collections::HashMap<String, String> = result
2191 .nodes
2192 .iter()
2193 .map(|n| (n.id.clone(), n.label.clone()))
2194 .collect();
2195 let has_inherits = result.edges.iter().any(|e| {
2196 e.relation == "inherits"
2197 && nodes
2198 .get(&e.source)
2199 .map(|s| s.contains("DataProcessor"))
2200 .unwrap_or(false)
2201 && nodes
2202 .get(&e.target)
2203 .map(|t| t.contains("Processor"))
2204 .unwrap_or(false)
2205 });
2206 let has_implements = result.edges.iter().any(|e| {
2207 e.relation == "implements"
2208 && nodes
2209 .get(&e.source)
2210 .map(|s| s.contains("DataProcessor"))
2211 .unwrap_or(false)
2212 && nodes
2213 .get(&e.target)
2214 .map(|t| t.contains("IProcessor"))
2215 .unwrap_or(false)
2216 });
2217 assert!(has_inherits, "expected inherits(DataProcessor, Processor)");
2218 assert!(
2219 has_implements,
2220 "expected implements(DataProcessor, IProcessor)"
2221 );
2222 }
2223
2224 #[test]
2225 fn test_csharp_call_edges_have_call_context() {
2226 let src = load_sample("sample.cs");
2227 let path = sample_path("sample.cs");
2228 let result = TsCSharpExtractor::extract(&src, &path).unwrap();
2229 let call_edges: Vec<_> = result
2230 .edges
2231 .iter()
2232 .filter(|e| e.relation == "calls")
2233 .collect();
2234 assert!(!call_edges.is_empty(), "expected call edges");
2235 assert!(
2236 call_edges
2237 .iter()
2238 .all(|e| e.context.as_deref() == Some("call")),
2239 "all call edges must have context='call'"
2240 );
2241 }
2242
2243 #[test]
2244 fn test_csharp_parameter_return_and_generic_contexts() {
2245 let src = load_sample("sample.cs");
2246 let path = sample_path("sample.cs");
2247 let result = TsCSharpExtractor::extract(&src, &path).unwrap();
2248 let nodes: std::collections::HashMap<String, String> = result
2249 .nodes
2250 .iter()
2251 .map(|n| (n.id.clone(), n.label.clone()))
2252 .collect();
2253 let refs: Vec<_> = result
2254 .edges
2255 .iter()
2256 .filter(|e| e.relation == "references")
2257 .collect();
2258 let has_param = refs.iter().any(|e| {
2259 nodes
2260 .get(&e.source)
2261 .map(|s| s.contains("Build"))
2262 .unwrap_or(false)
2263 && nodes
2264 .get(&e.target)
2265 .map(|t| t == "HttpClient")
2266 .unwrap_or(false)
2267 && e.context.as_deref() == Some("parameter_type")
2268 });
2269 assert!(
2270 has_param,
2271 "expected Build→HttpClient with parameter_type context"
2272 );
2273 let has_ret = refs.iter().any(|e| {
2274 nodes
2275 .get(&e.source)
2276 .map(|s| s.contains("Build"))
2277 .unwrap_or(false)
2278 && nodes.get(&e.target).map(|t| t == "Result").unwrap_or(false)
2279 && e.context.as_deref() == Some("return_type")
2280 });
2281 assert!(has_ret, "expected Build→Result with return_type context");
2282 let has_generic = refs.iter().any(|e| {
2283 nodes
2284 .get(&e.source)
2285 .map(|s| s.contains("Build"))
2286 .unwrap_or(false)
2287 && nodes
2288 .get(&e.target)
2289 .map(|t| t == "DataProcessor")
2290 .unwrap_or(false)
2291 && e.context.as_deref() == Some("generic_arg")
2292 });
2293 assert!(
2294 has_generic,
2295 "expected Build→DataProcessor with generic_arg context"
2296 );
2297 }
2298
2299 #[test]
2302 fn test_kotlin_splits_inherits_and_implements() {
2303 let src = load_sample("sample.kt");
2304 let path = sample_path("sample.kt");
2305 let result = TsKotlinExtractor::extract(&src, &path).unwrap();
2306 let nodes: std::collections::HashMap<String, String> = result
2307 .nodes
2308 .iter()
2309 .map(|n| (n.id.clone(), n.label.clone()))
2310 .collect();
2311 let has_inherits = result.edges.iter().any(|e| {
2312 e.relation == "inherits"
2313 && nodes
2314 .get(&e.source)
2315 .map(|s| s.contains("DataProcessor"))
2316 .unwrap_or(false)
2317 && nodes
2318 .get(&e.target)
2319 .map(|t| t.contains("BaseProcessor"))
2320 .unwrap_or(false)
2321 });
2322 let has_implements = result.edges.iter().any(|e| {
2323 e.relation == "implements"
2324 && nodes
2325 .get(&e.source)
2326 .map(|s| s.contains("DataProcessor"))
2327 .unwrap_or(false)
2328 && nodes
2329 .get(&e.target)
2330 .map(|t| t.contains("Loggable"))
2331 .unwrap_or(false)
2332 });
2333 assert!(
2334 has_inherits,
2335 "expected inherits(DataProcessor, BaseProcessor)"
2336 );
2337 assert!(
2338 has_implements,
2339 "expected implements(DataProcessor, Loggable)"
2340 );
2341 }
2342
2343 #[test]
2344 fn test_kotlin_parameter_return_generic_and_field_contexts() {
2345 let src = load_sample("sample.kt");
2346 let path = sample_path("sample.kt");
2347 let result = TsKotlinExtractor::extract(&src, &path).unwrap();
2348 let nodes: std::collections::HashMap<String, String> = result
2349 .nodes
2350 .iter()
2351 .map(|n| (n.id.clone(), n.label.clone()))
2352 .collect();
2353 let refs: Vec<_> = result
2354 .edges
2355 .iter()
2356 .filter(|e| e.relation == "references")
2357 .collect();
2358 let has_param = refs.iter().any(|e| {
2359 nodes
2360 .get(&e.source)
2361 .map(|s| s.contains("run"))
2362 .unwrap_or(false)
2363 && nodes
2364 .get(&e.target)
2365 .map(|t| t == "DataProcessor")
2366 .unwrap_or(false)
2367 && e.context.as_deref() == Some("parameter_type")
2368 });
2369 assert!(
2370 has_param,
2371 "expected run→DataProcessor with parameter_type context"
2372 );
2373 let has_field = refs.iter().any(|e| {
2374 nodes
2375 .get(&e.source)
2376 .map(|s| s.contains("DataProcessor"))
2377 .unwrap_or(false)
2378 && nodes.get(&e.target).map(|t| t == "Result").unwrap_or(false)
2379 && e.context.as_deref() == Some("field")
2380 });
2381 assert!(
2382 has_field,
2383 "expected DataProcessor→Result with field context, refs: {:?}",
2384 refs.iter()
2385 .map(|e| (nodes.get(&e.source), nodes.get(&e.target), &e.context))
2386 .collect::<Vec<_>>()
2387 );
2388 }
2389
2390 #[test]
2393 fn test_scala_import_edges_have_import_context() {
2394 let src = load_sample("sample.scala");
2395 let path = sample_path("sample.scala");
2396 let result = TsScalaExtractor::extract(&src, &path).unwrap();
2397 let import_edges: Vec<_> = result
2398 .edges
2399 .iter()
2400 .filter(|e| e.relation == "imports")
2401 .collect();
2402 assert!(!import_edges.is_empty(), "expected import edges");
2403 assert!(
2404 import_edges
2405 .iter()
2406 .all(|e| e.context.as_deref() == Some("import")),
2407 "all import edges must have context='import'"
2408 );
2409 }
2410
2411 #[test]
2412 fn test_scala_call_edges_have_call_context() {
2413 let src = load_sample("sample.scala");
2414 let path = sample_path("sample.scala");
2415 let result = TsScalaExtractor::extract(&src, &path).unwrap();
2416 let call_edges: Vec<_> = result
2417 .edges
2418 .iter()
2419 .filter(|e| e.relation == "calls")
2420 .collect();
2421 if !call_edges.is_empty() {
2423 assert!(
2424 call_edges
2425 .iter()
2426 .all(|e| e.context.as_deref() == Some("call")),
2427 "all call edges must have context='call'"
2428 );
2429 }
2430 }
2431
2432 #[test]
2435 fn test_php_import_edges_have_import_context() {
2436 let src = load_sample("sample.php");
2437 let path = sample_path("sample.php");
2438 let result = TsPhpExtractor::extract(&src, &path).unwrap();
2439 let import_edges: Vec<_> = result
2440 .edges
2441 .iter()
2442 .filter(|e| e.relation == "imports")
2443 .collect();
2444 assert!(!import_edges.is_empty(), "expected import edges");
2445 assert!(
2446 import_edges
2447 .iter()
2448 .all(|e| e.context.as_deref() == Some("import")),
2449 "all import edges must have context='import'"
2450 );
2451 }
2452
2453 #[test]
2454 fn test_php_call_edges_have_call_context() {
2455 let src = load_sample("sample.php");
2456 let path = sample_path("sample.php");
2457 let result = TsPhpExtractor::extract(&src, &path).unwrap();
2458 let call_edges: Vec<_> = result
2459 .edges
2460 .iter()
2461 .filter(|e| e.relation == "calls")
2462 .collect();
2463 if !call_edges.is_empty() {
2464 assert!(
2465 call_edges
2466 .iter()
2467 .all(|e| e.context.as_deref() == Some("call")),
2468 "all call edges must have context='call'"
2469 );
2470 }
2471 }
2472
2473 #[test]
2474 fn test_php_splits_inherits_implements_mixes_in() {
2475 let src = load_sample("sample.php");
2476 let path = sample_path("sample.php");
2477 let result = TsPhpExtractor::extract(&src, &path).unwrap();
2478 let nodes: std::collections::HashMap<String, String> = result
2479 .nodes
2480 .iter()
2481 .map(|n| (n.id.clone(), n.label.clone()))
2482 .collect();
2483 let has_inherits = result.edges.iter().any(|e| {
2484 e.relation == "inherits"
2485 && nodes
2486 .get(&e.source)
2487 .map(|s| s.contains("DataProcessor"))
2488 .unwrap_or(false)
2489 && nodes
2490 .get(&e.target)
2491 .map(|t| t.contains("BaseProcessor"))
2492 .unwrap_or(false)
2493 });
2494 let has_implements = result.edges.iter().any(|e| {
2495 e.relation == "implements"
2496 && nodes
2497 .get(&e.source)
2498 .map(|s| s.contains("DataProcessor"))
2499 .unwrap_or(false)
2500 && nodes
2501 .get(&e.target)
2502 .map(|t| t.contains("Loggable"))
2503 .unwrap_or(false)
2504 });
2505 let has_mixes_in = result.edges.iter().any(|e| {
2506 e.relation == "mixes_in"
2507 && nodes
2508 .get(&e.source)
2509 .map(|s| s.contains("DataProcessor"))
2510 .unwrap_or(false)
2511 && nodes
2512 .get(&e.target)
2513 .map(|t| t.contains("HasName"))
2514 .unwrap_or(false)
2515 });
2516 assert!(
2517 has_inherits,
2518 "expected inherits(DataProcessor, BaseProcessor)"
2519 );
2520 assert!(
2521 has_implements,
2522 "expected implements(DataProcessor, Loggable)"
2523 );
2524 assert!(has_mixes_in, "expected mixes_in(DataProcessor, HasName)");
2525 }
2526
2527 #[test]
2528 fn test_php_property_parameter_and_return_contexts() {
2529 let src = load_sample("sample.php");
2530 let path = sample_path("sample.php");
2531 let result = TsPhpExtractor::extract(&src, &path).unwrap();
2532 let nodes: std::collections::HashMap<String, String> = result
2533 .nodes
2534 .iter()
2535 .map(|n| (n.id.clone(), n.label.clone()))
2536 .collect();
2537 let refs: Vec<_> = result
2538 .edges
2539 .iter()
2540 .filter(|e| e.relation == "references")
2541 .collect();
2542 let has_field = refs.iter().any(|e| {
2543 nodes
2544 .get(&e.source)
2545 .map(|s| s.contains("DataProcessor"))
2546 .unwrap_or(false)
2547 && nodes.get(&e.target).map(|t| t == "Result").unwrap_or(false)
2548 && e.context.as_deref() == Some("field")
2549 });
2550 assert!(
2551 has_field,
2552 "expected DataProcessor→Result with field context"
2553 );
2554 }
2555
2556 #[test]
2559 fn test_swift_import_edges_have_import_context() {
2560 let src = load_sample("sample.swift");
2561 let path = sample_path("sample.swift");
2562 let result = TsSwiftExtractor::extract(&src, &path).unwrap();
2563 let import_edges: Vec<_> = result
2564 .edges
2565 .iter()
2566 .filter(|e| e.relation == "imports")
2567 .collect();
2568 assert!(!import_edges.is_empty(), "expected import edges");
2569 assert!(
2570 import_edges
2571 .iter()
2572 .all(|e| e.context.as_deref() == Some("import")),
2573 "all import edges must have context='import'"
2574 );
2575 }
2576
2577 #[test]
2578 fn test_swift_splits_inherits_and_implements() {
2579 let src = load_sample("sample.swift");
2580 let path = sample_path("sample.swift");
2581 let result = TsSwiftExtractor::extract(&src, &path).unwrap();
2582 let nodes: std::collections::HashMap<String, String> = result
2583 .nodes
2584 .iter()
2585 .map(|n| (n.id.clone(), n.label.clone()))
2586 .collect();
2587 let has_inherits = result.edges.iter().any(|e| {
2588 e.relation == "inherits"
2589 && nodes
2590 .get(&e.source)
2591 .map(|s| s.contains("DataProcessor"))
2592 .unwrap_or(false)
2593 && nodes
2594 .get(&e.target)
2595 .map(|t| t.contains("BaseProcessor"))
2596 .unwrap_or(false)
2597 });
2598 let has_implements = result.edges.iter().any(|e| {
2599 e.relation == "implements"
2600 && nodes
2601 .get(&e.source)
2602 .map(|s| s.contains("DataProcessor"))
2603 .unwrap_or(false)
2604 && nodes
2605 .get(&e.target)
2606 .map(|t| t.contains("Processor"))
2607 .unwrap_or(false)
2608 });
2609 assert!(
2610 has_inherits,
2611 "expected inherits(DataProcessor, BaseProcessor)"
2612 );
2613 assert!(
2614 has_implements,
2615 "expected implements(DataProcessor, Processor)"
2616 );
2617 }
2618
2619 #[test]
2620 fn test_swift_protocol_conformance_emits_implements() {
2621 let src = load_sample("sample.swift");
2622 let path = sample_path("sample.swift");
2623 let result = TsSwiftExtractor::extract(&src, &path).unwrap();
2624 let nodes: std::collections::HashMap<String, String> = result
2625 .nodes
2626 .iter()
2627 .map(|n| (n.id.clone(), n.label.clone()))
2628 .collect();
2629 let found = result.edges.iter().any(|e| {
2630 e.relation == "implements"
2631 && nodes
2632 .get(&e.source)
2633 .map(|s| s.contains("DataProcessor"))
2634 .unwrap_or(false)
2635 && nodes
2636 .get(&e.target)
2637 .map(|t| t.contains("Processor"))
2638 .unwrap_or(false)
2639 });
2640 assert!(
2641 found,
2642 "DataProcessor should have implements edge to Processor (protocol)"
2643 );
2644 }
2645
2646 #[test]
2647 fn test_swift_extension_conformance_emits_implements() {
2648 let src = load_sample("sample.swift");
2649 let path = sample_path("sample.swift");
2650 let result = TsSwiftExtractor::extract(&src, &path).unwrap();
2651 let nodes: std::collections::HashMap<String, String> = result
2652 .nodes
2653 .iter()
2654 .map(|n| (n.id.clone(), n.label.clone()))
2655 .collect();
2656 let found = result.edges.iter().any(|e| {
2657 e.relation == "implements"
2658 && nodes
2659 .get(&e.source)
2660 .map(|s| s.contains("DataProcessor"))
2661 .unwrap_or(false)
2662 && nodes
2663 .get(&e.target)
2664 .map(|t| t.contains("Loggable"))
2665 .unwrap_or(false)
2666 });
2667 assert!(
2668 found,
2669 "extension DataProcessor: Loggable should emit implements edge"
2670 );
2671 }
2672
2673 #[test]
2674 fn test_swift_call_edges_have_call_context() {
2675 let src = load_sample("sample.swift");
2676 let path = sample_path("sample.swift");
2677 let result = TsSwiftExtractor::extract(&src, &path).unwrap();
2678 let call_edges: Vec<_> = result
2679 .edges
2680 .iter()
2681 .filter(|e| e.relation == "calls")
2682 .collect();
2683 if !call_edges.is_empty() {
2684 assert!(
2685 call_edges
2686 .iter()
2687 .all(|e| e.context.as_deref() == Some("call")),
2688 "all call edges must have context='call'"
2689 );
2690 }
2691 }
2692
2693 #[test]
2696 fn test_elixir_import_edges_have_import_context() {
2697 let src = load_sample("sample.ex");
2698 let path = sample_path("sample.ex");
2699 let result = TsElixirExtractor::extract(&src, &path).unwrap();
2700 let import_edges: Vec<_> = result
2701 .edges
2702 .iter()
2703 .filter(|e| e.relation == "imports")
2704 .collect();
2705 assert!(
2706 !import_edges.is_empty(),
2707 "expected import edges (alias/import/require/use)"
2708 );
2709 assert!(
2710 import_edges
2711 .iter()
2712 .all(|e| e.context.as_deref() == Some("import")),
2713 "all import edges must have context='import'"
2714 );
2715 }
2716
2717 #[test]
2718 fn test_elixir_call_edges_have_call_context() {
2719 let src = load_sample("sample.ex");
2720 let path = sample_path("sample.ex");
2721 let result = TsElixirExtractor::extract(&src, &path).unwrap();
2722 let call_edges: Vec<_> = result
2723 .edges
2724 .iter()
2725 .filter(|e| e.relation == "calls")
2726 .collect();
2727 if !call_edges.is_empty() {
2728 assert!(
2729 call_edges
2730 .iter()
2731 .all(|e| e.context.as_deref() == Some("call")),
2732 "all call edges must have context='call'"
2733 );
2734 }
2735 }
2736
2737 #[test]
2740 fn test_objc_import_edges_have_import_context() {
2741 let src = load_sample("sample.m");
2742 let path = sample_path("sample.m");
2743 let result = ObjCExtractor::extract(&src, &path).unwrap();
2744 let import_edges: Vec<_> = result
2745 .edges
2746 .iter()
2747 .filter(|e| e.relation == "imports")
2748 .collect();
2749 assert!(!import_edges.is_empty(), "expected import edges");
2750 assert!(
2751 import_edges
2752 .iter()
2753 .all(|e| e.context.as_deref() == Some("import")),
2754 "all import edges must have context='import'"
2755 );
2756 }
2757
2758 #[test]
2761 fn test_julia_import_edges_have_import_context() {
2762 let src = load_sample("sample.jl");
2763 let path = sample_path("sample.jl");
2764 let result = TsJuliaExtractor::extract(&src, &path).unwrap();
2765 let import_edges: Vec<_> = result
2766 .edges
2767 .iter()
2768 .filter(|e| e.relation == "imports")
2769 .collect();
2770 assert!(!import_edges.is_empty(), "expected import edges");
2771 assert!(
2772 import_edges
2773 .iter()
2774 .all(|e| e.context.as_deref() == Some("import")),
2775 "all import edges must have context='import'"
2776 );
2777 }
2778
2779 #[test]
2780 fn test_julia_call_edges_have_call_context() {
2781 let src = load_sample("sample.jl");
2782 let path = sample_path("sample.jl");
2783 let result = TsJuliaExtractor::extract(&src, &path).unwrap();
2784 let call_edges: Vec<_> = result
2785 .edges
2786 .iter()
2787 .filter(|e| e.relation == "calls")
2788 .collect();
2789 if !call_edges.is_empty() {
2790 assert!(
2791 call_edges
2792 .iter()
2793 .all(|e| e.context.as_deref() == Some("call")),
2794 "all call edges must have context='call'"
2795 );
2796 }
2797 }
2798
2799 #[test]
2802 fn test_fortran_use_edges_have_use_context() {
2803 let src = load_sample("sample.f90");
2804 let path = sample_path("sample.f90");
2805 let result = FortranExtractor::extract(&src, &path).unwrap();
2806 let import_edges: Vec<_> = result
2807 .edges
2808 .iter()
2809 .filter(|e| e.relation == "imports")
2810 .collect();
2811 assert!(!import_edges.is_empty(), "expected use edges");
2812 assert!(
2813 import_edges
2814 .iter()
2815 .all(|e| e.context.as_deref() == Some("use")),
2816 "all use edges must have context='use'"
2817 );
2818 }
2819
2820 #[test]
2823 fn test_groovy_import_edges_have_import_context() {
2824 let src = load_sample("sample.groovy");
2825 let path = sample_path("sample.groovy");
2826 let result = TsGroovyExtractor::extract(&src, &path).unwrap();
2827 let import_edges: Vec<_> = result
2828 .edges
2829 .iter()
2830 .filter(|e| e.relation == "imports")
2831 .collect();
2832 assert!(!import_edges.is_empty(), "expected import edges");
2833 assert!(
2834 import_edges
2835 .iter()
2836 .all(|e| e.context.as_deref() == Some("import")),
2837 "all import edges must have context='import'"
2838 );
2839 }
2840
2841 #[test]
2844 fn test_sln_no_error() {
2845 let src = load_sample("sample.sln");
2846 let path = sample_path("sample.sln");
2847 let result = SlnExtractor::extract(&src, &path).unwrap();
2848 assert!(!result.nodes.is_empty(), "expected nodes");
2849 }
2850
2851 #[test]
2852 fn test_sln_finds_projects() {
2853 let src = load_sample("sample.sln");
2854 let path = sample_path("sample.sln");
2855 let result = SlnExtractor::extract(&src, &path).unwrap();
2856 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
2857 assert!(
2858 labels.iter().any(|l| l.contains("WebApi")),
2859 "expected WebApi project"
2860 );
2861 assert!(
2862 labels.iter().any(|l| l.contains("Domain")),
2863 "expected Domain project"
2864 );
2865 }
2866
2867 #[test]
2868 fn test_sln_contains_edges() {
2869 let src = load_sample("sample.sln");
2870 let path = sample_path("sample.sln");
2871 let result = SlnExtractor::extract(&src, &path).unwrap();
2872 assert!(
2873 result.edges.iter().any(|e| e.relation == "contains"),
2874 "expected contains edges"
2875 );
2876 }
2877
2878 #[test]
2879 fn test_sln_project_dependency_edges() {
2880 let src = load_sample("sample.sln");
2881 let path = sample_path("sample.sln");
2882 let result = SlnExtractor::extract(&src, &path).unwrap();
2883 assert!(
2884 result.edges.iter().any(|e| e.relation == "imports"),
2885 "expected dependency imports edges"
2886 );
2887 }
2888
2889 #[test]
2892 fn test_csproj_no_error() {
2893 let src = load_sample("sample.csproj");
2894 let path = sample_path("sample.csproj");
2895 let result = CsprojExtractor::extract(&src, &path).unwrap();
2896 assert!(!result.nodes.is_empty(), "expected nodes");
2897 }
2898
2899 #[test]
2900 fn test_csproj_finds_packages() {
2901 let src = load_sample("sample.csproj");
2902 let path = sample_path("sample.csproj");
2903 let result = CsprojExtractor::extract(&src, &path).unwrap();
2904 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
2905 assert!(
2906 labels.iter().any(|l| l.contains("MediatR")),
2907 "expected MediatR package"
2908 );
2909 assert!(
2910 labels.iter().any(|l| l.contains("FluentValidation")),
2911 "expected FluentValidation package"
2912 );
2913 }
2914
2915 #[test]
2916 fn test_csproj_finds_project_references() {
2917 let src = load_sample("sample.csproj");
2918 let path = sample_path("sample.csproj");
2919 let result = CsprojExtractor::extract(&src, &path).unwrap();
2920 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
2921 assert!(
2922 labels.iter().any(|l| l.contains("Domain.csproj")),
2923 "expected Domain.csproj reference"
2924 );
2925 }
2926
2927 #[test]
2928 fn test_csproj_finds_target_framework() {
2929 let src = load_sample("sample.csproj");
2930 let path = sample_path("sample.csproj");
2931 let result = CsprojExtractor::extract(&src, &path).unwrap();
2932 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
2933 assert!(
2934 labels.iter().any(|l| l.contains("net8.0")),
2935 "expected net8.0 target framework"
2936 );
2937 }
2938
2939 #[test]
2940 fn test_csproj_finds_sdk() {
2941 let src = load_sample("sample.csproj");
2942 let path = sample_path("sample.csproj");
2943 let result = CsprojExtractor::extract(&src, &path).unwrap();
2944 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
2945 assert!(
2946 labels.iter().any(|l| l.contains("Microsoft.NET.Sdk.Web")),
2947 "expected SDK node"
2948 );
2949 }
2950
2951 #[test]
2954 fn test_razor_no_error() {
2955 let src = load_sample("sample.razor");
2956 let path = sample_path("sample.razor");
2957 let result = RazorExtractor::extract(&src, &path).unwrap();
2958 assert!(!result.nodes.is_empty(), "expected nodes");
2959 }
2960
2961 #[test]
2962 fn test_razor_finds_using_directives() {
2963 let src = load_sample("sample.razor");
2964 let path = sample_path("sample.razor");
2965 let result = RazorExtractor::extract(&src, &path).unwrap();
2966 assert!(
2967 result.edges.iter().any(|e| e.relation == "imports"),
2968 "expected import edges"
2969 );
2970 }
2971
2972 #[test]
2973 fn test_razor_finds_component_references() {
2974 let src = load_sample("sample.razor");
2975 let path = sample_path("sample.razor");
2976 let result = RazorExtractor::extract(&src, &path).unwrap();
2977 assert!(
2978 result.edges.iter().any(|e| e.relation == "calls"),
2979 "expected component call edges"
2980 );
2981 }
2982
2983 #[test]
2984 fn test_razor_finds_inherits() {
2985 let src = load_sample("sample.razor");
2986 let path = sample_path("sample.razor");
2987 let result = RazorExtractor::extract(&src, &path).unwrap();
2988 assert!(
2989 result.edges.iter().any(|e| e.relation == "inherits"),
2990 "expected inherits edge"
2991 );
2992 }
2993
2994 #[test]
2995 fn test_razor_finds_code_block_methods() {
2996 let src = load_sample("sample.razor");
2997 let path = sample_path("sample.razor");
2998 let result = RazorExtractor::extract(&src, &path).unwrap();
2999 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
3000 assert!(
3001 labels.iter().any(|l| l.contains("IncrementCount")),
3002 "expected IncrementCount method"
3003 );
3004 assert!(
3005 labels.iter().any(|l| l.contains("LoadData")),
3006 "expected LoadData method"
3007 );
3008 }
3009
3010 #[test]
3011 fn test_razor_no_dangling_edges() {
3012 let src = load_sample("sample.razor");
3013 let path = sample_path("sample.razor");
3014 let result = RazorExtractor::extract(&src, &path).unwrap();
3015 let node_ids: std::collections::HashSet<_> =
3016 result.nodes.iter().map(|n| n.id.as_str()).collect();
3017 for e in &result.edges {
3018 assert!(
3019 node_ids.contains(e.source.as_str()),
3020 "dangling source: {}",
3021 e.source
3022 );
3023 }
3024 }
3025
3026 #[test]
3029 fn test_dm_no_error() {
3030 let src = load_sample("sample.dm");
3031 let path = sample_path("sample.dm");
3032 let result = DmExtractor::extract(&src, &path).unwrap();
3033 assert!(!result.nodes.is_empty(), "expected nodes");
3034 }
3035
3036 #[test]
3037 fn test_dm_finds_global_proc() {
3038 let src = load_sample("sample.dm");
3039 let path = sample_path("sample.dm");
3040 let result = DmExtractor::extract(&src, &path).unwrap();
3041 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
3042 assert!(labels.contains(&"log_event()"), "expected log_event() node");
3043 assert!(labels.contains(&"RunTest()"), "expected RunTest() node");
3044 }
3045
3046 #[test]
3047 fn test_dm_finds_type_definition() {
3048 let src = load_sample("sample.dm");
3049 let path = sample_path("sample.dm");
3050 let result = DmExtractor::extract(&src, &path).unwrap();
3051 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
3052 assert!(
3053 labels.contains(&"/datum/weapon"),
3054 "expected /datum/weapon node"
3055 );
3056 assert!(
3057 labels.contains(&"/datum/weapon/sword"),
3058 "expected /datum/weapon/sword node"
3059 );
3060 }
3061
3062 #[test]
3063 fn test_dm_qualifies_proc_with_type_path() {
3064 let src = load_sample("sample.dm");
3065 let path = sample_path("sample.dm");
3066 let result = DmExtractor::extract(&src, &path).unwrap();
3067 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
3068 assert!(
3069 labels.contains(&"/datum/weapon/attack()"),
3070 "expected /datum/weapon/attack()"
3071 );
3072 assert!(
3073 labels.contains(&"/datum/weapon/sword/attack()"),
3074 "expected /datum/weapon/sword/attack()"
3075 );
3076 }
3077
3078 #[test]
3079 fn test_dm_finds_path_form_proc_definition() {
3080 let src = load_sample("sample.dm");
3081 let path = sample_path("sample.dm");
3082 let result = DmExtractor::extract(&src, &path).unwrap();
3083 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
3084 assert!(
3085 labels.contains(&"/datum/weapon/sword/sharpen()"),
3086 "expected /datum/weapon/sword/sharpen()"
3087 );
3088 }
3089
3090 #[test]
3091 fn test_dm_emits_include_edge() {
3092 let src = load_sample("sample.dm");
3093 let path = sample_path("sample.dm");
3094 let result = DmExtractor::extract(&src, &path).unwrap();
3095 let import_edges: Vec<_> = result
3096 .edges
3097 .iter()
3098 .filter(|e| e.relation == "imports" || e.relation == "imports_from")
3099 .collect();
3100 assert!(!import_edges.is_empty(), "expected import edge");
3101 assert!(
3102 import_edges
3103 .iter()
3104 .all(|e| e.context.as_deref() == Some("import")),
3105 "import edges must have context='import'"
3106 );
3107 }
3108
3109 #[test]
3110 fn test_dm_unresolved_include_flagged_external() {
3111 let src = load_sample("sample.dm");
3112 let path = sample_path("sample.dm");
3113 let result = DmExtractor::extract(&src, &path).unwrap();
3114 let helpers_edge = result.edges.iter().find(|e| {
3115 e.target.contains("helpers")
3116 && (e.relation == "imports" || e.relation == "imports_from")
3117 });
3118 assert!(helpers_edge.is_some(), "expected edge targeting helpers");
3119 assert_eq!(
3121 helpers_edge.unwrap().relation,
3122 "imports",
3123 "unresolved include must use 'imports' relation"
3124 );
3125 }
3126
3127 #[test]
3128 fn test_dm_resolves_in_file_calls() {
3129 let src = load_sample("sample.dm");
3130 let path = sample_path("sample.dm");
3131 let result = DmExtractor::extract(&src, &path).unwrap();
3132 let node_by_id: std::collections::HashMap<_, _> = result
3133 .nodes
3134 .iter()
3135 .map(|n| (n.id.as_str(), n.label.as_str()))
3136 .collect();
3137 let calls: Vec<(&str, &str)> = result
3138 .edges
3139 .iter()
3140 .filter(|e| e.relation == "calls")
3141 .map(|e| {
3142 (
3143 node_by_id.get(e.source.as_str()).copied().unwrap_or(""),
3144 node_by_id.get(e.target.as_str()).copied().unwrap_or(""),
3145 )
3146 })
3147 .collect();
3148 assert!(
3149 calls.iter().any(|(_, callee)| *callee == "log_event()"),
3150 "expected some proc to call log_event()"
3151 );
3152 assert!(
3153 calls.contains(&(
3154 "/datum/weapon/sword/attack()",
3155 "/datum/weapon/sword/sharpen()"
3156 )),
3157 "expected /datum/weapon/sword/attack() calls /datum/weapon/sword/sharpen()"
3158 );
3159 }
3160
3161 #[test]
3162 fn test_dm_ambiguous_member_call_left_unresolved() {
3163 let src = load_sample("sample.dm");
3164 let path = sample_path("sample.dm");
3165 let result = DmExtractor::extract(&src, &path).unwrap();
3166 let node_by_id: std::collections::HashMap<_, _> = result
3167 .nodes
3168 .iter()
3169 .map(|n| (n.id.as_str(), n.label.as_str()))
3170 .collect();
3171 let runtest_to_attack: Vec<_> = result
3172 .edges
3173 .iter()
3174 .filter(|e| {
3175 e.relation == "calls"
3176 && node_by_id.get(e.source.as_str()).copied() == Some("RunTest()")
3177 && node_by_id
3178 .get(e.target.as_str())
3179 .copied()
3180 .map(|l| l.contains("attack"))
3181 .unwrap_or(false)
3182 })
3183 .collect();
3184 assert!(
3185 runtest_to_attack.is_empty(),
3186 "ambiguous call to 'attack' must not be resolved"
3187 );
3188 }
3189
3190 #[test]
3191 fn test_dm_emits_new_as_instantiates() {
3192 let src = load_sample("sample.dm");
3193 let path = sample_path("sample.dm");
3194 let result = DmExtractor::extract(&src, &path).unwrap();
3195 let node_by_id: std::collections::HashMap<_, _> = result
3196 .nodes
3197 .iter()
3198 .map(|n| (n.id.as_str(), n.label.as_str()))
3199 .collect();
3200 let inst: Vec<(&str, &str)> = result
3201 .edges
3202 .iter()
3203 .filter(|e| e.relation == "instantiates")
3204 .map(|e| {
3205 (
3206 node_by_id.get(e.source.as_str()).copied().unwrap_or(""),
3207 node_by_id.get(e.target.as_str()).copied().unwrap_or(""),
3208 )
3209 })
3210 .collect();
3211 assert!(
3212 inst.contains(&("RunTest()", "/datum/weapon/sword")),
3213 "expected RunTest() instantiates /datum/weapon/sword, got: {:?}",
3214 inst
3215 );
3216 }
3217
3218 #[test]
3219 fn test_dm_call_edges_have_call_context() {
3220 let src = load_sample("sample.dm");
3221 let path = sample_path("sample.dm");
3222 let result = DmExtractor::extract(&src, &path).unwrap();
3223 let call_edges: Vec<_> = result
3224 .edges
3225 .iter()
3226 .filter(|e| e.relation == "calls" || e.relation == "instantiates")
3227 .collect();
3228 assert!(!call_edges.is_empty(), "expected call/instantiates edges");
3229 assert!(
3230 call_edges
3231 .iter()
3232 .all(|e| e.context.as_deref() == Some("call")),
3233 "all call/instantiates edges must have context='call'"
3234 );
3235 }
3236
3237 #[test]
3238 fn test_dm_no_dangling_edges() {
3239 let src = load_sample("sample.dm");
3240 let path = sample_path("sample.dm");
3241 let result = DmExtractor::extract(&src, &path).unwrap();
3242 let node_ids: std::collections::HashSet<_> =
3243 result.nodes.iter().map(|n| n.id.as_str()).collect();
3244 for e in &result.edges {
3245 assert!(
3246 node_ids.contains(e.source.as_str()),
3247 "dangling source: {} (relation={})",
3248 e.source,
3249 e.relation
3250 );
3251 }
3252 }
3253
3254 #[test]
3255 fn test_dm_super_call_not_emitted() {
3256 let src = load_sample("sample.dm");
3257 let path = sample_path("sample.dm");
3258 let result = DmExtractor::extract(&src, &path).unwrap();
3259 let node_by_label: std::collections::HashMap<_, _> = result
3260 .nodes
3261 .iter()
3262 .map(|n| (n.label.as_str(), n.id.as_str()))
3263 .collect();
3264 for n in &result.nodes {
3266 assert!(!n.label.contains(".."), "node with .. label: {}", n.label);
3267 }
3268 for e in &result.edges {
3269 let src_label = node_by_label.get(e.source.as_str()).copied().unwrap_or("");
3270 let tgt_label = node_by_label.get(e.target.as_str()).copied().unwrap_or("");
3271 assert!(
3272 !src_label.contains("..") && !tgt_label.contains(".."),
3273 "edge involving ..: {} -> {}",
3274 e.source,
3275 e.target
3276 );
3277 }
3278 }
3279
3280 #[test]
3283 fn test_dmi_no_error() {
3284 let src = load_sample("sample.dmi");
3285 let path = sample_path("sample.dmi");
3286 let result = DmiExtractor::extract(&src, &path).unwrap();
3287 assert!(!result.nodes.is_empty(), "expected nodes");
3288 }
3289
3290 #[test]
3291 fn test_dmi_emits_state_nodes() {
3292 let src = load_sample("sample.dmi");
3293 let path = sample_path("sample.dmi");
3294 let result = DmiExtractor::extract(&src, &path).unwrap();
3295 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
3296 assert!(labels.contains(&"\"mob\""), "expected \"mob\" state node");
3297 }
3298
3299 #[test]
3300 fn test_dmi_state_contained_by_file() {
3301 let src = load_sample("sample.dmi");
3302 let path = sample_path("sample.dmi");
3303 let result = DmiExtractor::extract(&src, &path).unwrap();
3304 let node_by_id: std::collections::HashMap<_, _> = result
3305 .nodes
3306 .iter()
3307 .map(|n| (n.id.as_str(), n.label.as_str()))
3308 .collect();
3309 let contains: Vec<(&str, &str)> = result
3310 .edges
3311 .iter()
3312 .filter(|e| e.relation == "contains")
3313 .map(|e| {
3314 (
3315 node_by_id.get(e.source.as_str()).copied().unwrap_or(""),
3316 node_by_id.get(e.target.as_str()).copied().unwrap_or(""),
3317 )
3318 })
3319 .collect();
3320 assert!(
3321 contains.contains(&("sample.dmi", "\"mob\"")),
3322 "expected (sample.dmi, \"mob\") contains edge, got: {:?}",
3323 contains
3324 );
3325 }
3326
3327 #[test]
3330 fn test_dmm_no_error() {
3331 let src = load_sample("sample.dmm");
3332 let path = sample_path("sample.dmm");
3333 let result = DmmExtractor::extract(&src, &path).unwrap();
3334 assert!(!result.nodes.is_empty(), "expected nodes");
3335 }
3336
3337 #[test]
3338 fn test_dmm_extracts_type_paths_as_uses_edges() {
3339 let src = load_sample("sample.dmm");
3340 let path = sample_path("sample.dmm");
3341 let result = DmmExtractor::extract(&src, &path).unwrap();
3342 let targets: std::collections::HashSet<_> = result
3343 .edges
3344 .iter()
3345 .filter(|e| e.relation == "uses")
3346 .map(|e| e.target.as_str())
3347 .collect();
3348 assert!(
3350 targets
3351 .iter()
3352 .any(|t| t.contains("turf") && t.contains("closed") && t.contains("wall")),
3353 "expected turf_closed_wall target"
3354 );
3355 assert!(
3356 targets
3357 .iter()
3358 .any(|t| t.contains("obj") && t.contains("structure") && t.contains("table")),
3359 "expected obj_structure_table target"
3360 );
3361 assert!(
3362 targets.iter().any(|t| t.contains("obj")
3363 && t.contains("item")
3364 && t.contains("weapon")
3365 && t.contains("sword")),
3366 "expected obj_item_weapon_sword target"
3367 );
3368 }
3369
3370 #[test]
3371 fn test_dmm_strips_var_overrides() {
3372 let src = load_sample("sample.dmm");
3373 let path = sample_path("sample.dmm");
3374 let result = DmmExtractor::extract(&src, &path).unwrap();
3375 let targets: Vec<_> = result
3376 .edges
3377 .iter()
3378 .filter(|e| e.relation == "uses")
3379 .map(|e| e.target.as_str())
3380 .collect();
3381 assert!(
3382 !targets.iter().any(|t| t.contains('{')),
3383 "no targets should contain {{"
3384 );
3385 assert!(
3386 targets.iter().any(|t| t.contains("sword")),
3387 "sword target should exist after stripping overrides"
3388 );
3389 }
3390
3391 #[test]
3392 fn test_dmm_handles_multiline_tile_definition() {
3393 let src = load_sample("sample.dmm");
3394 let path = sample_path("sample.dmm");
3395 let result = DmmExtractor::extract(&src, &path).unwrap();
3396 let targets: std::collections::HashSet<_> = result
3397 .edges
3398 .iter()
3399 .filter(|e| e.relation == "uses")
3400 .map(|e| e.target.as_str())
3401 .collect();
3402 assert!(
3403 targets
3404 .iter()
3405 .any(|t| t.contains("area") && t.contains("station") && t.contains("maintenance")),
3406 "expected area_station_maintenance from multiline tile"
3407 );
3408 }
3409
3410 #[test]
3411 fn test_dmm_skips_grid_section() {
3412 let src = load_sample("sample.dmm");
3413 let path = sample_path("sample.dmm");
3414 let result = DmmExtractor::extract(&src, &path).unwrap();
3415 let targets: std::collections::HashSet<_> = result
3416 .edges
3417 .iter()
3418 .filter(|e| e.relation == "uses")
3419 .map(|e| e.target.as_str())
3420 .collect();
3421 assert_eq!(
3422 targets.len(),
3423 5,
3424 "expected exactly 5 unique type-path targets, got: {:?}",
3425 targets
3426 );
3427 }
3428
3429 #[test]
3432 fn test_dmf_no_error() {
3433 let src = load_sample("sample.dmf");
3434 let path = sample_path("sample.dmf");
3435 let result = DmfExtractor::extract(&src, &path).unwrap();
3436 assert!(!result.nodes.is_empty(), "expected nodes");
3437 }
3438
3439 #[test]
3440 fn test_dmf_extracts_windows() {
3441 let src = load_sample("sample.dmf");
3442 let path = sample_path("sample.dmf");
3443 let result = DmfExtractor::extract(&src, &path).unwrap();
3444 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
3445 assert!(
3446 labels.contains(&"window \"mapwindow\""),
3447 "expected window mapwindow"
3448 );
3449 assert!(
3450 labels.contains(&"window \"infowindow\""),
3451 "expected window infowindow"
3452 );
3453 }
3454
3455 #[test]
3456 fn test_dmf_elem_labels_carry_control_type() {
3457 let src = load_sample("sample.dmf");
3458 let path = sample_path("sample.dmf");
3459 let result = DmfExtractor::extract(&src, &path).unwrap();
3460 let labels: Vec<_> = result.nodes.iter().map(|n| n.label.as_str()).collect();
3461 assert!(
3462 labels.contains(&"elem \"map\" [MAP]"),
3463 "expected elem \"map\" [MAP], got: {:?}",
3464 labels
3465 );
3466 }
3467
3468 #[test]
3469 fn test_dmf_elem_under_window() {
3470 let src = load_sample("sample.dmf");
3471 let path = sample_path("sample.dmf");
3472 let result = DmfExtractor::extract(&src, &path).unwrap();
3473 let node_by_id: std::collections::HashMap<_, _> = result
3474 .nodes
3475 .iter()
3476 .map(|n| (n.id.as_str(), n.label.as_str()))
3477 .collect();
3478 let contains: Vec<(&str, &str)> = result
3479 .edges
3480 .iter()
3481 .filter(|e| e.relation == "contains")
3482 .map(|e| {
3483 (
3484 node_by_id.get(e.source.as_str()).copied().unwrap_or(""),
3485 node_by_id.get(e.target.as_str()).copied().unwrap_or(""),
3486 )
3487 })
3488 .collect();
3489 assert!(
3490 contains.contains(&("window \"mapwindow\"", "elem \"map\" [MAP]")),
3491 "expected mapwindow contains map elem, got: {:?}",
3492 contains
3493 );
3494 }
3495
3496 #[test]
3497 fn test_dmf_no_dangling_edges() {
3498 let src = load_sample("sample.dmf");
3499 let path = sample_path("sample.dmf");
3500 let result = DmfExtractor::extract(&src, &path).unwrap();
3501 let node_ids: std::collections::HashSet<_> =
3502 result.nodes.iter().map(|n| n.id.as_str()).collect();
3503 for e in &result.edges {
3504 assert!(
3505 node_ids.contains(e.source.as_str()),
3506 "dangling source: {}",
3507 e.source
3508 );
3509 assert!(
3510 node_ids.contains(e.target.as_str()),
3511 "dangling target: {}",
3512 e.target
3513 );
3514 }
3515 }
3516
3517 #[test]
3520 fn test_strip_docstring_triple_quote() {
3521 let raw = r#""""Handles payment processing.""""#;
3522 assert_eq!(
3523 strip_docstring(raw),
3524 Some("Handles payment processing.".into())
3525 );
3526 }
3527
3528 #[test]
3529 fn test_strip_docstring_triple_quote_multiline() {
3530 let raw = "\"\"\"\n Line one.\n Line two.\n \"\"\"";
3531 let result = strip_docstring(raw).unwrap();
3532 assert!(
3533 result.contains("Line one."),
3534 "expected Line one. in {}",
3535 result
3536 );
3537 assert!(
3538 result.contains("Line two."),
3539 "expected Line two. in {}",
3540 result
3541 );
3542 }
3543
3544 #[test]
3545 fn test_strip_docstring_javadoc() {
3546 let raw = "/** Processes payments. */";
3547 assert_eq!(strip_docstring(raw), Some("Processes payments.".into()));
3548 }
3549
3550 #[test]
3551 fn test_strip_docstring_javadoc_multiline() {
3552 let raw = "/**\n * Manages connections.\n * Pooled.\n */";
3553 let result = strip_docstring(raw).unwrap();
3554 assert!(result.contains("Manages connections."), "got: {}", result);
3555 assert!(result.contains("Pooled."), "got: {}", result);
3556 }
3557
3558 #[test]
3559 fn test_strip_docstring_empty_returns_none() {
3560 assert_eq!(strip_docstring(r#"""" """"#), None);
3561 }
3562
3563 #[test]
3566 fn test_run_query_matches_per_match_groups() {
3567 let source = b"class Foo(Bar):\n pass\nclass Baz:\n pass\n";
3568 let lang = tree_sitter_python::LANGUAGE.into();
3569 let query = r#"
3570 (class_definition
3571 name: (identifier) @class.name
3572 superclasses: (argument_list
3573 (identifier) @class.base
3574 )?
3575 )
3576 "#;
3577 let matches = run_query_matches(source, &lang, query).unwrap();
3578 assert_eq!(matches.len(), 2, "expected 2 matches");
3579 let foo_match = matches
3580 .iter()
3581 .find(|m| m.get("class.name").map(|s| s == "Foo").unwrap_or(false));
3582 assert!(foo_match.is_some());
3583 assert_eq!(
3584 foo_match.unwrap().get("class.base").map(|s| s.as_str()),
3585 Some("Bar")
3586 );
3587 let baz_match = matches
3588 .iter()
3589 .find(|m| m.get("class.name").map(|s| s == "Baz").unwrap_or(false));
3590 assert!(baz_match.is_some());
3591 assert!(
3592 baz_match.unwrap().get("class.base").is_none(),
3593 "Baz has no base, capture must be absent"
3594 );
3595 }
3596
3597 #[test]
3600 fn test_python_class_with_docstring() {
3601 let source = b"class Foo:\n \"\"\"Handles payment processing.\"\"\"\n pass\n";
3602 let result = TsPythonExtractor::extract(source, Path::new("test.py")).unwrap();
3603 let foo = result.nodes.iter().find(|n| n.label == "Foo").unwrap();
3604 assert_eq!(foo.docstring, Some("Handles payment processing.".into()));
3605 }
3606
3607 #[test]
3608 fn test_python_class_without_docstring() {
3609 let source = b"class Bar:\n pass\n";
3610 let result = TsPythonExtractor::extract(source, Path::new("test.py")).unwrap();
3611 let bar = result.nodes.iter().find(|n| n.label == "Bar").unwrap();
3612 assert_eq!(bar.docstring, None);
3613 }
3614
3615 #[test]
3616 fn test_python_fn_with_docstring() {
3617 let source = b"def greet(name):\n \"\"\"Say hello.\"\"\"\n pass\n";
3618 let result = TsPythonExtractor::extract(source, Path::new("test.py")).unwrap();
3619 let greet = result.nodes.iter().find(|n| n.label == "greet()").unwrap();
3620 assert_eq!(greet.docstring, Some("Say hello.".into()));
3621 }
3622
3623 #[test]
3624 fn test_python_fn_without_docstring() {
3625 let source = b"def no_doc():\n pass\n";
3626 let result = TsPythonExtractor::extract(source, Path::new("test.py")).unwrap();
3627 let node = result.nodes.iter().find(|n| n.label == "no_doc()").unwrap();
3628 assert_eq!(node.docstring, None);
3629 }
3630
3631 #[test]
3634 fn test_java_class_with_javadoc() {
3635 let source = b"/** Processes payments. */\npublic class PaymentService {}\n";
3636 let result = TsJavaExtractor::extract(source, Path::new("PaymentService.java")).unwrap();
3637 let node = result
3638 .nodes
3639 .iter()
3640 .find(|n| n.label == "PaymentService")
3641 .unwrap();
3642 assert_eq!(node.docstring, Some("Processes payments.".into()));
3643 }
3644
3645 #[test]
3646 fn test_java_class_without_javadoc() {
3647 let source = b"public class NoDoc {}\n";
3648 let result = TsJavaExtractor::extract(source, Path::new("NoDoc.java")).unwrap();
3649 let node = result.nodes.iter().find(|n| n.label == "NoDoc").unwrap();
3650 assert_eq!(node.docstring, None);
3651 }
3652
3653 #[test]
3654 fn test_java_non_javadoc_comment_ignored() {
3655 let source = b"/* Not a javadoc */\npublic class AlsoNoDoc {}\n";
3656 let result = TsJavaExtractor::extract(source, Path::new("AlsoNoDoc.java")).unwrap();
3657 let node = result
3658 .nodes
3659 .iter()
3660 .find(|n| n.label == "AlsoNoDoc")
3661 .unwrap();
3662 assert_eq!(node.docstring, None);
3663 }
3664
3665 #[test]
3666 fn test_java_method_with_javadoc() {
3667 let source =
3668 b"public class Svc {\n /** Charge the card. */\n public void charge() {}\n}\n";
3669 let result = TsJavaExtractor::extract(source, Path::new("Svc.java")).unwrap();
3670 let node = result.nodes.iter().find(|n| n.label == "charge()").unwrap();
3671 assert_eq!(node.docstring, Some("Charge the card.".into()));
3672 }
3673
3674 #[test]
3677 fn test_js_class_with_jsdoc() {
3678 let source = b"/** Handles auth flows. */\nclass AuthManager {}\n";
3679 let result = TsJavaScriptExtractor::extract(source, Path::new("auth.js")).unwrap();
3680 let node = result
3681 .nodes
3682 .iter()
3683 .find(|n| n.label == "AuthManager")
3684 .unwrap();
3685 assert_eq!(node.docstring, Some("Handles auth flows.".into()));
3686 }
3687
3688 #[test]
3689 fn test_js_class_without_jsdoc() {
3690 let source = b"class NoDocs {}\n";
3691 let result = TsJavaScriptExtractor::extract(source, Path::new("nodocs.js")).unwrap();
3692 let node = result.nodes.iter().find(|n| n.label == "NoDocs").unwrap();
3693 assert_eq!(node.docstring, None);
3694 }
3695
3696 #[test]
3699 fn test_js_fn_source_location_set() {
3700 let source = b"function greet(name) {\n return 'hi';\n}\n";
3701 let result = TsJavaScriptExtractor::extract(source, Path::new("greet.js")).unwrap();
3702 let fn_node = result.nodes.iter().find(|n| n.label == "greet()").unwrap();
3703 let loc = fn_node
3704 .source_location
3705 .as_ref()
3706 .expect("source_location set");
3707 let parts: Vec<&str> = loc.split(':').collect();
3708 assert_eq!(parts.len(), 2);
3709 let start: usize = parts[0].parse().unwrap();
3710 let end: usize = parts[1].parse().unwrap();
3711 assert_eq!(start, 0);
3712 assert!(end > start);
3713 assert!(source[start..end].starts_with(b"function"));
3714 }
3715
3716 #[test]
3717 fn test_js_method_source_location_set() {
3718 let source = b"class Foo {\n bar() {\n return 1;\n }\n}\n";
3719 let result = TsJavaScriptExtractor::extract(source, Path::new("foo.js")).unwrap();
3720 let method_node = result.nodes.iter().find(|n| n.label == "bar()").unwrap();
3721 let loc = method_node
3722 .source_location
3723 .as_ref()
3724 .expect("source_location set");
3725 let parts: Vec<&str> = loc.split(':').collect();
3726 assert_eq!(parts.len(), 2);
3727 let start: usize = parts[0].parse().unwrap();
3728 let end: usize = parts[1].parse().unwrap();
3729 assert!(end > start);
3730 }
3731
3732 #[test]
3733 fn test_js_arrow_fn_source_location_set() {
3734 let source = b"const add = (a, b) => a + b;\n";
3735 let result = TsJavaScriptExtractor::extract(source, Path::new("add.js")).unwrap();
3736 let fn_node = result.nodes.iter().find(|n| n.label == "add()").unwrap();
3737 let loc = fn_node
3738 .source_location
3739 .as_ref()
3740 .expect("source_location set");
3741 let parts: Vec<&str> = loc.split(':').collect();
3742 assert_eq!(parts.len(), 2);
3743 let start: usize = parts[0].parse().unwrap();
3744 let end: usize = parts[1].parse().unwrap();
3745 assert!(end > start);
3746 }
3747
3748 #[test]
3749 fn test_js_class_source_location_set() {
3750 let source = b"class Widget {\n render() {}\n}\n";
3751 let result = TsJavaScriptExtractor::extract(source, Path::new("widget.js")).unwrap();
3752 let class_node = result.nodes.iter().find(|n| n.label == "Widget").unwrap();
3753 let loc = class_node
3754 .source_location
3755 .as_ref()
3756 .expect("source_location set");
3757 let parts: Vec<&str> = loc.split(':').collect();
3758 assert_eq!(parts.len(), 2);
3759 let start: usize = parts[0].parse().unwrap();
3760 let end: usize = parts[1].parse().unwrap();
3761 assert_eq!(start, 0);
3762 assert!(end > start);
3763 }
3764
3765 #[test]
3766 fn test_ts_fn_source_location_set() {
3767 let source = b"function parse(input: string): number {\n return 0;\n}\n";
3768 let result = TsTypeScriptExtractor::extract(source, Path::new("parse.ts")).unwrap();
3769 let fn_node = result.nodes.iter().find(|n| n.label == "parse()").unwrap();
3770 let loc = fn_node
3771 .source_location
3772 .as_ref()
3773 .expect("source_location set");
3774 let parts: Vec<&str> = loc.split(':').collect();
3775 assert_eq!(parts.len(), 2);
3776 let start: usize = parts[0].parse().unwrap();
3777 let end: usize = parts[1].parse().unwrap();
3778 assert_eq!(start, 0);
3779 assert!(end > start);
3780 }
3781
3782 #[test]
3783 fn test_java_method_source_location_set() {
3784 let source = b"class Svc {\n void process() {\n return;\n }\n}\n";
3785 let result = TsJavaExtractor::extract(source, Path::new("Svc.java")).unwrap();
3786 let method_node = result
3787 .nodes
3788 .iter()
3789 .find(|n| n.label == "process()")
3790 .unwrap();
3791 let loc = method_node
3792 .source_location
3793 .as_ref()
3794 .expect("source_location set");
3795 let parts: Vec<&str> = loc.split(':').collect();
3796 assert_eq!(parts.len(), 2);
3797 let start: usize = parts[0].parse().unwrap();
3798 let end: usize = parts[1].parse().unwrap();
3799 assert!(end > start);
3800 }
3801
3802 #[test]
3803 fn test_java_class_calls_edges_via_field_injection() {
3804 let source =
3805 b"class OrderService {\n private PaymentGateway paymentGateway;\n private NotificationSender notificationSender;\n void pay() {}\n}\n";
3806 let result = TsJavaExtractor::extract(source, Path::new("OrderService.java")).unwrap();
3807 let calls_edges: Vec<_> = result
3808 .edges
3809 .iter()
3810 .filter(|e| e.relation == "calls")
3811 .collect();
3812 assert_eq!(calls_edges.len(), 2, "expected 2 calls edges");
3813 let targets: Vec<&str> = calls_edges.iter().map(|e| e.target.as_str()).collect();
3814 assert!(
3815 targets.iter().any(|t| t.contains("paymentgateway")),
3816 "expected calls edge to PaymentGateway"
3817 );
3818 assert!(
3819 targets.iter().any(|t| t.contains("notificationsender")),
3820 "expected calls edge to NotificationSender"
3821 );
3822 }
3823
3824 #[test]
3825 fn test_java_class_source_location_set() {
3826 let source = b"class MyService {\n void run() {}\n}\n";
3827 let result = TsJavaExtractor::extract(source, Path::new("MyService.java")).unwrap();
3828 let class_node = result
3829 .nodes
3830 .iter()
3831 .find(|n| n.label == "MyService")
3832 .unwrap();
3833 let loc = class_node
3834 .source_location
3835 .as_ref()
3836 .expect("source_location set");
3837 let parts: Vec<&str> = loc.split(':').collect();
3838 assert_eq!(parts.len(), 2);
3839 let start: usize = parts[0].parse().unwrap();
3840 let end: usize = parts[1].parse().unwrap();
3841 assert_eq!(start, 0);
3842 assert!(end > start);
3843 }
3844
3845 #[test]
3848 fn test_kotlin_class_file_type() {
3849 let source = b"class UserService {\n}\n";
3850 let result = TsKotlinExtractor::extract(source, Path::new("UserService.kt")).unwrap();
3851 let node = result
3852 .nodes
3853 .iter()
3854 .find(|n| n.label == "UserService")
3855 .unwrap();
3856 assert_eq!(node.file_type, "class");
3857 }
3858
3859 #[test]
3860 fn test_kotlin_interface_file_type() {
3861 let source = b"interface Repository {\n}\n";
3862 let result = TsKotlinExtractor::extract(source, Path::new("Repo.kt")).unwrap();
3863 let node = result
3864 .nodes
3865 .iter()
3866 .find(|n| n.label == "Repository")
3867 .unwrap();
3868 assert_eq!(node.file_type, "interface");
3869 }
3870
3871 #[test]
3872 fn test_kotlin_enum_class_file_type() {
3873 let source = b"enum class Status { ACTIVE, INACTIVE }\n";
3874 let result = TsKotlinExtractor::extract(source, Path::new("Status.kt")).unwrap();
3875 let node = result.nodes.iter().find(|n| n.label == "Status").unwrap();
3876 assert_eq!(node.file_type, "enum");
3877 }
3878
3879 #[test]
3880 fn test_kotlin_toplevel_fun_file_type() {
3881 let source = b"fun buildClient(): Client {\n}\n";
3882 let result = TsKotlinExtractor::extract(source, Path::new("client.kt")).unwrap();
3883 let node = result
3884 .nodes
3885 .iter()
3886 .find(|n| n.label == ".buildClient()")
3887 .unwrap();
3888 assert_eq!(node.file_type, "function");
3889 }
3890
3891 #[test]
3892 fn test_kotlin_class_method_file_type() {
3893 let source = b"class Service {\n fun process() {}\n}\n";
3894 let result = TsKotlinExtractor::extract(source, Path::new("Service.kt")).unwrap();
3895 let node = result
3896 .nodes
3897 .iter()
3898 .find(|n| n.label == ".process()")
3899 .unwrap();
3900 assert_eq!(node.file_type, "method");
3901 }
3902
3903 #[test]
3906 fn test_swift_class_file_type() {
3907 let source = b"class AuthManager {\n}\n";
3908 let result = TsSwiftExtractor::extract(source, Path::new("Auth.swift")).unwrap();
3909 let node = result
3910 .nodes
3911 .iter()
3912 .find(|n| n.label == "AuthManager")
3913 .unwrap();
3914 assert_eq!(node.file_type, "class");
3915 }
3916
3917 #[test]
3918 fn test_swift_protocol_file_type() {
3919 let source = b"protocol Authenticatable {\n}\n";
3920 let result = TsSwiftExtractor::extract(source, Path::new("Auth.swift")).unwrap();
3921 let node = result
3922 .nodes
3923 .iter()
3924 .find(|n| n.label == "Authenticatable")
3925 .unwrap();
3926 assert_eq!(node.file_type, "trait");
3927 }
3928
3929 #[test]
3930 fn test_swift_toplevel_function_file_type() {
3931 let source = b"func buildURL(path: String) -> String { return path }\n";
3932 let result = TsSwiftExtractor::extract(source, Path::new("util.swift")).unwrap();
3933 let node = result
3934 .nodes
3935 .iter()
3936 .find(|n| n.label == "buildURL()")
3937 .unwrap();
3938 assert_eq!(node.file_type, "function");
3939 }
3940
3941 #[test]
3942 fn test_swift_method_file_type() {
3943 let source = b"class Session {\n func login() {}\n}\n";
3944 let result = TsSwiftExtractor::extract(source, Path::new("Session.swift")).unwrap();
3945 let node = result.nodes.iter().find(|n| n.label == "login()").unwrap();
3946 assert_eq!(node.file_type, "method");
3947 }
3948
3949 #[test]
3952 fn test_php_class_file_type() {
3953 let source = b"<?php\nclass OrderController {\n}\n";
3954 let result = TsPhpExtractor::extract(source, Path::new("Order.php")).unwrap();
3955 let node = result
3956 .nodes
3957 .iter()
3958 .find(|n| n.label == "OrderController")
3959 .unwrap();
3960 assert_eq!(node.file_type, "class");
3961 }
3962
3963 #[test]
3964 fn test_php_method_file_type() {
3965 let source = b"<?php\nclass OrderController {\n public function create() {}\n}\n";
3966 let result = TsPhpExtractor::extract(source, Path::new("Order.php")).unwrap();
3967 let node = result.nodes.iter().find(|n| n.label == "create()").unwrap();
3968 assert_eq!(node.file_type, "method");
3969 }
3970
3971 #[test]
3972 fn test_php_function_file_type() {
3973 let source = b"<?php\nfunction formatDate($d) { return $d; }\n";
3974 let result = TsPhpExtractor::extract(source, Path::new("utils.php")).unwrap();
3975 let node = result
3976 .nodes
3977 .iter()
3978 .find(|n| n.label == "formatDate()")
3979 .unwrap();
3980 assert_eq!(node.file_type, "function");
3981 }
3982
3983 #[test]
3986 fn test_cpp_class_file_type() {
3987 let source = b"class Connection {\n};\n";
3988 let result = TsCppExtractor::extract(source, Path::new("conn.cpp")).unwrap();
3989 let node = result
3990 .nodes
3991 .iter()
3992 .find(|n| n.label == "Connection")
3993 .unwrap();
3994 assert_eq!(node.file_type, "class");
3995 }
3996
3997 #[test]
3998 fn test_cpp_struct_file_type() {
3999 let source = b"struct Config {\n int port;\n};\n";
4000 let result = TsCppExtractor::extract(source, Path::new("cfg.cpp")).unwrap();
4001 let node = result.nodes.iter().find(|n| n.label == "Config").unwrap();
4002 assert_eq!(node.file_type, "struct");
4003 }
4004
4005 #[test]
4006 fn test_cpp_function_file_type() {
4007 let source = b"int add(int a, int b) { return a + b; }\n";
4008 let result = TsCppExtractor::extract(source, Path::new("math.cpp")).unwrap();
4009 let node = result.nodes.iter().find(|n| n.label == "add()").unwrap();
4010 assert_eq!(node.file_type, "function");
4011 }
4012
4013 #[test]
4016 fn test_dart_class_file_type() {
4017 let source = b"class Widget {\n}\n";
4018 let result = TsDartExtractor::extract(source, Path::new("widget.dart")).unwrap();
4019 let node = result.nodes.iter().find(|n| n.label == "Widget").unwrap();
4020 assert_eq!(node.file_type, "class");
4021 }
4022
4023 #[test]
4026 fn test_groovy_class_file_type() {
4027 let source = b"class Pipeline {\n}\n";
4028 let result = TsGroovyExtractor::extract(source, Path::new("Pipeline.groovy")).unwrap();
4029 let node = result.nodes.iter().find(|n| n.label == "Pipeline").unwrap();
4030 assert_eq!(node.file_type, "class");
4031 }
4032
4033 #[test]
4036 fn test_julia_struct_file_type() {
4037 let source = b"struct Point\n x::Float64\n y::Float64\nend\n";
4038 let result = TsJuliaExtractor::extract(source, Path::new("geom.jl")).unwrap();
4039 let node = result.nodes.iter().find(|n| n.label == "Point").unwrap();
4040 assert_eq!(node.file_type, "struct");
4041 }
4042
4043 #[test]
4044 fn test_julia_abstract_type_file_type() {
4045 let source = b"abstract type Shape end\n";
4046 let result = TsJuliaExtractor::extract(source, Path::new("geom.jl")).unwrap();
4047 let node = result.nodes.iter().find(|n| n.label == "Shape").unwrap();
4048 assert_eq!(node.file_type, "class");
4049 }
4050
4051 #[test]
4054 fn test_zig_struct_file_type() {
4055 let source = b"const Server = struct {\n port: u16,\n};\n";
4056 let result = TsZigExtractor::extract(source, Path::new("server.zig")).unwrap();
4057 let node = result.nodes.iter().find(|n| n.label == "Server").unwrap();
4058 assert_eq!(node.file_type, "struct");
4059 }
4060
4061 #[test]
4064 fn test_bash_function_file_type() {
4065 let source = b"setup() {\n echo hello\n}\n";
4066 let result = TsBashExtractor::extract(source, Path::new("deploy.sh")).unwrap();
4067 let node = result.nodes.iter().find(|n| n.label == "setup()").unwrap();
4068 assert_eq!(node.file_type, "function");
4069 }
4070
4071 #[test]
4074 fn test_c_function_file_type() {
4075 let source = b"int compute(int x) { return x * 2; }\n";
4076 let result = TsCExtractor::extract(source, Path::new("math.c")).unwrap();
4077 let node = result
4078 .nodes
4079 .iter()
4080 .find(|n| n.label == "compute()")
4081 .unwrap();
4082 assert_eq!(node.file_type, "function");
4083 }
4084
4085 #[test]
4088 fn test_vue_component_file_type() {
4089 let source = b"<script>\nexport default {\n name: 'UserCard',\n}\n</script>\n";
4090 let result = TsVueExtractor::extract(source, Path::new("UserCard.vue")).unwrap();
4091 let node = result.nodes.iter().find(|n| n.label == "UserCard").unwrap();
4092 assert_eq!(node.file_type, "class");
4093 }
4094}