Skip to main content

codesynapse_core/ts_extract/
mod.rs

1//! Tree-sitter based language extractors.
2//!
3//! Each language gets its own submodule. Shared helpers live here.
4
5use 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
198/// Walk CST to find preceding `/** */` Javadoc/JSDoc comment for each named item.
199/// Returns map of item_name → cleaned docstring text.
200/// For items with duplicate names, last occurrence wins.
201pub(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
261/// Strip delimiter characters from a raw docstring capture and dedent.
262/// Returns None if the result is empty.
263pub(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    // --- run_query_matches_ranged ---
429
430    #[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    // --- Python source_location ---
460
461    #[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    // --- Python tree-sitter extractor tests ---
506
507    #[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    // --- JavaScript extractor tests ---
622
623    #[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    // --- TypeScript extractor tests ---
677
678    #[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    // --- Go extractor tests ---
765
766    #[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    // --- Rust extractor tests ---
887
888    #[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    // --- Java extractor tests (25-26) ---
990
991    #[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    // --- C extractor tests (27-28) ---
1050
1051    #[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    // --- C++ extractor test (29) ---
1072
1073    #[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    // --- C# extractor tests (30-31) ---
1087
1088    #[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    // --- Kotlin extractor test (32) ---
1163
1164    #[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    // --- Swift extractor test (33) ---
1178
1179    #[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    // --- PHP extractor test (34) ---
1200
1201    #[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    // --- Ruby extractor tests (35+) ---
1215
1216    #[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    // --- SQL extractor tests (36-37) ---
1363
1364    #[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    // --- Vue SFC test (38) ---
1385
1386    #[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    // --- Svelte SFC test (39) ---
1397
1398    #[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    // --- Bash extractor tests (40-41) ---
1409
1410    #[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    // --- Package.json test (42) ---
1431
1432    #[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    // --- MCP config test (43) ---
1447
1448    #[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    // --- CMake extractor test (44) ---
1459
1460    #[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    // --- Dart extractor test (45) ---
1470
1471    #[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    // --- Groovy extractor test (46) ---
1490
1491    #[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    // --- Haskell extractor test (47) ---
1510
1511    #[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    // --- Lua extractor test (48) ---
1530
1531    #[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    // --- Racket extractor test (49) ---
1550
1551    #[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    // --- Scala extractor test (50) ---
1570
1571    #[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    // --- Zig extractor test (51) ---
1590
1591    #[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    // --- Julia extractor tests (52-53) ---
1610
1611    #[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    // --- Elixir extractor tests (54-55) ---
1630
1631    #[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    // --- Pascal extractor tests (56-57) ---
1650
1651    #[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    // --- Markdown extractor tests (58-61) ---
1672
1673    #[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        // Both "Top" headings kept (different IDs, same label)
1718        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        // Only file node
1736        assert_eq!(result.nodes.len(), 1);
1737        assert!(result.edges.is_empty());
1738    }
1739
1740    // --- Astro extractor tests (62-63) ---
1741
1742    #[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    // --- PowerShell extractor tests (64-65) ---
1776
1777    #[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        // class node or file node present
1796        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    // --- Verilog extractor tests (66-67) ---
1805
1806    #[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    // --- Fortran extractor tests (68-69) ---
1824
1825    #[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    // --- ObjC extractor tests (70-71) ---
1843
1844    #[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    // ── Phase 6: sample-fixture tests ───────────────────────────────────────────
1862
1863    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    // ── Java (72–74) ─────────────────────────────────────────────────────────────
1874
1875    #[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    // ── C (75–76) ────────────────────────────────────────────────────────────────
2010
2011    #[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    // ── C++ (77–79) ──────────────────────────────────────────────────────────────
2050
2051    #[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    // ── C# (80–86) ───────────────────────────────────────────────────────────────
2125
2126    #[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    // ── Kotlin (87–88) ───────────────────────────────────────────────────────────
2300
2301    #[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    // ── Scala (89–90) ────────────────────────────────────────────────────────────
2391
2392    #[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        // Scala call extraction is best-effort; only check context if calls exist
2422        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    // ── PHP (91–94) ──────────────────────────────────────────────────────────────
2433
2434    #[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    // ── Swift (95–100) ───────────────────────────────────────────────────────────
2557
2558    #[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    // ── Elixir (101–102) ─────────────────────────────────────────────────────────
2694
2695    #[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    // ── ObjC (103) ───────────────────────────────────────────────────────────────
2738
2739    #[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    // ── Julia (104–105) ──────────────────────────────────────────────────────────
2759
2760    #[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    // ── Fortran (106) ────────────────────────────────────────────────────────────
2800
2801    #[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    // ── Groovy (107) ─────────────────────────────────────────────────────────────
2821
2822    #[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    // ── SLN extractor (108–111) ──────────────────────────────────────────────────
2842
2843    #[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    // ── CSPROJ extractor (112–116) ───────────────────────────────────────────────
2890
2891    #[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    // ── Razor extractor (117–122) ────────────────────────────────────────────────
2952
2953    #[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    // ── DM extractor (123–135) ───────────────────────────────────────────────────
3027
3028    #[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        // Unresolved includes use "imports" relation (not "imports_from")
3120        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        // No node or edge should involve ".."
3265        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    // ── DMI extractor (136–138) ──────────────────────────────────────────────────
3281
3282    #[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    // ── DMM extractor (139–143) ──────────────────────────────────────────────────
3328
3329    #[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        // make_id(&["/turf/closed/wall"]) = "_turf_closed_wall" (leading _ not stripped in Rust)
3349        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    // ── DMF extractor (144–148) ──────────────────────────────────────────────────
3430
3431    #[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    // ── strip_docstring helper ─────────────────────────────────────────────────
3518
3519    #[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    // ── run_query_matches ──────────────────────────────────────────────────────
3564
3565    #[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    // ── Python docstring extraction ────────────────────────────────────────────
3598
3599    #[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    // ── Java docstring extraction ──────────────────────────────────────────────
3632
3633    #[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    // ── JS docstring extraction ────────────────────────────────────────────────
3675
3676    #[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    // --- JS/TS/Java source_location (Priority 2) ---
3697
3698    #[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    // ---- Tier 3: Kotlin ----
3846
3847    #[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    // ---- Tier 3: Swift ----
3904
3905    #[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    // ---- Tier 3: PHP ----
3950
3951    #[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    // ---- Tier 3: C++ ----
3984
3985    #[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    // ---- Tier 3: Dart ----
4014
4015    #[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    // ---- Tier 3: Groovy ----
4024
4025    #[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    // ---- Tier 3: Julia ----
4034
4035    #[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    // ---- Tier 3: Zig ----
4052
4053    #[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    // ---- Tier 4: Bash ----
4062
4063    #[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    // ---- Tier 4: C ----
4072
4073    #[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    // ---- Tier 4: Vue ----
4086
4087    #[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}