Skip to main content

tsift_graph/
lang.rs

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                // `method_declaration` is a func with a receiver; its `name` field
218                // is the method name, which is what callers write at the call site.
219                // Package-level `var`/`const` blocks nest their specs, so the
220                // capture targets the spec's name rather than the declaration.
221                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                // `class_name Foo` declares the script's own type and is the
250                // name every other script refers to it by, so it has to be a
251                // symbol even though it is a statement rather than a block.
252                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                // `pkg.Fn()` and `recv.Method()` are both `selector_expression`,
314                // whose `field` holds the callee name a symbol row can resolve to.
315                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                // A bare `foo()` is `(call (identifier) ...)`, while `a.foo()`
332                // puts the callee in an `attribute_call` under `attribute`, and
333                // the Godot-1.x style `.foo()` super call is `base_call`.
334                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                // tree-sitter-kotlin-ng names these `identifier` /
343                // `navigation_expression`; `simple_identifier` is from the
344                // older tree-sitter-kotlin grammar and does not compile here,
345                // which silently left every Kotlin file with zero call edges.
346                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    /// Whether this language is prose rather than code. A document language is
512    /// parsed into structural nodes — headings, list items, fenced blocks — that
513    /// are useful for navigation but are not symbols: they have no callers, no
514    /// callees, and no identity a caller can look up by name. Consumers that
515    /// report "symbols" must budget and label document nodes separately
516    /// (`#docsym`).
517    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    // #goindex: Go was structural-only (ast-grep could match it, the indexer
909    // could not see it), so `search`, `explain`, and `graph` were blind to every
910    // Go symbol in a Go module while `status` still called the scope `fresh`.
911    #[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    // #csharpindex: C# is a fully indexed language, not only an ast-grep
1007    // grammar. Keep representative type, callable, and value declarations in
1008    // this fixture so grammar upgrades cannot silently erase the index surface.
1009    #[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}