Skip to main content

scope_engine/
treesitter.rs

1use std::path::{Path, PathBuf};
2
3use tree_sitter::StreamingIterator;
4
5use crate::language::LanguageRegistry;
6use crate::selector::{ParsedSelector, SelectorTarget, SymbolKind, SymbolSelector};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct SymbolMatch {
10    pub name: String,
11    pub kind: SymbolKind,
12    pub kind_prefix: &'static str,
13    pub start_line: usize,
14    pub end_line: usize,
15}
16
17fn normalize_path_for_cmp(p: &Path) -> PathBuf {
18    let s = p.to_string_lossy();
19    s.strip_prefix(r"\\?\")
20        .map_or_else(|| p.to_path_buf(), PathBuf::from)
21}
22
23impl SymbolMatch {
24    #[must_use]
25    pub fn canonical_selector(&self, file_path: &Path, project_root: &Path) -> String {
26        let rel_path = normalize_path_for_cmp(file_path)
27            .strip_prefix(normalize_path_for_cmp(project_root))
28            .ok()
29            .map_or_else(
30                || file_path.to_string_lossy().to_string(),
31                |p| p.to_string_lossy().to_string(),
32            )
33            .replace('\\', "/");
34
35        format!(
36            "{}::{}{} #L{}-L{}",
37            rel_path, self.kind_prefix, self.name, self.start_line, self.end_line
38        )
39    }
40
41    #[must_use]
42    pub fn source_from(&self, content: &str) -> String {
43        let lines: Vec<&str> = content.lines().collect();
44        if self.start_line == 0 || self.end_line < self.start_line || self.start_line > lines.len()
45        {
46            return String::new();
47        }
48
49        let start_idx = self.start_line - 1;
50        let end_idx = self.end_line.min(lines.len());
51        let mut snippet = lines[start_idx..end_idx].join("\n");
52        if content.ends_with('\n') || self.end_line < lines.len() {
53            snippet.push('\n');
54        }
55        snippet
56    }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct ParseErrorDiagnostic {
61    pub kind: ParseErrorKind,
62    pub node_kind: String,
63    pub start_line: usize,
64    pub start_column: usize,
65    pub end_line: usize,
66    pub end_column: usize,
67    pub snippet: String,
68}
69
70impl ParseErrorDiagnostic {
71    #[must_use]
72    pub fn message(&self) -> String {
73        format!(
74            "first parse error: {} node `{}` at L{}:C{}-L{}:C{}\n{}",
75            self.kind.as_str(),
76            self.node_kind,
77            self.start_line,
78            self.start_column,
79            self.end_line,
80            self.end_column,
81            self.snippet
82        )
83    }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum ParseErrorKind {
88    Error,
89    Missing,
90}
91
92impl ParseErrorKind {
93    const fn as_str(self) -> &'static str {
94        match self {
95            Self::Error => "ERROR",
96            Self::Missing => "MISSING",
97        }
98    }
99}
100
101pub struct TreeSitterAnalyzer {
102    registry: LanguageRegistry,
103}
104
105impl Default for TreeSitterAnalyzer {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl TreeSitterAnalyzer {
112    #[must_use]
113    pub fn new() -> Self {
114        Self {
115            registry: LanguageRegistry::new(),
116        }
117    }
118
119    /// Given a file path and a 1-based line number, find the innermost
120    /// named definition (function, struct, enum, trait, impl) that contains
121    /// that line. Returns a canonical CodeStruct-style selector like
122    /// `src/foo.rs::fn authenticate #L10-L20`.
123    #[must_use]
124    pub fn find_containing_symbol(
125        &self,
126        file_path: &Path,
127        line_number: usize,
128        project_root: &Path,
129    ) -> Option<String> {
130        self.find_containing_symbol_match(file_path, line_number)
131            .map(|m| m.canonical_selector(file_path, project_root))
132    }
133
134    #[must_use]
135    pub fn find_containing_symbol_match(
136        &self,
137        file_path: &Path,
138        line_number: usize,
139    ) -> Option<SymbolMatch> {
140        let symbols = self.symbols_in_file(file_path).ok()?;
141        symbols
142            .into_iter()
143            .filter(|m| line_number >= m.start_line && line_number <= m.end_line)
144            .max_by_key(|m| (m.start_line, usize::MAX - m.end_line))
145    }
146
147    /// # Errors
148    ///
149    /// Returns an error if the source file cannot be read, parsed, or matched to a symbol.
150    pub fn resolve_selector(
151        &self,
152        file_path: &Path,
153        parsed: &ParsedSelector,
154    ) -> Result<SymbolMatch, String> {
155        let symbols = self.symbols_in_file(file_path)?;
156        let mut matches: Vec<SymbolMatch> = symbols
157            .into_iter()
158            .filter(|m| symbol_matches_selector(m, parsed))
159            .collect();
160
161        let SelectorTarget::Symbol(symbol) = &parsed.target else {
162            return Err(format!(
163                "selector target is not a symbol and cannot be resolved as a symbol: {}",
164                file_path.display()
165            ));
166        };
167
168        if let Some((start, end)) = symbol.line_range {
169            matches.retain(|m| m.start_line == start && m.end_line == end);
170        }
171
172        match matches.len() {
173            0 => Err(format!(
174                "symbol '{}' not found in {}",
175                symbol.name,
176                file_path.display()
177            )),
178            1 => Ok(matches.remove(0)),
179            _ => {
180                let candidates = matches
181                    .iter()
182                    .map(|m| {
183                        format!(
184                            "{}{} #L{}-L{}",
185                            m.kind_prefix, m.name, m.start_line, m.end_line
186                        )
187                    })
188                    .collect::<Vec<_>>()
189                    .join(", ");
190                Err(format!(
191                    "ambiguous selector for '{}' in {}; candidates: {}",
192                    symbol.name,
193                    file_path.display(),
194                    candidates
195                ))
196            }
197        }
198    }
199
200    /// # Errors
201    ///
202    /// Returns an error if the file extension is unsupported, the file cannot be read, or parsing fails.
203    pub fn symbols_in_file(&self, file_path: &Path) -> Result<Vec<SymbolMatch>, String> {
204        let ext = file_path
205            .extension()
206            .and_then(|e| e.to_str())
207            .ok_or_else(|| {
208                format!(
209                    "cannot determine language from file: {}",
210                    file_path.display()
211                )
212            })?;
213        let adapter = self
214            .registry
215            .get(ext)
216            .ok_or_else(|| format!("unsupported language extension: {ext}"))?;
217
218        let content = std::fs::read_to_string(file_path)
219            .map_err(|e| format!("failed to read {}: {e}", file_path.display()))?;
220        let mut parser = adapter.parser();
221        let tree = parser
222            .parse(&content, None)
223            .ok_or_else(|| format!("failed to parse {}", file_path.display()))?;
224        let mut symbols = Vec::new();
225        Self::collect_symbols(tree.root_node(), &content, &mut symbols);
226        Ok(symbols)
227    }
228
229    #[must_use]
230    pub fn is_import_only_reference(&self, file_path: &Path, line_number: usize) -> bool {
231        let Some(ext) = file_path.extension().and_then(|e| e.to_str()) else {
232            return false;
233        };
234        let Some(adapter) = self.registry.get(ext) else {
235            return false;
236        };
237        if adapter.language_name() != "rust" {
238            return false;
239        }
240
241        let Ok(content) = std::fs::read_to_string(file_path) else {
242            return false;
243        };
244        let mut parser = adapter.parser();
245        let Some(tree) = parser.parse(&content, None) else {
246            return false;
247        };
248
249        let Ok(query) = tree_sitter::Query::new(&adapter.language(), RUST_USE_IMPORT_QUERY) else {
250            return false;
251        };
252        let mut cursor = tree_sitter::QueryCursor::new();
253        let mut matches = cursor.matches(&query, tree.root_node(), content.as_bytes());
254        while let Some(query_match) = matches.next() {
255            for capture in query_match.captures {
256                let node = capture.node;
257                let start = node.start_position().row + 1;
258                let end = node.end_position().row + 1;
259                if line_number >= start && line_number <= end {
260                    return true;
261                }
262            }
263        }
264
265        false
266    }
267
268    /// Validate that a file's content can be parsed by tree-sitter.
269    /// Returns true if parsing succeeds (i.e. the file is syntactically valid
270    /// for the given language), false otherwise.
271    #[must_use]
272    pub fn can_parse(&self, ext: &str, content: &str) -> bool {
273        self.parse_error_diagnostic(ext, content).is_none()
274    }
275
276    /// Return the first tree-sitter parse diagnostic for this source, if any.
277    ///
278    /// Tree-sitter does not expose compiler-style syntax diagnostics. It
279    /// recovers by placing ERROR and MISSING nodes in the parse tree. This
280    /// helper reports the first such node with source coordinates and a compact
281    /// snippet so edit rejection messages can point at the likely problem.
282    #[must_use]
283    pub fn parse_error_diagnostic(&self, ext: &str, content: &str) -> Option<ParseErrorDiagnostic> {
284        let Some(adapter) = self.registry.get(ext) else {
285            return Some(ParseErrorDiagnostic {
286                kind: ParseErrorKind::Error,
287                node_kind: format!("unsupported extension `{ext}`"),
288                start_line: 1,
289                start_column: 1,
290                end_line: 1,
291                end_column: 1,
292                snippet: parse_error_snippet(content, 1, 1),
293            });
294        };
295        let mut parser = adapter.parser();
296        let tree = parser.parse(content, None)?;
297        first_parse_error_node(tree.root_node()).map(|node| {
298            let start = node.start_position();
299            let end = node.end_position();
300            let start_line = start.row + 1;
301            let start_column = start.column + 1;
302            ParseErrorDiagnostic {
303                kind: if node.is_missing() {
304                    ParseErrorKind::Missing
305                } else {
306                    ParseErrorKind::Error
307                },
308                node_kind: node.kind().to_string(),
309                start_line,
310                start_column,
311                end_line: end.row + 1,
312                end_column: end.column + 1,
313                snippet: parse_error_snippet(content, start_line, start_column),
314            }
315        })
316    }
317
318    /// Return the SCOPE language adapter that owns semantic source operations
319    /// for the given extension.
320    #[must_use]
321    pub fn responsible_language_for_extension(&self, ext: &str) -> Option<&'static str> {
322        self.registry
323            .get(ext)
324            .map(super::language::LanguageAdapter::language_name)
325    }
326
327    /// Return true when SCOPE owns semantic source operations for this path.
328    #[must_use]
329    pub fn is_responsible_source_path(&self, file_path: &Path) -> bool {
330        file_path
331            .extension()
332            .and_then(|ext| ext.to_str())
333            .and_then(|ext| self.responsible_language_for_extension(ext))
334            .is_some()
335    }
336
337    fn collect_symbols(node: tree_sitter::Node, source: &str, symbols: &mut Vec<SymbolMatch>) {
338        let kind = node.kind();
339        if is_definition_kind(kind)
340            && let Some(name) = Self::extract_def_name(node, source)
341        {
342            let start_line = node.start_position().row + 1;
343            let end_line = node.end_position().row + 1;
344            symbols.push(SymbolMatch {
345                name,
346                kind: SymbolKind::from_ts_node_kind(kind),
347                kind_prefix: kind_prefix(kind),
348                start_line,
349                end_line,
350            });
351        }
352
353        for i in 0..node.child_count() {
354            if let Some(child) = node.child(i) {
355                Self::collect_symbols(child, source, symbols);
356            }
357        }
358    }
359
360    fn extract_def_name(node: tree_sitter::Node, source: &str) -> Option<String> {
361        for i in 0..node.child_count() {
362            let child = node.child(i)?;
363            let kind = child.kind();
364            if kind == "identifier" || kind == "type_identifier" {
365                return child
366                    .utf8_text(source.as_bytes())
367                    .ok()
368                    .map(std::string::ToString::to_string);
369            }
370        }
371        None
372    }
373}
374
375fn symbol_matches_selector(symbol: &SymbolMatch, parsed: &ParsedSelector) -> bool {
376    let Some(selector) = parsed.as_symbol() else {
377        return false;
378    };
379    symbol_matches_symbol_selector(symbol, selector)
380}
381
382fn symbol_matches_symbol_selector(symbol: &SymbolMatch, selector: &SymbolSelector) -> bool {
383    symbol.name == selector.name
384        && (selector.kind == SymbolKind::Unknown || symbol.kind == selector.kind)
385}
386
387fn is_definition_kind(kind: &str) -> bool {
388    matches!(
389        kind,
390        "function_item"
391            | "struct_item"
392            | "enum_item"
393            | "trait_item"
394            | "impl_item"
395            | "function_definition"
396            | "class_definition"
397            | "decorated_definition"
398            | "function_declaration"
399            | "class_declaration"
400            | "interface_declaration"
401            | "enum_declaration"
402            | "method_definition"
403            | "type_alias_declaration"
404    )
405}
406
407fn kind_prefix(kind: &str) -> &'static str {
408    match kind {
409        "struct_item" => "struct ",
410        "enum_item" | "enum_declaration" => "enum ",
411        "trait_item" | "interface_declaration" => "trait ",
412        "impl_item" => "impl ",
413        "class_definition" | "class_declaration" => "class ",
414        "function_item" | "function_definition" | "function_declaration" | "method_definition" => {
415            "fn "
416        }
417        "type_alias_declaration" => "type ",
418        _ => "",
419    }
420}
421
422fn first_parse_error_node(node: tree_sitter::Node<'_>) -> Option<tree_sitter::Node<'_>> {
423    if node.is_error() || node.is_missing() {
424        return Some(node);
425    }
426
427    let mut cursor = node.walk();
428    for child in node.children(&mut cursor) {
429        if (child.has_error() || child.is_error() || child.is_missing())
430            && let Some(found) = first_parse_error_node(child)
431        {
432            return Some(found);
433        }
434    }
435    None
436}
437
438fn parse_error_snippet(content: &str, line: usize, column: usize) -> String {
439    let lines = content.lines().collect::<Vec<_>>();
440    let start_line = line.saturating_sub(2).max(1);
441    let end_line = line
442        .saturating_add(2)
443        .max(start_line)
444        .min(lines.len().max(line));
445    let width = end_line.to_string().len().max(1);
446    let mut snippet = String::new();
447
448    for current_line in start_line..=end_line {
449        let text = lines
450            .get(current_line.saturating_sub(1))
451            .copied()
452            .unwrap_or("");
453        let _ = std::fmt::Write::write_fmt(
454            &mut snippet,
455            format_args!("{current_line:>width$} | {text}\n"),
456        );
457        if current_line == line {
458            let caret_padding = " ".repeat(column.saturating_sub(1));
459            let _ = std::fmt::Write::write_fmt(
460                &mut snippet,
461                format_args!("{:>width$} | {caret_padding}^\n", ""),
462            );
463        }
464    }
465
466    snippet.trim_end().to_string()
467}
468
469const RUST_USE_IMPORT_QUERY: &str = "(use_declaration) @import";
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474    use std::io::Write;
475    use std::path::PathBuf;
476
477    fn write_temp_rust_file(dir: &Path, name: &str, content: &str) -> PathBuf {
478        let path = dir.join(name);
479        let mut f = std::fs::File::create(&path).unwrap();
480        f.write_all(content.as_bytes()).unwrap();
481        path
482    }
483
484    const RUST_CODE: &str = "// line 1\n                 fn startup() {\n                    inner_call();\n                }\n            }\n            ";
485
486    #[test]
487    fn test_find_containing_symbol_fn() {
488        let dir = tempfile::tempdir().unwrap();
489        let path = write_temp_rust_file(dir.path(), "test.rs", RUST_CODE);
490        let analyzer = TreeSitterAnalyzer::new();
491        // Line 4 should be inside startup() (adjusted for the actual structure)
492        let result = analyzer.find_containing_symbol(&path, 3, dir.path());
493        // Just check it doesn't crash; exact line numbers depend on the test string
494        println!("find_containing_symbol result: {result:?}");
495    }
496
497    #[test]
498    fn symbol_match_source_from_returns_exact_line_range() {
499        let symbol = SymbolMatch {
500            name: "target".to_string(),
501            kind: SymbolKind::Function,
502            kind_prefix: "fn ",
503            start_line: 3,
504            end_line: 5,
505        };
506        let content = "line 1\nline 2\nfn target() {\n    body();\n}\nfn other() {}\n";
507        assert_eq!(
508            symbol.source_from(content),
509            "fn target() {\n    body();\n}\n"
510        );
511    }
512
513    #[test]
514    fn tsx_files_use_tsx_parser_and_expose_top_level_functions() {
515        let dir = tempfile::tempdir().unwrap();
516        let path = dir.path().join("status-page.tsx");
517        std::fs::write(
518            &path,
519            "function AgentChatActivityHeader() {\n  return <div />;\n}\n\nfunction agentChatActivityGlyph(bubble: { kind: string }) {\n  return bubble.kind;\n}\n",
520        )
521        .unwrap();
522        let analyzer = TreeSitterAnalyzer::new();
523
524        assert!(analyzer.can_parse("tsx", &std::fs::read_to_string(&path).unwrap()));
525        let symbol = analyzer
526            .resolve_selector(
527                &path,
528                &crate::selector::parse_selector("status-page.tsx::fn agentChatActivityGlyph")
529                    .unwrap(),
530            )
531            .expect("TSX top-level function should resolve");
532        assert_eq!(symbol.name, "agentChatActivityGlyph");
533        assert_eq!(symbol.start_line, 5);
534    }
535
536    #[test]
537    fn canonical_selector_disambiguates_duplicate_method_names() {
538        let dir = tempfile::tempdir().unwrap();
539        let code = r#"trait Hints {
540    fn setup_hints(&self);
541}
542
543struct Alpha;
544struct Beta;
545
546impl Hints for Alpha {
547    fn setup_hints(&self) {
548        println!("alpha");
549    }
550}
551
552impl Hints for Beta {
553    fn setup_hints(&self) {
554        println!("beta");
555    }
556}
557"#;
558        let path = write_temp_rust_file(dir.path(), "dup.rs", code);
559        let analyzer = TreeSitterAnalyzer::new();
560
561        let canonical = analyzer
562            .find_containing_symbol(&path, 16, dir.path())
563            .expect("line inside Beta::setup_hints should resolve");
564        assert!(canonical.starts_with("dup.rs::fn setup_hints #L"));
565        assert!(canonical.contains("-L"));
566
567        let parsed = crate::selector::parse_selector(&canonical).unwrap();
568        let resolved = analyzer.resolve_selector(&path, &parsed).unwrap();
569        assert_eq!(resolved.name, "setup_hints");
570        assert_eq!(resolved.start_line, 15);
571    }
572
573    #[test]
574    fn legacy_duplicate_method_selector_is_rejected_as_ambiguous() {
575        let dir = tempfile::tempdir().unwrap();
576        let code = r"trait Hints {
577    fn setup_hints(&self);
578}
579
580struct Alpha;
581struct Beta;
582
583impl Hints for Alpha {
584    fn setup_hints(&self) {}
585}
586
587impl Hints for Beta {
588    fn setup_hints(&self) {}
589}
590";
591        let path = write_temp_rust_file(dir.path(), "dup.rs", code);
592        let analyzer = TreeSitterAnalyzer::new();
593        let parsed = crate::selector::parse_selector("dup.rs::fn setup_hints").unwrap();
594        let err = analyzer.resolve_selector(&path, &parsed).unwrap_err();
595        assert!(err.contains("ambiguous selector"));
596        assert!(err.contains("#L"));
597    }
598
599    #[test]
600    fn test_can_parse_valid_rust() {
601        let analyzer = TreeSitterAnalyzer::new();
602        let valid = "fn main() { println!(\"hello\"); }";
603        assert!(analyzer.can_parse("rs", valid));
604    }
605
606    #[test]
607    fn rust_use_declaration_is_import_only_reference() {
608        let dir = tempfile::tempdir().unwrap();
609        let code = r"use crate::parser::Parser;
610use crate::{engine::Engine, runtime};
611
612fn run() {
613    Parser::new();
614}
615";
616        let path = write_temp_rust_file(dir.path(), "imports.rs", code);
617        let analyzer = TreeSitterAnalyzer::new();
618
619        assert!(analyzer.is_import_only_reference(&path, 1));
620        assert!(analyzer.is_import_only_reference(&path, 2));
621        assert!(!analyzer.is_import_only_reference(&path, 5));
622    }
623
624    #[test]
625    fn test_can_parse_rejects_rust_error_nodes() {
626        let analyzer = TreeSitterAnalyzer::new();
627        let invalid = "fn main( {\n";
628        assert!(!analyzer.can_parse("rs", invalid));
629    }
630
631    #[test]
632    fn parse_error_diagnostic_reports_location_and_snippet() {
633        let analyzer = TreeSitterAnalyzer::new();
634        let invalid = "fn main( {\n";
635
636        let diagnostic = analyzer
637            .parse_error_diagnostic("rs", invalid)
638            .expect("invalid rust should produce a parse diagnostic");
639        let message = diagnostic.message();
640
641        assert!(message.contains("first parse error:"));
642        assert!(message.contains("L1:C"));
643        assert!(message.contains("fn main( {"));
644        assert!(message.contains('^'));
645    }
646
647    #[test]
648    fn test_can_parse_empty_string() {
649        let analyzer = TreeSitterAnalyzer::new();
650        assert!(analyzer.can_parse("rs", ""));
651    }
652
653    #[test]
654    fn test_can_parse_unknown_language_returns_false() {
655        let analyzer = TreeSitterAnalyzer::new();
656        assert!(!analyzer.can_parse("unknown_ext", "fn main() {}"));
657    }
658
659    #[test]
660    fn test_can_parse_valid_python() {
661        let analyzer = TreeSitterAnalyzer::new();
662        let py_code = "def greet(name):\n    return f\"Hello, {name}!\"\n";
663        assert!(analyzer.can_parse("py", py_code));
664    }
665
666    #[test]
667    fn test_can_parse_valid_go() {
668        let analyzer = TreeSitterAnalyzer::new();
669        let go_code = "package main\nfunc greet(name string) string { return \"Hello\" }\n";
670        assert!(analyzer.can_parse("go", go_code));
671    }
672
673    #[test]
674    fn test_can_parse_valid_java() {
675        let analyzer = TreeSitterAnalyzer::new();
676        let java_code = "public class Hello { public static void main(String[] args) {} }\n";
677        assert!(analyzer.can_parse("java", java_code));
678    }
679
680    #[test]
681    fn test_can_parse_valid_typescript() {
682        let analyzer = TreeSitterAnalyzer::new();
683        let ts_code = "function greet(name: string): string { return \"Hello\"; }\n";
684        assert!(analyzer.can_parse("ts", ts_code));
685    }
686
687    #[test]
688    fn test_can_parse_valid_javascript() {
689        let analyzer = TreeSitterAnalyzer::new();
690        let js_code = "function greet(name) { return \"Hello\"; }\n";
691        assert!(analyzer.can_parse("js", js_code));
692    }
693
694    #[test]
695    fn test_can_parse_valid_c() {
696        let analyzer = TreeSitterAnalyzer::new();
697        let c_code = "int main() { return 0; }\n";
698        assert!(analyzer.can_parse("c", c_code));
699    }
700
701    #[test]
702    fn test_can_parse_valid_cpp() {
703        let analyzer = TreeSitterAnalyzer::new();
704        let cpp_code = "class Hello { public: void greet() {} };\n";
705        assert!(analyzer.can_parse("cpp", cpp_code));
706    }
707
708    #[test]
709    fn test_can_parse_valid_ruby() {
710        let analyzer = TreeSitterAnalyzer::new();
711        let ruby_code = "def greet(name)\n  \"Hello, #{name}!\"\nend\n";
712        assert!(analyzer.can_parse("rb", ruby_code));
713    }
714
715    #[test]
716    fn test_can_parse_valid_php() {
717        let analyzer = TreeSitterAnalyzer::new();
718        let php_code = "<?php\nfunction greet($name) { return \"Hello\"; }\n";
719        assert!(analyzer.can_parse("php", php_code));
720    }
721
722    #[test]
723    fn responsible_source_path_matches_registered_languages() {
724        let analyzer = TreeSitterAnalyzer::new();
725        assert!(analyzer.is_responsible_source_path(Path::new("src/lib.rs")));
726        assert!(analyzer.is_responsible_source_path(Path::new("script.py")));
727        assert!(!analyzer.is_responsible_source_path(Path::new("README.md")));
728        assert!(!analyzer.is_responsible_source_path(Path::new("Makefile")));
729        assert_eq!(
730            analyzer.responsible_language_for_extension("rs"),
731            Some("rust")
732        );
733        assert_eq!(analyzer.responsible_language_for_extension("md"), None);
734    }
735    #[test]
736    fn test_language_registry_has_all_languages() {
737        let registry = LanguageRegistry::new();
738        assert!(registry.get("rs").is_some(), "Rust should be registered");
739        assert!(registry.get("py").is_some(), "Python should be registered");
740        assert!(registry.get("go").is_some(), "Go should be registered");
741        assert!(registry.get("java").is_some(), "Java should be registered");
742        assert!(
743            registry.get("ts").is_some(),
744            "TypeScript should be registered"
745        );
746        assert!(
747            registry.get("js").is_some(),
748            "JavaScript should be registered"
749        );
750        assert!(registry.get("c").is_some(), "C should be registered");
751        assert!(registry.get("cpp").is_some(), "C++ should be registered");
752        assert!(registry.get("rb").is_some(), "Ruby should be registered");
753        assert!(registry.get("php").is_some(), "PHP should be registered");
754    }
755
756    #[test]
757    fn test_language_registry_all_names() {
758        let registry = LanguageRegistry::new();
759        let langs = registry.list_languages();
760        let names: Vec<&str> = langs.iter().map(|(n, _)| *n).collect();
761        assert!(names.contains(&"rust"), "rust in {names:?}");
762        assert!(names.contains(&"python"), "python in {names:?}");
763        assert!(names.contains(&"go"), "go in {names:?}");
764        assert!(names.contains(&"java"), "java in {names:?}");
765        assert!(names.contains(&"typescript"), "typescript in {names:?}");
766        assert!(names.contains(&"javascript"), "javascript in {names:?}");
767        assert!(names.contains(&"c"), "c in {names:?}");
768        assert!(names.contains(&"cpp"), "cpp in {names:?}");
769        assert!(names.contains(&"ruby"), "ruby in {names:?}");
770        assert!(names.contains(&"php"), "php in {names:?}");
771    }
772}