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 #[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 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 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 #[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 #[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 #[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 #[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 let result = analyzer.find_containing_symbol(&path, 3, dir.path());
493 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}