1use anyhow::Result;
2use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct Symbol {
6 pub name: String,
7 pub kind: String,
8 pub line: usize,
9 pub end_line: usize,
10 pub node_kind: String,
11 pub start_byte: usize,
12 pub end_byte: usize,
13 pub body_start_byte: Option<usize>,
14 pub body_end_byte: Option<usize>,
15}
16
17#[allow(dead_code)]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum Lang {
20 #[cfg(feature = "lang-rust")]
21 Rust,
22 #[cfg(feature = "lang-python")]
23 Python,
24 #[cfg(feature = "lang-typescript")]
25 TypeScript,
26 #[cfg(feature = "lang-typescript")]
27 Tsx,
28 #[cfg(feature = "lang-javascript")]
29 JavaScript,
30 #[cfg(feature = "lang-javascript")]
31 Jsx,
32 #[cfg(feature = "lang-kotlin")]
33 Kotlin,
34 #[cfg(feature = "lang-zig")]
35 Zig,
36 #[cfg(feature = "lang-bash")]
37 Bash,
38 #[cfg(feature = "lang-go")]
39 Go,
40 #[cfg(feature = "lang-csharp")]
41 CSharp,
42 #[cfg(feature = "lang-gdscript")]
43 GdScript,
44 #[cfg(feature = "lang-markdown")]
45 Markdown,
46}
47
48#[allow(dead_code)]
49impl Lang {
50 pub fn from_extension(ext: &str) -> Option<Self> {
51 match ext {
52 #[cfg(feature = "lang-rust")]
53 "rs" => Some(Self::Rust),
54 #[cfg(feature = "lang-python")]
55 "py" | "pyi" => Some(Self::Python),
56 #[cfg(feature = "lang-typescript")]
57 "ts" => Some(Self::TypeScript),
58 #[cfg(feature = "lang-typescript")]
59 "tsx" => Some(Self::Tsx),
60 #[cfg(feature = "lang-javascript")]
61 "js" | "mjs" | "cjs" => Some(Self::JavaScript),
62 #[cfg(feature = "lang-javascript")]
63 "jsx" => Some(Self::Jsx),
64 #[cfg(feature = "lang-kotlin")]
65 "kt" | "kts" => Some(Self::Kotlin),
66 #[cfg(feature = "lang-zig")]
67 "zig" => Some(Self::Zig),
68 #[cfg(feature = "lang-bash")]
69 "sh" | "bash" | "zsh" => Some(Self::Bash),
70 #[cfg(feature = "lang-go")]
71 "go" => Some(Self::Go),
72 #[cfg(feature = "lang-csharp")]
73 "cs" => Some(Self::CSharp),
74 #[cfg(feature = "lang-gdscript")]
75 "gd" => Some(Self::GdScript),
76 #[cfg(feature = "lang-markdown")]
77 "md" | "mdx" => Some(Self::Markdown),
78 _ => None,
79 }
80 }
81
82 pub fn tree_sitter_language(&self) -> Language {
83 match self {
84 #[cfg(feature = "lang-rust")]
85 Self::Rust => tree_sitter_rust::LANGUAGE.into(),
86 #[cfg(feature = "lang-python")]
87 Self::Python => tree_sitter_python::LANGUAGE.into(),
88 #[cfg(feature = "lang-typescript")]
89 Self::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
90 #[cfg(feature = "lang-typescript")]
91 Self::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
92 #[cfg(feature = "lang-javascript")]
93 Self::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
94 #[cfg(feature = "lang-javascript")]
95 Self::Jsx => tree_sitter_javascript::LANGUAGE.into(),
96 #[cfg(feature = "lang-kotlin")]
97 Self::Kotlin => tree_sitter_kotlin_ng::LANGUAGE.into(),
98 #[cfg(feature = "lang-zig")]
99 Self::Zig => tree_sitter_zig::LANGUAGE.into(),
100 #[cfg(feature = "lang-bash")]
101 Self::Bash => tree_sitter_bash::LANGUAGE.into(),
102 #[cfg(feature = "lang-go")]
103 Self::Go => tree_sitter_go::LANGUAGE.into(),
104 #[cfg(feature = "lang-csharp")]
105 Self::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
106 #[cfg(feature = "lang-gdscript")]
107 Self::GdScript => tree_sitter_gdscript::LANGUAGE.into(),
108 #[cfg(feature = "lang-markdown")]
109 Self::Markdown => tsift_md_ast::markdown_language(),
110 }
111 }
112
113 pub fn name(&self) -> &'static str {
114 match self {
115 #[cfg(feature = "lang-rust")]
116 Self::Rust => "rust",
117 #[cfg(feature = "lang-python")]
118 Self::Python => "python",
119 #[cfg(feature = "lang-typescript")]
120 Self::TypeScript => "typescript",
121 #[cfg(feature = "lang-typescript")]
122 Self::Tsx => "tsx",
123 #[cfg(feature = "lang-javascript")]
124 Self::JavaScript => "javascript",
125 #[cfg(feature = "lang-javascript")]
126 Self::Jsx => "jsx",
127 #[cfg(feature = "lang-kotlin")]
128 Self::Kotlin => "kotlin",
129 #[cfg(feature = "lang-zig")]
130 Self::Zig => "zig",
131 #[cfg(feature = "lang-bash")]
132 Self::Bash => "bash",
133 #[cfg(feature = "lang-go")]
134 Self::Go => "go",
135 #[cfg(feature = "lang-csharp")]
136 Self::CSharp => "csharp",
137 #[cfg(feature = "lang-gdscript")]
138 Self::GdScript => "gdscript",
139 #[cfg(feature = "lang-markdown")]
140 Self::Markdown => "markdown",
141 }
142 }
143
144 pub fn symbol_query(&self) -> &'static str {
145 match self {
146 #[cfg(feature = "lang-rust")]
147 Self::Rust => {
148 r#"
149 (function_item name: (identifier) @function.name)
150 (struct_item name: (type_identifier) @struct.name)
151 (enum_item name: (type_identifier) @enum.name)
152 (trait_item name: (type_identifier) @trait.name)
153 (impl_item type: (type_identifier) @impl.name)
154 (mod_item name: (identifier) @mod.name)
155 (type_item name: (type_identifier) @type_alias.name)
156 (const_item name: (identifier) @const.name)
157 (static_item name: (identifier) @static.name)
158 "#
159 }
160 #[cfg(feature = "lang-python")]
161 Self::Python => {
162 r#"
163 (function_definition name: (identifier) @function.name)
164 (class_definition name: (identifier) @class.name)
165 "#
166 }
167 #[cfg(feature = "lang-typescript")]
168 Self::TypeScript | Self::Tsx => {
169 r#"
170 (function_declaration name: (identifier) @function.name)
171 (class_declaration name: (type_identifier) @class.name)
172 (interface_declaration name: (type_identifier) @interface.name)
173 (type_alias_declaration name: (type_identifier) @type_alias.name)
174 (enum_declaration name: (identifier) @enum.name)
175 (variable_declarator name: (identifier) @function.name value: (arrow_function))
176 "#
177 }
178 #[cfg(feature = "lang-javascript")]
179 Self::JavaScript | Self::Jsx => {
180 r#"
181 (function_declaration name: (identifier) @function.name)
182 (class_declaration name: (identifier) @class.name)
183 (variable_declarator name: (identifier) @function.name value: (arrow_function))
184 "#
185 }
186 #[cfg(feature = "lang-kotlin")]
187 Self::Kotlin => {
188 r#"
189 (function_declaration name: (identifier) @function.name)
190 (class_declaration "interface" name: (identifier) @interface.name)
191 (class_declaration (modifiers (class_modifier "data")) name: (identifier) @data_class.name)
192 (class_declaration (modifiers (class_modifier "sealed")) name: (identifier) @sealed_class.name)
193 (class_declaration (modifiers (class_modifier "enum")) name: (identifier) @enum_class.name)
194 (class_declaration "class" name: (identifier) @class.name)
195 (object_declaration name: (identifier) @object.name)
196 (companion_object name: (identifier) @companion_object.name)
197 "#
198 }
199 #[cfg(feature = "lang-zig")]
200 Self::Zig => {
201 r#"
202 (function_declaration (identifier) @function.name)
203 (variable_declaration (identifier) @struct.name (struct_declaration))
204 (variable_declaration (identifier) @enum.name (enum_declaration))
205 (variable_declaration (identifier) @union.name (union_declaration))
206 (variable_declaration (identifier) @const.name)
207 "#
208 }
209 #[cfg(feature = "lang-bash")]
210 Self::Bash => {
211 r#"
212 (function_definition name: (word) @function.name)
213 "#
214 }
215 #[cfg(feature = "lang-go")]
216 Self::Go => {
217 r#"
222 (function_declaration name: (identifier) @function.name)
223 (method_declaration name: (field_identifier) @method.name)
224 (type_declaration (type_spec name: (type_identifier) @struct.name type: (struct_type)))
225 (type_declaration (type_spec name: (type_identifier) @interface.name type: (interface_type)))
226 (type_declaration (type_spec name: (type_identifier) @type.name))
227 (type_declaration (type_alias name: (type_identifier) @type_alias.name))
228 (const_declaration (const_spec name: (identifier) @const.name))
229 (var_declaration (var_spec name: (identifier) @variable.name))
230 "#
231 }
232 #[cfg(feature = "lang-csharp")]
233 Self::CSharp => {
234 r#"
235 (class_declaration name: (identifier) @class.name)
236 (struct_declaration name: (identifier) @struct.name)
237 (interface_declaration name: (identifier) @interface.name)
238 (enum_declaration name: (identifier) @enum.name)
239 (record_declaration name: (identifier) @record.name)
240 (delegate_declaration name: (identifier) @delegate.name)
241 (method_declaration name: (identifier) @method.name)
242 (local_function_statement name: (identifier) @function.name)
243 (property_declaration name: (identifier) @property.name)
244 (enum_member_declaration name: (identifier) @enum_member.name)
245 "#
246 }
247 #[cfg(feature = "lang-gdscript")]
248 Self::GdScript => {
249 r#"
253 (function_definition name: (name) @function.name)
254 (class_definition name: (name) @class.name)
255 (class_name_statement name: (name) @class.name)
256 (enum_definition name: (name) @enum.name)
257 (signal_statement name: (name) @signal.name)
258 (const_statement name: (name) @const.name)
259 (variable_statement name: (name) @variable.name)
260 (export_variable_statement name: (name) @variable.name)
261 (onready_variable_statement name: (name) @variable.name)
262 "#
263 }
264 #[cfg(feature = "lang-markdown")]
265 Self::Markdown => {
266 r#"
267 (atx_heading (atx_h1_marker) (inline) @heading.name)
268 (atx_heading (atx_h2_marker) (inline) @heading.name)
269 (atx_heading (atx_h3_marker) (inline) @heading.name)
270 (atx_heading (atx_h4_marker) (inline) @heading.name)
271 (atx_heading (atx_h5_marker) (inline) @heading.name)
272 (atx_heading (atx_h6_marker) (inline) @heading.name)
273 (fenced_code_block (info_string (language) @code_block.name))
274 "#
275 }
276 }
277 }
278
279 pub fn call_query(&self) -> Option<&'static str> {
280 match self {
281 #[cfg(feature = "lang-rust")]
282 Self::Rust => Some(
283 r#"
284 (call_expression function: (identifier) @call.name)
285 (call_expression function: (field_expression field: (field_identifier) @call.name))
286 (call_expression function: (scoped_identifier name: (identifier) @call.name))
287 (macro_invocation macro: (identifier) @call.name)
288 "#,
289 ),
290 #[cfg(feature = "lang-python")]
291 Self::Python => Some(
292 r#"
293 (call function: (identifier) @call.name)
294 (call function: (attribute attribute: (identifier) @call.name))
295 "#,
296 ),
297 #[cfg(feature = "lang-typescript")]
298 Self::TypeScript | Self::Tsx => Some(
299 r#"
300 (call_expression function: (identifier) @call.name)
301 (call_expression function: (member_expression property: (property_identifier) @call.name))
302 "#,
303 ),
304 #[cfg(feature = "lang-javascript")]
305 Self::JavaScript | Self::Jsx => Some(
306 r#"
307 (call_expression function: (identifier) @call.name)
308 (call_expression function: (member_expression property: (property_identifier) @call.name))
309 "#,
310 ),
311 #[cfg(feature = "lang-go")]
312 Self::Go => Some(
313 r#"
316(call_expression function: (identifier) @call.name)
317(call_expression function: (selector_expression field: (field_identifier) @call.name))
318"#,
319 ),
320 #[cfg(feature = "lang-csharp")]
321 Self::CSharp => Some(
322 r#"
323(invocation_expression function: (identifier) @call.name)
324(invocation_expression function: (member_access_expression name: (identifier) @call.name))
325(invocation_expression function: (generic_name (identifier) @call.name))
326(invocation_expression function: (member_access_expression name: (generic_name (identifier) @call.name)))
327"#,
328 ),
329 #[cfg(feature = "lang-gdscript")]
330 Self::GdScript => Some(
331 r#"
335 (call (identifier) @call.name)
336 (attribute_call (identifier) @call.name)
337 (base_call (identifier) @call.name)
338 "#,
339 ),
340 #[cfg(feature = "lang-kotlin")]
341 Self::Kotlin => Some(
342 r#"
347(call_expression (identifier) @call.name)
348(call_expression (navigation_expression (identifier) (identifier) @call.name))
349"#,
350 ),
351 #[cfg(feature = "lang-zig")]
352 Self::Zig => Some(
353 r#"
354(call_expression function: (identifier) @call.name)
355(call_expression function: (field_expression member: (identifier) @call.name))
356"#,
357 ),
358 _ => None,
359 }
360 }
361
362 pub fn extract_symbols(&self, source: &[u8]) -> Result<Vec<Symbol>> {
363 let mut parser = Parser::new();
364 let ts_lang = self.tree_sitter_language();
365 parser.set_language(&ts_lang)?;
366 let tree = parser
367 .parse(source, None)
368 .ok_or_else(|| anyhow::anyhow!("parse failed"))?;
369 #[cfg(feature = "lang-markdown")]
370 if *self == Self::Markdown {
371 return Ok(tsift_md_ast::markdown_symbols_from_tree(&tree, source)
372 .into_iter()
373 .map(md_symbol_to_symbol)
374 .collect());
375 }
376 let query = Query::new(&ts_lang, self.symbol_query())?;
377 let mut cursor = QueryCursor::new();
378 let mut symbols = Vec::new();
379 let capture_names: Vec<String> = query
380 .capture_names()
381 .iter()
382 .map(|s| s.to_string())
383 .collect();
384
385 let mut matches = cursor.matches(&query, tree.root_node(), source);
386 while let Some(m) = matches.next() {
387 for capture in m.captures {
388 let capture_name = &capture_names[capture.index as usize];
389 if let Some(kind_str) = capture_name.strip_suffix(".name") {
390 let name = capture
391 .node
392 .utf8_text(source)
393 .unwrap_or("<invalid utf8>")
394 .to_string();
395 let node = symbol_node_for_capture(kind_str, capture.node);
396 let body_span = symbol_body_span(node);
397 symbols.push(Symbol {
398 name,
399 kind: kind_str.to_string(),
400 line: node.start_position().row,
401 end_line: node.end_position().row,
402 node_kind: node.kind().to_string(),
403 start_byte: node.start_byte(),
404 end_byte: node.end_byte(),
405 body_start_byte: body_span.map(|(start, _)| start),
406 body_end_byte: body_span.map(|(_, end)| end),
407 });
408 }
409 }
410 }
411
412 #[cfg(feature = "lang-bash")]
413 if *self == Self::Bash {
414 Self::extract_bash_aliases(&tree, source, &mut symbols);
415 }
416 symbols.sort_by(|a, b| a.line.cmp(&b.line).then(a.name.cmp(&b.name)));
417 symbols.dedup_by(|b, a| {
418 a.name == b.name && a.line == b.line && {
419 let a_generic = matches!(a.kind.as_str(), "variable" | "const");
420 let b_generic = matches!(b.kind.as_str(), "variable" | "const");
421 match (a_generic, b_generic) {
422 (true, false) => a.kind.clone_from(&b.kind),
423 (false, true) => {}
424 _ => {
425 if b.kind.len() > a.kind.len() {
426 a.kind.clone_from(&b.kind);
427 }
428 }
429 }
430 true
431 }
432 });
433 Ok(symbols)
434 }
435
436 #[cfg(feature = "lang-bash")]
437 fn extract_bash_aliases(tree: &tree_sitter::Tree, source: &[u8], symbols: &mut Vec<Symbol>) {
438 let mut tree_cursor = tree.root_node().walk();
439 if !tree_cursor.goto_first_child() {
440 return;
441 }
442 loop {
443 let node = tree_cursor.node();
444 if node.kind() == "command"
445 && let Some(name_node) = node.child_by_field_name("name")
446 {
447 let cmd = name_node.utf8_text(source).unwrap_or("");
448 if cmd == "alias" {
449 for i in 0..node.named_child_count() {
450 if let Some(arg) = node.named_child(i as u32)
451 && (arg.kind() == "concatenation" || arg.kind() == "word")
452 {
453 let text = arg.utf8_text(source).unwrap_or("");
454 if let Some(alias_name) = text.split('=').next()
455 && !alias_name.is_empty()
456 && alias_name != cmd
457 {
458 symbols.push(Symbol {
459 name: alias_name.to_string(),
460 kind: "alias".to_string(),
461 line: arg.start_position().row,
462 end_line: node.end_position().row,
463 node_kind: node.kind().to_string(),
464 start_byte: arg.start_byte(),
465 end_byte: node.end_byte(),
466 body_start_byte: None,
467 body_end_byte: None,
468 });
469 }
470 }
471 }
472 }
473 }
474 if !tree_cursor.goto_next_sibling() {
475 break;
476 }
477 }
478 }
479
480 pub fn all() -> Vec<Self> {
481 vec![
482 #[cfg(feature = "lang-rust")]
483 Self::Rust,
484 #[cfg(feature = "lang-python")]
485 Self::Python,
486 #[cfg(feature = "lang-typescript")]
487 Self::TypeScript,
488 #[cfg(feature = "lang-typescript")]
489 Self::Tsx,
490 #[cfg(feature = "lang-javascript")]
491 Self::JavaScript,
492 #[cfg(feature = "lang-javascript")]
493 Self::Jsx,
494 #[cfg(feature = "lang-kotlin")]
495 Self::Kotlin,
496 #[cfg(feature = "lang-zig")]
497 Self::Zig,
498 #[cfg(feature = "lang-bash")]
499 Self::Bash,
500 #[cfg(feature = "lang-go")]
501 Self::Go,
502 #[cfg(feature = "lang-csharp")]
503 Self::CSharp,
504 #[cfg(feature = "lang-gdscript")]
505 Self::GdScript,
506 #[cfg(feature = "lang-markdown")]
507 Self::Markdown,
508 ]
509 }
510
511 pub fn is_document(&self) -> bool {
518 #[cfg(feature = "lang-markdown")]
519 if *self == Self::Markdown {
520 return true;
521 }
522 false
523 }
524}
525
526fn symbol_node_for_capture<'tree>(
527 kind: &str,
528 name_node: tree_sitter::Node<'tree>,
529) -> tree_sitter::Node<'tree> {
530 let mut node = name_node.parent().unwrap_or(name_node);
531 if kind == "code_block" {
532 while let Some(parent) = node.parent() {
533 node = parent;
534 if node.kind() == "fenced_code_block" {
535 break;
536 }
537 }
538 }
539 node
540}
541
542fn symbol_body_span(node: tree_sitter::Node<'_>) -> Option<(usize, usize)> {
543 if let Some(body) = node.child_by_field_name("body") {
544 return Some((body.start_byte(), body.end_byte()));
545 }
546 for idx in 0..node.named_child_count() {
547 let Some(child) = node.named_child(idx as u32) else {
548 continue;
549 };
550 if matches!(
551 child.kind(),
552 "block"
553 | "declaration_list"
554 | "field_declaration_list"
555 | "enum_variant_list"
556 | "match_block"
557 | "statement_block"
558 | "suite"
559 ) {
560 return Some((child.start_byte(), child.end_byte()));
561 }
562 }
563 None
564}
565
566#[cfg(feature = "lang-markdown")]
567fn md_symbol_to_symbol(md: tsift_md_ast::MdSymbol) -> Symbol {
568 Symbol {
569 name: md.name,
570 kind: md.kind,
571 line: md.line,
572 end_line: md.end_line,
573 node_kind: md.node_kind,
574 start_byte: md.start_byte,
575 end_byte: md.end_byte,
576 body_start_byte: md.body_start_byte,
577 body_end_byte: md.body_end_byte,
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn test_all_grammars_create_parser() {
587 for lang in Lang::all() {
588 let ts_lang = lang.tree_sitter_language();
589 let mut parser = tree_sitter::Parser::new();
590 parser
591 .set_language(&ts_lang)
592 .unwrap_or_else(|e| panic!("failed to set language for {:?}: {}", lang, e));
593 }
594 }
595
596 #[test]
597 fn test_extension_dispatch() {
598 let cases = [
599 ("rs", "rust"),
600 ("py", "python"),
601 ("pyi", "python"),
602 ("ts", "typescript"),
603 ("tsx", "tsx"),
604 ("js", "javascript"),
605 ("mjs", "javascript"),
606 ("cjs", "javascript"),
607 ("jsx", "jsx"),
608 ("kt", "kotlin"),
609 ("kts", "kotlin"),
610 ("zig", "zig"),
611 ("sh", "bash"),
612 ("bash", "bash"),
613 ("zsh", "bash"),
614 ("go", "go"),
615 ("cs", "csharp"),
616 ("gd", "gdscript"),
617 ("md", "markdown"),
618 ("mdx", "markdown"),
619 ];
620 for (ext, expected_name) in cases {
621 let lang = Lang::from_extension(ext)
622 .unwrap_or_else(|| panic!("no language for extension: {ext}"));
623 assert_eq!(lang.name(), expected_name, "wrong language for .{ext}");
624 }
625 }
626
627 #[test]
628 fn test_unknown_extension_returns_none() {
629 assert!(Lang::from_extension("xyz").is_none());
630 assert!(Lang::from_extension("").is_none());
631 assert!(Lang::from_extension("txt").is_none());
632 }
633
634 #[cfg(feature = "lang-rust")]
635 #[test]
636 fn test_parse_rust_snippet() {
637 let lang = Lang::Rust;
638 let mut parser = tree_sitter::Parser::new();
639 parser.set_language(&lang.tree_sitter_language()).unwrap();
640 let tree = parser.parse("fn main() {}", None).unwrap();
641 assert_eq!(tree.root_node().kind(), "source_file");
642 assert!(!tree.root_node().has_error());
643 }
644
645 #[cfg(feature = "lang-python")]
646 #[test]
647 fn test_parse_python_snippet() {
648 let lang = Lang::Python;
649 let mut parser = tree_sitter::Parser::new();
650 parser.set_language(&lang.tree_sitter_language()).unwrap();
651 let tree = parser.parse("def hello():\n pass\n", None).unwrap();
652 assert_eq!(tree.root_node().kind(), "module");
653 assert!(!tree.root_node().has_error());
654 }
655
656 #[cfg(feature = "lang-typescript")]
657 #[test]
658 fn test_parse_typescript_snippet() {
659 let lang = Lang::TypeScript;
660 let mut parser = tree_sitter::Parser::new();
661 parser.set_language(&lang.tree_sitter_language()).unwrap();
662 let tree = parser
663 .parse("function greet(name: string): void {}", None)
664 .unwrap();
665 assert_eq!(tree.root_node().kind(), "program");
666 assert!(!tree.root_node().has_error());
667 }
668
669 #[cfg(feature = "lang-typescript")]
670 #[test]
671 fn test_parse_tsx_snippet() {
672 let lang = Lang::Tsx;
673 let mut parser = tree_sitter::Parser::new();
674 parser.set_language(&lang.tree_sitter_language()).unwrap();
675 let tree = parser
676 .parse("const App = () => <div>hello</div>;", None)
677 .unwrap();
678 assert_eq!(tree.root_node().kind(), "program");
679 assert!(!tree.root_node().has_error());
680 }
681
682 #[cfg(feature = "lang-javascript")]
683 #[test]
684 fn test_parse_javascript_snippet() {
685 let lang = Lang::JavaScript;
686 let mut parser = tree_sitter::Parser::new();
687 parser.set_language(&lang.tree_sitter_language()).unwrap();
688 let tree = parser
689 .parse("function hello() { return 42; }", None)
690 .unwrap();
691 assert_eq!(tree.root_node().kind(), "program");
692 assert!(!tree.root_node().has_error());
693 }
694
695 #[cfg(feature = "lang-kotlin")]
696 #[test]
697 fn test_parse_kotlin_snippet() {
698 let lang = Lang::Kotlin;
699 let mut parser = tree_sitter::Parser::new();
700 parser.set_language(&lang.tree_sitter_language()).unwrap();
701 let tree = parser
702 .parse("fun main() { println(\"hello\") }", None)
703 .unwrap();
704 assert_eq!(tree.root_node().kind(), "source_file");
705 assert!(!tree.root_node().has_error());
706 }
707
708 #[cfg(feature = "lang-zig")]
709 #[test]
710 fn test_parse_zig_snippet() {
711 let lang = Lang::Zig;
712 let mut parser = tree_sitter::Parser::new();
713 parser.set_language(&lang.tree_sitter_language()).unwrap();
714 let tree = parser.parse("pub fn main() !void {}", None).unwrap();
715 assert_eq!(tree.root_node().kind(), "source_file");
716 }
717
718 #[cfg(feature = "lang-bash")]
719 #[test]
720 fn test_parse_bash_snippet() {
721 let lang = Lang::Bash;
722 let mut parser = tree_sitter::Parser::new();
723 parser.set_language(&lang.tree_sitter_language()).unwrap();
724 let tree = parser
725 .parse("#!/bin/bash\nhello() { echo hi; }\n", None)
726 .unwrap();
727 assert_eq!(tree.root_node().kind(), "program");
728 assert!(!tree.root_node().has_error());
729 }
730
731 #[cfg(feature = "lang-gdscript")]
732 #[test]
733 fn test_parse_gdscript_snippet() {
734 let lang = Lang::GdScript;
735 let mut parser = tree_sitter::Parser::new();
736 parser.set_language(&lang.tree_sitter_language()).unwrap();
737 let tree = parser
738 .parse("extends Node\n\nfunc _ready():\n\tprint(\"hi\")\n", None)
739 .unwrap();
740 assert_eq!(tree.root_node().kind(), "source");
741 assert!(!tree.root_node().has_error());
742 }
743
744 #[cfg(feature = "lang-markdown")]
745 #[test]
746 fn test_parse_markdown_snippet() {
747 let lang = Lang::Markdown;
748 let mut parser = tree_sitter::Parser::new();
749 parser.set_language(&lang.tree_sitter_language()).unwrap();
750 let tree = parser.parse("# Hello\n\nSome text.\n", None).unwrap();
751 assert_eq!(tree.root_node().kind(), "document");
752 assert!(!tree.root_node().has_error());
753 }
754
755 #[test]
756 fn test_all_symbol_queries_compile() {
757 for lang in Lang::all() {
758 let ts_lang = lang.tree_sitter_language();
759 tree_sitter::Query::new(&ts_lang, lang.symbol_query())
760 .unwrap_or_else(|e| panic!("query compile failed for {:?}: {}", lang, e));
761 }
762 }
763
764 #[cfg(feature = "lang-rust")]
765 #[test]
766 fn test_extract_rust_symbols() {
767 let source = b"fn main() {}\nstruct Foo;\nenum Bar {}\ntrait Baz {}\nconst X: i32 = 1;\nstatic Y: i32 = 2;\nmod inner {}\ntype Alias = i32;\n";
768 let symbols = Lang::Rust.extract_symbols(source).unwrap();
769 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
770 assert!(names.contains(&"main"), "missing main, got {:?}", names);
771 assert!(names.contains(&"Foo"), "missing Foo, got {:?}", names);
772 assert!(names.contains(&"Bar"), "missing Bar, got {:?}", names);
773 assert!(names.contains(&"Baz"), "missing Baz, got {:?}", names);
774 assert!(names.contains(&"X"), "missing X, got {:?}", names);
775 assert!(names.contains(&"Y"), "missing Y, got {:?}", names);
776 assert!(names.contains(&"inner"), "missing inner, got {:?}", names);
777 assert!(names.contains(&"Alias"), "missing Alias, got {:?}", names);
778 let main_sym = symbols.iter().find(|s| s.name == "main").unwrap();
779 assert_eq!(main_sym.kind, "function");
780 let foo_sym = symbols.iter().find(|s| s.name == "Foo").unwrap();
781 assert_eq!(foo_sym.kind, "struct");
782 }
783
784 #[cfg(feature = "lang-python")]
785 #[test]
786 fn test_extract_python_symbols() {
787 let source =
788 b"def hello():\n pass\n\nclass MyClass:\n def method(self):\n pass\n";
789 let symbols = Lang::Python.extract_symbols(source).unwrap();
790 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
791 assert!(names.contains(&"hello"), "missing hello, got {:?}", names);
792 assert!(
793 names.contains(&"MyClass"),
794 "missing MyClass, got {:?}",
795 names
796 );
797 assert!(names.contains(&"method"), "missing method, got {:?}", names);
798 let cls = symbols.iter().find(|s| s.name == "MyClass").unwrap();
799 assert_eq!(cls.kind, "class");
800 }
801
802 #[cfg(feature = "lang-typescript")]
803 #[test]
804 fn test_extract_typescript_symbols() {
805 let source = b"function greet(name: string): void {}\nclass Foo {}\ninterface Bar {}\ntype Alias = string;\nenum Color { Red, Green }\n";
806 let symbols = Lang::TypeScript.extract_symbols(source).unwrap();
807 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
808 assert!(names.contains(&"greet"), "missing greet, got {:?}", names);
809 assert!(names.contains(&"Foo"), "missing Foo, got {:?}", names);
810 assert!(names.contains(&"Bar"), "missing Bar, got {:?}", names);
811 assert!(names.contains(&"Alias"), "missing Alias, got {:?}", names);
812 assert!(names.contains(&"Color"), "missing Color, got {:?}", names);
813 }
814
815 #[cfg(feature = "lang-javascript")]
816 #[test]
817 fn test_extract_javascript_symbols() {
818 let source = b"function hello() { return 42; }\nclass Widget {}\n";
819 let symbols = Lang::JavaScript.extract_symbols(source).unwrap();
820 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
821 assert!(names.contains(&"hello"), "missing hello, got {:?}", names);
822 assert!(names.contains(&"Widget"), "missing Widget, got {:?}", names);
823 }
824
825 #[cfg(feature = "lang-kotlin")]
826 #[test]
827 fn test_extract_kotlin_symbols() {
828 let source = b"fun main() { println(\"hi\") }\nclass Foo\ninterface Bar\ndata class Baz(val x: Int)\nsealed class Qux\nenum class Color { RED, GREEN }\nobject Singleton\n";
829 let symbols = Lang::Kotlin.extract_symbols(source).unwrap();
830 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
831 assert!(names.contains(&"main"), "missing main, got {:?}", names);
832 assert!(names.contains(&"Foo"), "missing Foo, got {:?}", names);
833 assert!(names.contains(&"Bar"), "missing Bar, got {:?}", names);
834 assert!(names.contains(&"Baz"), "missing Baz, got {:?}", names);
835 assert!(names.contains(&"Qux"), "missing Qux, got {:?}", names);
836 assert!(names.contains(&"Color"), "missing Color, got {:?}", names);
837 assert!(
838 names.contains(&"Singleton"),
839 "missing Singleton, got {:?}",
840 names
841 );
842 let main_sym = symbols.iter().find(|s| s.name == "main").unwrap();
843 assert_eq!(main_sym.kind, "function");
844 let foo_sym = symbols.iter().find(|s| s.name == "Foo").unwrap();
845 assert_eq!(foo_sym.kind, "class");
846 let bar_sym = symbols.iter().find(|s| s.name == "Bar").unwrap();
847 assert_eq!(bar_sym.kind, "interface");
848 let baz_sym = symbols.iter().find(|s| s.name == "Baz").unwrap();
849 assert_eq!(baz_sym.kind, "data_class");
850 let qux_sym = symbols.iter().find(|s| s.name == "Qux").unwrap();
851 assert_eq!(qux_sym.kind, "sealed_class");
852 let color_sym = symbols.iter().find(|s| s.name == "Color").unwrap();
853 assert_eq!(color_sym.kind, "enum_class");
854 let singleton_sym = symbols.iter().find(|s| s.name == "Singleton").unwrap();
855 assert_eq!(singleton_sym.kind, "object");
856 assert_eq!(
857 symbols.len(),
858 7,
859 "expected exactly 7 symbols, got {:?}",
860 symbols
861 );
862 }
863
864 #[cfg(feature = "lang-zig")]
865 #[test]
866 fn test_extract_zig_symbols() {
867 let source = b"const std = @import(\"std\");\npub fn main() !void {}\nconst Point = struct { x: i32, y: i32 };\nconst Color = enum { red, green, blue };\nconst Result = union(enum) { ok: i32, err: []const u8 };\nconst MAX: i32 = 100;\n";
868 let symbols = Lang::Zig.extract_symbols(source).unwrap();
869 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
870 assert!(names.contains(&"main"), "missing main, got {:?}", names);
871 assert!(names.contains(&"Point"), "missing Point, got {:?}", names);
872 assert!(names.contains(&"Color"), "missing Color, got {:?}", names);
873 assert!(names.contains(&"Result"), "missing Result, got {:?}", names);
874 assert!(names.contains(&"std"), "missing std, got {:?}", names);
875 assert!(names.contains(&"MAX"), "missing MAX, got {:?}", names);
876 let main_sym = symbols.iter().find(|s| s.name == "main").unwrap();
877 assert_eq!(main_sym.kind, "function");
878 let point_sym = symbols.iter().find(|s| s.name == "Point").unwrap();
879 assert_eq!(point_sym.kind, "struct");
880 let color_sym = symbols.iter().find(|s| s.name == "Color").unwrap();
881 assert_eq!(color_sym.kind, "enum");
882 let result_sym = symbols.iter().find(|s| s.name == "Result").unwrap();
883 assert_eq!(result_sym.kind, "union");
884 let max_sym = symbols.iter().find(|s| s.name == "MAX").unwrap();
885 assert_eq!(max_sym.kind, "const");
886 }
887
888 #[cfg(feature = "lang-bash")]
889 #[test]
890 fn test_extract_bash_symbols() {
891 let source = b"#!/bin/bash\nhello() { echo hi; }\nfunction world { echo world; }\nalias ll='ls -la'\nalias grep='grep --color=auto'\n";
892 let symbols = Lang::Bash.extract_symbols(source).unwrap();
893 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
894 assert!(names.contains(&"hello"), "missing hello, got {:?}", names);
895 assert!(names.contains(&"world"), "missing world, got {:?}", names);
896 assert!(names.contains(&"ll"), "missing alias ll, got {:?}", names);
897 assert!(
898 names.contains(&"grep"),
899 "missing alias grep, got {:?}",
900 names
901 );
902 let hello_sym = symbols.iter().find(|s| s.name == "hello").unwrap();
903 assert_eq!(hello_sym.kind, "function");
904 let ll_sym = symbols.iter().find(|s| s.name == "ll").unwrap();
905 assert_eq!(ll_sym.kind, "alias");
906 }
907
908 #[cfg(feature = "lang-go")]
912 #[test]
913 fn test_extract_go_symbols() {
914 let source = br#"package native
915
916import "fmt"
917
918type Opener interface {
919 Open() error
920}
921
922type Clipboard struct {
923 buf string
924}
925
926const DefaultTimeout = 5
927
928var globalClipboard Clipboard
929
930type Handle = Clipboard
931
932func open() error {
933 return nil
934}
935
936func (c *Clipboard) Set(text string) error {
937 fmt.Println(text)
938 return open()
939}
940"#;
941 let symbols = Lang::Go.extract_symbols(source).unwrap();
942 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
943 for expected in [
944 "Opener",
945 "Clipboard",
946 "DefaultTimeout",
947 "globalClipboard",
948 "Handle",
949 "open",
950 "Set",
951 ] {
952 assert!(
953 names.contains(&expected),
954 "missing {expected}, got {names:?}"
955 );
956 }
957 let kind_of = |name: &str| {
958 symbols
959 .iter()
960 .find(|s| s.name == name)
961 .unwrap_or_else(|| panic!("missing {name}"))
962 .kind
963 .clone()
964 };
965 assert_eq!(kind_of("Opener"), "interface");
966 assert_eq!(kind_of("Clipboard"), "struct");
967 assert_eq!(kind_of("open"), "function");
968 assert_eq!(kind_of("Set"), "method");
969 }
970
971 #[cfg(feature = "lang-go")]
972 #[test]
973 fn test_extract_go_call_edges() {
974 let source = br#"package main
975
976import "fmt"
977
978func helper() int { return 1 }
979
980func main() {
981 helper()
982 fmt.Println("hi")
983}
984"#;
985 let symbols = Lang::Go.extract_symbols(source).unwrap();
986 let call_sites = crate::extract_call_sites(Lang::Go, source).unwrap();
987 let edges = crate::resolve_edges(&symbols, &call_sites);
988 let pairs: Vec<String> = edges
989 .iter()
990 .map(|edge| format!("{} -> {}", edge.caller, edge.callee))
991 .collect();
992 assert!(
993 pairs.contains(&"main -> helper".to_string()),
994 "expected a main -> helper call edge, got {pairs:?}"
995 );
996 let callees: Vec<&str> = call_sites
997 .iter()
998 .map(|site| site.callee.as_str())
999 .collect();
1000 assert!(
1001 callees.contains(&"Println"),
1002 "selector calls resolve to the field name, got {callees:?}"
1003 );
1004 }
1005
1006 #[cfg(feature = "lang-csharp")]
1010 #[test]
1011 fn test_extract_csharp_symbols() {
1012 let source = br#"namespace Native;
1013
1014public interface IOpener { void Open(); }
1015public record Handle(string Value);
1016public delegate void Changed();
1017public enum State { Ready }
1018public struct Clipboard {
1019 public string Text { get; set; }
1020 public Clipboard(string text) { Text = text; }
1021 public void Set(string text) { Text = text; }
1022 public static void Reset() { }
1023}
1024"#;
1025 let symbols = Lang::CSharp.extract_symbols(source).unwrap();
1026 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1027 for expected in [
1028 "IOpener", "Handle", "Changed", "State", "Ready", "Clipboard", "Text",
1029 "Set", "Reset",
1030 ] {
1031 assert!(names.contains(&expected), "missing {expected}, got {names:?}");
1032 }
1033 let kind_of = |name: &str| {
1034 symbols
1035 .iter()
1036 .find(|s| s.name == name)
1037 .unwrap_or_else(|| panic!("missing {name}"))
1038 .kind
1039 .clone()
1040 };
1041 assert_eq!(kind_of("IOpener"), "interface");
1042 assert_eq!(kind_of("Handle"), "record");
1043 assert_eq!(kind_of("Changed"), "delegate");
1044 assert_eq!(kind_of("Set"), "method");
1045 assert_eq!(kind_of("Text"), "property");
1046 }
1047
1048 #[cfg(feature = "lang-csharp")]
1049 #[test]
1050 fn test_extract_csharp_call_edges() {
1051 let source = br#"class Program {
1052 static int Helper() => 1;
1053 static int Main() {
1054 Console.WriteLine("hi");
1055 return Helper();
1056 }
1057}
1058"#;
1059 let symbols = Lang::CSharp.extract_symbols(source).unwrap();
1060 let call_sites = crate::extract_call_sites(Lang::CSharp, source).unwrap();
1061 let edges = crate::resolve_edges(&symbols, &call_sites);
1062 let pairs: Vec<String> = edges
1063 .iter()
1064 .map(|edge| format!("{} -> {}", edge.caller, edge.callee))
1065 .collect();
1066 assert!(
1067 pairs.contains(&"Main -> Helper".to_string()),
1068 "expected a Main -> Helper call edge, got {pairs:?}"
1069 );
1070 assert!(
1071 call_sites.iter().any(|site| site.callee == "WriteLine"),
1072 "member invocations must expose their method name: {call_sites:?}"
1073 );
1074 }
1075
1076 #[cfg(feature = "lang-gdscript")]
1077 #[test]
1078 fn test_extract_gdscript_symbols() {
1079 let source = b"class_name Player\nextends CharacterBody2D\n\nsignal died(cause)\n\nenum State { IDLE, RUNNING }\n\nconst SPEED = 300.0\n\n@export var health := 100\nvar velocity_scale := 1.0\n@onready var sprite = $Sprite2D\n\nclass Inventory:\n\tvar slots = []\n\nfunc _ready():\n\tset_physics_process(true)\n";
1080 let symbols = Lang::GdScript.extract_symbols(source).unwrap();
1081 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1082 for expected in [
1083 "Player",
1084 "died",
1085 "State",
1086 "SPEED",
1087 "health",
1088 "velocity_scale",
1089 "sprite",
1090 "Inventory",
1091 "_ready",
1092 ] {
1093 assert!(
1094 names.contains(&expected),
1095 "missing {expected}, got {names:?}"
1096 );
1097 }
1098 let kind_of = |name: &str| {
1099 symbols
1100 .iter()
1101 .find(|s| s.name == name)
1102 .unwrap_or_else(|| panic!("missing {name}"))
1103 .kind
1104 .clone()
1105 };
1106 assert_eq!(kind_of("Player"), "class");
1107 assert_eq!(kind_of("Inventory"), "class");
1108 assert_eq!(kind_of("_ready"), "function");
1109 assert_eq!(kind_of("died"), "signal");
1110 assert_eq!(kind_of("State"), "enum");
1111 assert_eq!(kind_of("SPEED"), "const");
1112 assert_eq!(kind_of("health"), "variable");
1113 assert_eq!(kind_of("sprite"), "variable");
1114 }
1115
1116 #[cfg(feature = "lang-markdown")]
1117 #[test]
1118 fn test_extract_markdown_symbols() {
1119 let source = b"# Title\n\n## Section One\n\nSome text.\n\n- Run setup\n - Confirm setup\n\n```rust\nfn main() {}\n```\n\n### Subsection\n\n```python\ndef hello():\n pass\n```\n\n## Next Section\n\nDone.\n";
1120 let symbols = Lang::Markdown.extract_symbols(source).unwrap();
1121 let headings: Vec<&Symbol> = symbols.iter().filter(|s| s.kind == "heading").collect();
1122 let code_blocks: Vec<&Symbol> = symbols.iter().filter(|s| s.kind == "code_block").collect();
1123 let list_items: Vec<&Symbol> = symbols.iter().filter(|s| s.kind == "list_item").collect();
1124 assert_eq!(headings.len(), 4, "expected 4 headings, got {:?}", headings);
1125 assert_eq!(
1126 code_blocks.len(),
1127 2,
1128 "expected 2 code blocks, got {:?}",
1129 code_blocks
1130 );
1131 assert_eq!(
1132 list_items.len(),
1133 2,
1134 "expected 2 list items, got {:?}",
1135 list_items
1136 );
1137 let title = headings.iter().find(|s| s.name == "Title").unwrap();
1138 let section = headings.iter().find(|s| s.name == "Section One").unwrap();
1139 let next = headings.iter().find(|s| s.name == "Next Section").unwrap();
1140 assert_eq!(title.node_kind, "atx_heading");
1141 assert!(title.end_byte > next.start_byte);
1142 assert_eq!(section.end_byte, next.start_byte);
1143 assert!(
1144 section.body_start_byte.unwrap() > section.start_byte,
1145 "heading body should begin after the marker line"
1146 );
1147 assert!(
1148 code_blocks.iter().any(|s| s.name == "rust"),
1149 "missing rust block, got {:?}",
1150 code_blocks
1151 );
1152 assert!(
1153 code_blocks.iter().any(|s| s.name == "python"),
1154 "missing python block, got {:?}",
1155 code_blocks
1156 );
1157 assert!(
1158 list_items.iter().any(|s| s.name == "Run setup"),
1159 "missing top-level list item, got {:?}",
1160 list_items
1161 );
1162 }
1163
1164 #[cfg(feature = "lang-python")]
1165 #[test]
1166 fn test_python_async_def() {
1167 let source = b"async def fetch_data():\n await get()\n\ndef sync_fn():\n pass\n";
1168 let symbols = Lang::Python.extract_symbols(source).unwrap();
1169 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1170 assert!(
1171 names.contains(&"fetch_data"),
1172 "missing async function, got {:?}",
1173 names
1174 );
1175 assert!(
1176 names.contains(&"sync_fn"),
1177 "missing sync function, got {:?}",
1178 names
1179 );
1180 }
1181
1182 #[cfg(feature = "lang-python")]
1183 #[test]
1184 fn test_python_decorated_function() {
1185 let source = b"@staticmethod\ndef helper():\n pass\n\n@property\ndef name(self):\n return self._name\n";
1186 let symbols = Lang::Python.extract_symbols(source).unwrap();
1187 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1188 assert!(
1189 names.contains(&"helper"),
1190 "missing decorated function, got {:?}",
1191 names
1192 );
1193 assert!(
1194 names.contains(&"name"),
1195 "missing property function, got {:?}",
1196 names
1197 );
1198 }
1199
1200 #[cfg(feature = "lang-typescript")]
1201 #[test]
1202 fn test_typescript_arrow_exports() {
1203 let source = b"export const Foo = () => { return 42; };\nexport const Bar = (x: number): number => x + 1;\nconst local = () => {};\nfunction regular() {}\n";
1204 let symbols = Lang::TypeScript.extract_symbols(source).unwrap();
1205 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1206 assert!(
1207 names.contains(&"Foo"),
1208 "missing arrow export Foo, got {:?}",
1209 names
1210 );
1211 assert!(
1212 names.contains(&"Bar"),
1213 "missing arrow export Bar, got {:?}",
1214 names
1215 );
1216 assert!(
1217 names.contains(&"local"),
1218 "missing local arrow, got {:?}",
1219 names
1220 );
1221 assert!(
1222 names.contains(&"regular"),
1223 "missing regular function, got {:?}",
1224 names
1225 );
1226 }
1227
1228 #[cfg(feature = "lang-typescript")]
1229 #[test]
1230 fn test_tsx_arrow_component() {
1231 let source = b"export const MyComponent = () => <div>hello</div>;\nfunction Other() { return <span/>; }\n";
1232 let symbols = Lang::Tsx.extract_symbols(source).unwrap();
1233 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1234 assert!(
1235 names.contains(&"MyComponent"),
1236 "missing arrow component, got {:?}",
1237 names
1238 );
1239 assert!(
1240 names.contains(&"Other"),
1241 "missing function component, got {:?}",
1242 names
1243 );
1244 }
1245
1246 #[cfg(feature = "lang-javascript")]
1247 #[test]
1248 fn test_javascript_arrow_exports() {
1249 let source = b"export const handler = () => { return 'ok'; };\nconst helper = (x) => x * 2;\nfunction regular() {}\n";
1250 let symbols = Lang::JavaScript.extract_symbols(source).unwrap();
1251 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1252 assert!(
1253 names.contains(&"handler"),
1254 "missing arrow export, got {:?}",
1255 names
1256 );
1257 assert!(
1258 names.contains(&"helper"),
1259 "missing local arrow, got {:?}",
1260 names
1261 );
1262 assert!(
1263 names.contains(&"regular"),
1264 "missing regular function, got {:?}",
1265 names
1266 );
1267 }
1268
1269 #[cfg(feature = "lang-javascript")]
1270 #[test]
1271 fn test_jsx_arrow_component() {
1272 let source = b"const App = () => <div>hi</div>;\nfunction Page() { return <main/>; }\n";
1273 let symbols = Lang::Jsx.extract_symbols(source).unwrap();
1274 let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
1275 assert!(
1276 names.contains(&"App"),
1277 "missing arrow JSX component, got {:?}",
1278 names
1279 );
1280 assert!(
1281 names.contains(&"Page"),
1282 "missing function component, got {:?}",
1283 names
1284 );
1285 }
1286}