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