Skip to main content

mimir_graph/
languages.rs

1//! Per-language tree-sitter adapters: which AST nodes are definitions,
2//! scopes, calls, and imports — and how to read docs/signatures off them.
3
4use tree_sitter::Node;
5
6use crate::extract::ImportRef;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Lang {
10    Rust,
11    TypeScript,
12    Tsx,
13    Python,
14    Go,
15    Java,
16    Ruby,
17    C,
18    CSharp,
19    Sql,
20}
21
22impl Lang {
23    pub fn from_path(path: &str) -> Option<Lang> {
24        let ext = path.rsplit('.').next()?;
25        Some(match ext {
26            "rs" => Lang::Rust,
27            "ts" | "mts" | "cts" => Lang::TypeScript,
28            "tsx" | "jsx" | "js" | "mjs" | "cjs" => Lang::Tsx,
29            "py" | "pyi" => Lang::Python,
30            "go" => Lang::Go,
31            "java" => Lang::Java,
32            "rb" | "rake" => Lang::Ruby,
33            "c" | "h" => Lang::C,
34            "cs" | "csx" => Lang::CSharp,
35            "sql" => Lang::Sql,
36            _ => return None,
37        })
38    }
39
40    pub fn name(&self) -> &'static str {
41        match self {
42            Lang::Rust => "rust",
43            Lang::TypeScript | Lang::Tsx => "typescript",
44            Lang::Python => "python",
45            Lang::Go => "go",
46            Lang::Java => "java",
47            Lang::Ruby => "ruby",
48            Lang::C => "c",
49            Lang::CSharp => "csharp",
50            Lang::Sql => "sql",
51        }
52    }
53
54    pub fn language(&self) -> tree_sitter::Language {
55        match self {
56            Lang::Rust => tree_sitter_rust::LANGUAGE.into(),
57            Lang::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
58            Lang::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
59            Lang::Python => tree_sitter_python::LANGUAGE.into(),
60            Lang::Go => tree_sitter_go::LANGUAGE.into(),
61            Lang::Java => tree_sitter_java::LANGUAGE.into(),
62            Lang::Ruby => tree_sitter_ruby::LANGUAGE.into(),
63            Lang::C => tree_sitter_c::LANGUAGE.into(),
64            Lang::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
65            Lang::Sql => tree_sitter_sequel_tsql::LANGUAGE.into(),
66        }
67    }
68
69    pub fn separator(&self) -> String {
70        "::".into()
71    }
72
73    /// Is this node a symbol definition? Returns (name, kind). The name may
74    /// be pre-qualified with `::` (Go methods carry their receiver).
75    pub fn definition(&self, node: Node, src: &str) -> Option<(String, &'static str)> {
76        let text = |n: Node| src[n.byte_range()].to_string();
77        match self {
78            Lang::Rust => match node.kind() {
79                "function_item" => Some((text(node.child_by_field_name("name")?), "function")),
80                "struct_item" => Some((text(node.child_by_field_name("name")?), "struct")),
81                "enum_item" => Some((text(node.child_by_field_name("name")?), "enum")),
82                "trait_item" => Some((text(node.child_by_field_name("name")?), "trait")),
83                "union_item" => Some((text(node.child_by_field_name("name")?), "struct")),
84                _ => None,
85            },
86            Lang::TypeScript | Lang::Tsx => match node.kind() {
87                "function_declaration" | "generator_function_declaration" => {
88                    Some((text(node.child_by_field_name("name")?), "function"))
89                }
90                "class_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
91                "method_definition" => {
92                    let name = text(node.child_by_field_name("name")?);
93                    if name == "constructor" {
94                        return None;
95                    }
96                    Some((name, "method"))
97                }
98                "interface_declaration" => {
99                    Some((text(node.child_by_field_name("name")?), "interface"))
100                }
101                "enum_declaration" => Some((text(node.child_by_field_name("name")?), "enum")),
102                "type_alias_declaration" => Some((text(node.child_by_field_name("name")?), "type")),
103                // const f = (..) => ..  /  const f = function(..) {..}
104                "variable_declarator" => {
105                    let value = node.child_by_field_name("value")?;
106                    if matches!(value.kind(), "arrow_function" | "function_expression") {
107                        let name = node.child_by_field_name("name")?;
108                        if name.kind() == "identifier" {
109                            return Some((text(name), "function"));
110                        }
111                    }
112                    None
113                }
114                _ => None,
115            },
116            Lang::Python => match node.kind() {
117                "function_definition" => {
118                    Some((text(node.child_by_field_name("name")?), "function"))
119                }
120                "class_definition" => Some((text(node.child_by_field_name("name")?), "class")),
121                _ => None,
122            },
123            Lang::Go => match node.kind() {
124                "function_declaration" => {
125                    Some((text(node.child_by_field_name("name")?), "function"))
126                }
127                "method_declaration" => {
128                    let name = text(node.child_by_field_name("name")?);
129                    let recv = node
130                        .child_by_field_name("receiver")
131                        .and_then(|r| receiver_type(r, src));
132                    Some((
133                        match recv {
134                            Some(t) => format!("{t}::{name}"),
135                            None => name,
136                        },
137                        "method",
138                    ))
139                }
140                "type_spec" => {
141                    let name = text(node.child_by_field_name("name")?);
142                    let kind = match node.child_by_field_name("type").map(|t| t.kind()) {
143                        Some("struct_type") => "struct",
144                        Some("interface_type") => "interface",
145                        _ => "type",
146                    };
147                    Some((name, kind))
148                }
149                _ => None,
150            },
151            Lang::Java => match node.kind() {
152                "class_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
153                "interface_declaration" => {
154                    Some((text(node.child_by_field_name("name")?), "interface"))
155                }
156                "enum_declaration" => Some((text(node.child_by_field_name("name")?), "enum")),
157                "record_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
158                "method_declaration" => Some((text(node.child_by_field_name("name")?), "method")),
159                "constructor_declaration" => {
160                    Some((text(node.child_by_field_name("name")?), "method"))
161                }
162                _ => None,
163            },
164            Lang::Ruby => match node.kind() {
165                "class" => Some((const_name(node.child_by_field_name("name")?, src), "class")),
166                "module" => Some((const_name(node.child_by_field_name("name")?, src), "module")),
167                "method" => Some((text(node.child_by_field_name("name")?), "method")),
168                // def self.foo — a class method.
169                "singleton_method" => Some((text(node.child_by_field_name("name")?), "method")),
170                _ => None,
171            },
172            Lang::C => match node.kind() {
173                "function_definition" => Some((
174                    c_declarator_name(node.child_by_field_name("declarator")?, src)?,
175                    "function",
176                )),
177                "struct_specifier" => Some((text(node.child_by_field_name("name")?), "struct")),
178                "union_specifier" => Some((text(node.child_by_field_name("name")?), "struct")),
179                "enum_specifier" => Some((text(node.child_by_field_name("name")?), "enum")),
180                _ => None,
181            },
182            Lang::CSharp => match node.kind() {
183                // Both `namespace Foo { .. }` and file-scoped `namespace Foo;`
184                // — a scope that qualifies the types nested under it.
185                "namespace_declaration" | "file_scoped_namespace_declaration" => {
186                    Some((text(node.child_by_field_name("name")?), "namespace"))
187                }
188                "class_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
189                "interface_declaration" => {
190                    Some((text(node.child_by_field_name("name")?), "interface"))
191                }
192                "struct_declaration" => Some((text(node.child_by_field_name("name")?), "struct")),
193                "enum_declaration" => Some((text(node.child_by_field_name("name")?), "enum")),
194                // Records are reference types by default; treat as a class.
195                "record_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
196                "record_struct_declaration" => {
197                    Some((text(node.child_by_field_name("name")?), "struct"))
198                }
199                "method_declaration" => Some((text(node.child_by_field_name("name")?), "method")),
200                "property_declaration" => {
201                    Some((text(node.child_by_field_name("name")?), "property"))
202                }
203                // Constructor's name field is the enclosing type identifier.
204                "constructor_declaration" => {
205                    Some((text(node.child_by_field_name("name")?), "constructor"))
206                }
207                _ => None,
208            },
209            Lang::Sql => match node.kind() {
210                "create_table" => sql_def_name(node, src).map(|n| (n, "table")),
211                "create_view" => sql_def_name(node, src).map(|n| (n, "view")),
212                "create_function" => sql_def_name(node, src).map(|n| (n, "function")),
213                "create_procedure" => sql_def_name(node, src).map(|n| (n, "procedure")),
214                _ => None,
215            },
216        }
217    }
218
219    /// Containers that qualify children without being symbols themselves.
220    pub fn scope_only(&self, node: Node, src: &str) -> Option<String> {
221        match self {
222            Lang::Rust => match node.kind() {
223                // impl Foo { .. } / impl Trait for Foo { .. } → scope "Foo"
224                "impl_item" => {
225                    let ty = node.child_by_field_name("type")?;
226                    Some(base_type_name(ty, src))
227                }
228                "mod_item" => Some(src[node.child_by_field_name("name")?.byte_range()].to_string()),
229                _ => None,
230            },
231            _ => None,
232        }
233    }
234
235    /// Field holding the body (cut point for signatures), per node kind.
236    pub fn body_field(&self) -> Option<&'static str> {
237        // All supported definition kinds use "body" except TS declarators,
238        // which signature_text handles via the generic fallback.
239        Some("body")
240    }
241
242    /// If this node is a call, return the bare callee name.
243    pub fn call(&self, node: Node, src: &str) -> Option<String> {
244        let text = |n: Node| src[n.byte_range()].to_string();
245        match self {
246            Lang::Rust => {
247                if node.kind() != "call_expression" {
248                    return None;
249                }
250                let f = node.child_by_field_name("function")?;
251                match f.kind() {
252                    "identifier" => Some(text(f)),
253                    "field_expression" => f.child_by_field_name("field").map(text),
254                    "scoped_identifier" => f.child_by_field_name("name").map(text),
255                    "generic_function" => {
256                        let inner = f.child_by_field_name("function")?;
257                        match inner.kind() {
258                            "identifier" => Some(text(inner)),
259                            "scoped_identifier" => inner.child_by_field_name("name").map(text),
260                            _ => None,
261                        }
262                    }
263                    _ => None,
264                }
265            }
266            Lang::TypeScript | Lang::Tsx => {
267                if node.kind() != "call_expression" {
268                    return None;
269                }
270                let f = node.child_by_field_name("function")?;
271                match f.kind() {
272                    "identifier" => Some(text(f)),
273                    "member_expression" => f.child_by_field_name("property").map(text),
274                    _ => None,
275                }
276            }
277            Lang::Python => {
278                if node.kind() != "call" {
279                    return None;
280                }
281                let f = node.child_by_field_name("function")?;
282                match f.kind() {
283                    "identifier" => Some(text(f)),
284                    "attribute" => f.child_by_field_name("attribute").map(text),
285                    _ => None,
286                }
287            }
288            Lang::Go => {
289                if node.kind() != "call_expression" {
290                    return None;
291                }
292                let f = node.child_by_field_name("function")?;
293                match f.kind() {
294                    "identifier" => Some(text(f)),
295                    "selector_expression" => f.child_by_field_name("field").map(text),
296                    _ => None,
297                }
298            }
299            Lang::Java => {
300                if node.kind() != "method_invocation" {
301                    return None;
302                }
303                node.child_by_field_name("name").map(text)
304            }
305            Lang::Ruby => {
306                // `foo(...)`, `obj.foo(...)`, `obj.foo` — the method name.
307                if node.kind() != "call" {
308                    return None;
309                }
310                node.child_by_field_name("method").map(text)
311            }
312            Lang::C => {
313                if node.kind() != "call_expression" {
314                    return None;
315                }
316                let f = node.child_by_field_name("function")?;
317                match f.kind() {
318                    "identifier" => Some(text(f)),
319                    _ => None,
320                }
321            }
322            Lang::CSharp => {
323                if node.kind() != "invocation_expression" {
324                    return None;
325                }
326                let f = node.child_by_field_name("function")?;
327                match f.kind() {
328                    "identifier" => Some(text(f)),
329                    // obj.Method() / Type.Method() — the rightmost name.
330                    "member_access_expression" => f.child_by_field_name("name").map(text),
331                    _ => None,
332                }
333            }
334            Lang::Sql => {
335                // SQL has no calls; reuse the call edge for table dependencies.
336                // A table reference is an `object_reference` sitting in a query
337                // relation (FROM/JOIN), a DELETE/UPDATE `from` target, or a
338                // column's `REFERENCES` (foreign key) clause. The enclosing
339                // CREATE … is the caller, so the edge is view/proc/table → table.
340                if node.kind() != "object_reference" {
341                    return None;
342                }
343                match node.parent()?.kind() {
344                    "relation" | "from" | "column_definition" => {
345                        Some(text(node.child_by_field_name("name").unwrap_or(node)))
346                    }
347                    _ => None,
348                }
349            }
350        }
351    }
352
353    /// Collect imports declared by this node.
354    pub fn imports(&self, node: Node, src: &str, out: &mut Vec<ImportRef>) {
355        let text = |n: Node| src[n.byte_range()].to_string();
356        match self {
357            Lang::Rust => {
358                if node.kind() == "use_declaration" {
359                    if let Some(arg) = node.child_by_field_name("argument") {
360                        rust_use_tree(arg, src, "", out);
361                    }
362                }
363            }
364            Lang::TypeScript | Lang::Tsx => {
365                if node.kind() != "import_statement" {
366                    return;
367                }
368                let Some(source) = node
369                    .child_by_field_name("source")
370                    .map(|s| text(s).trim_matches(['"', '\'']).to_string())
371                else {
372                    return;
373                };
374                let mut cursor = node.walk();
375                for child in node.children(&mut cursor) {
376                    if child.kind() != "import_clause" {
377                        continue;
378                    }
379                    let mut c2 = child.walk();
380                    for part in child.children(&mut c2) {
381                        match part.kind() {
382                            "identifier" => out.push(ImportRef {
383                                local: text(part),
384                                source: source.clone(),
385                            }),
386                            "named_imports" => {
387                                let mut c3 = part.walk();
388                                for spec in part.children(&mut c3) {
389                                    if spec.kind() != "import_specifier" {
390                                        continue;
391                                    }
392                                    let local = spec
393                                        .child_by_field_name("alias")
394                                        .or_else(|| spec.child_by_field_name("name"))
395                                        .map(text);
396                                    if let Some(local) = local {
397                                        out.push(ImportRef {
398                                            local,
399                                            source: source.clone(),
400                                        });
401                                    }
402                                }
403                            }
404                            "namespace_import" => {
405                                // import * as ns from "x"
406                                let mut c3 = part.walk();
407                                for id in part.children(&mut c3) {
408                                    if id.kind() == "identifier" {
409                                        out.push(ImportRef {
410                                            local: text(id),
411                                            source: source.clone(),
412                                        });
413                                    }
414                                }
415                            }
416                            _ => {}
417                        }
418                    }
419                }
420            }
421            Lang::Python => match node.kind() {
422                "import_statement" => {
423                    let mut cursor = node.walk();
424                    for child in node.children(&mut cursor) {
425                        match child.kind() {
426                            "dotted_name" => out.push(ImportRef {
427                                local: text(child)
428                                    .rsplit('.')
429                                    .next()
430                                    .unwrap_or_default()
431                                    .to_string(),
432                                source: text(child),
433                            }),
434                            "aliased_import" => {
435                                let name = child.child_by_field_name("name").map(text);
436                                let alias = child.child_by_field_name("alias").map(text);
437                                if let (Some(name), Some(alias)) = (name, alias) {
438                                    out.push(ImportRef {
439                                        local: alias,
440                                        source: name,
441                                    });
442                                }
443                            }
444                            _ => {}
445                        }
446                    }
447                }
448                "import_from_statement" => {
449                    let Some(module) = node.child_by_field_name("module_name").map(text) else {
450                        return;
451                    };
452                    let mut cursor = node.walk();
453                    let mut past_import = false;
454                    for child in node.children(&mut cursor) {
455                        if child.kind() == "import" {
456                            past_import = true;
457                            continue;
458                        }
459                        if !past_import {
460                            continue;
461                        }
462                        match child.kind() {
463                            "dotted_name" => out.push(ImportRef {
464                                local: text(child),
465                                source: module.clone(),
466                            }),
467                            "aliased_import" => {
468                                if let Some(alias) = child.child_by_field_name("alias").map(text) {
469                                    out.push(ImportRef {
470                                        local: alias,
471                                        source: module.clone(),
472                                    });
473                                }
474                            }
475                            _ => {}
476                        }
477                    }
478                }
479                _ => {}
480            },
481            Lang::Go => {
482                if node.kind() != "import_spec" {
483                    return;
484                }
485                let Some(path) = node
486                    .child_by_field_name("path")
487                    .map(|p| text(p).trim_matches('"').to_string())
488                else {
489                    return;
490                };
491                let local = node
492                    .child_by_field_name("name")
493                    .map(text)
494                    .unwrap_or_else(|| path.rsplit('/').next().unwrap_or(&path).to_string());
495                out.push(ImportRef {
496                    local,
497                    source: path,
498                });
499            }
500            Lang::Java => {
501                // import a.b.C;  /  import static a.b.C.m;  → bind the last segment.
502                if node.kind() != "import_declaration" {
503                    return;
504                }
505                let mut cursor = node.walk();
506                let Some(scoped) = node
507                    .children(&mut cursor)
508                    .find(|c| c.kind() == "scoped_identifier")
509                else {
510                    return;
511                };
512                let source = text(scoped);
513                let local = source.rsplit('.').next().unwrap_or(&source).to_string();
514                out.push(ImportRef { local, source });
515            }
516            Lang::C => {
517                // #include "foo.h" / <foo.h> → a file→file edge by path.
518                if node.kind() != "preproc_include" {
519                    return;
520                }
521                let Some(path_node) = node.child_by_field_name("path") else {
522                    return;
523                };
524                let source = text(path_node).trim_matches(['"', '<', '>']).to_string();
525                let local = source
526                    .rsplit('/')
527                    .next()
528                    .unwrap_or(&source)
529                    .trim_end_matches(".h")
530                    .to_string();
531                out.push(ImportRef { local, source });
532            }
533            Lang::CSharp => {
534                // `using A.B;` / `using static A.B.C;` / `using X = A.B;` —
535                // bind the last namespace segment, or the alias when present.
536                if node.kind() != "using_directive" {
537                    return;
538                }
539                let mut cursor = node.walk();
540                let names: Vec<String> = node
541                    .children(&mut cursor)
542                    .filter(|c| {
543                        matches!(
544                            c.kind(),
545                            "identifier" | "qualified_name" | "alias_qualified_name"
546                        )
547                    })
548                    .map(text)
549                    .collect();
550                match names.as_slice() {
551                    // `using Alias = Some.Namespace;`
552                    [alias, source, ..] => out.push(ImportRef {
553                        local: alias.clone(),
554                        source: source.clone(),
555                    }),
556                    // `using Some.Namespace;` — local is the last segment.
557                    [source] => out.push(ImportRef {
558                        local: source.rsplit('.').next().unwrap_or(source).to_string(),
559                        source: source.clone(),
560                    }),
561                    [] => {}
562                }
563            }
564            // Ruby's `require` is a method call, not an import node; calls
565            // still resolve same-file (tier 1) and globally by name (tier 3).
566            Lang::Ruby => {}
567            // SQL has no import construct.
568            Lang::Sql => {}
569        }
570    }
571
572    /// Doc comment attached to a definition node.
573    pub fn doc_comment(&self, node: Node, src: &str) -> Option<String> {
574        match self {
575            Lang::Python => {
576                // Docstring: first statement of the body is a string literal.
577                let body = node.child_by_field_name("body")?;
578                let first = body.named_child(0)?;
579                if first.kind() != "expression_statement" {
580                    return None;
581                }
582                let s = first.named_child(0)?;
583                if s.kind() != "string" {
584                    return None;
585                }
586                let raw = &src[s.byte_range()];
587                let cleaned = raw
588                    .trim_start_matches(['r', 'b', 'f', 'u', 'R', 'B', 'F', 'U'])
589                    .trim_matches(['"', '\''])
590                    .trim();
591                Some(cleaned.lines().next().unwrap_or("").trim().to_string())
592                    .filter(|s| !s.is_empty())
593            }
594            Lang::Rust
595            | Lang::Go
596            | Lang::TypeScript
597            | Lang::Tsx
598            | Lang::Java
599            | Lang::C
600            | Lang::CSharp
601            | Lang::Ruby
602            | Lang::Sql => {
603                // Contiguous comment siblings directly above the node
604                // (a blank line breaks the chain; `//!` belongs to the
605                // module, not this item).
606                // SQL wraps each `CREATE …` in a `statement`, so the comment
607                // is a sibling of that wrapper — climb to it first.
608                let mut anchor = node;
609                if matches!(self, Lang::Sql) {
610                    while let Some(p) = anchor.parent() {
611                        if p.kind() == "statement" {
612                            anchor = p;
613                        } else {
614                            break;
615                        }
616                    }
617                }
618                let mut lines: Vec<String> = Vec::new();
619                let mut expect_row = anchor.start_position().row;
620                let mut prev = anchor.prev_sibling();
621                while let Some(p) = prev {
622                    if !p.kind().contains("comment")
623                        || expect_row.saturating_sub(p.end_position().row) > 1
624                        || src[p.byte_range()].starts_with("//!")
625                    {
626                        break;
627                    }
628                    lines.push(src[p.byte_range()].to_string());
629                    expect_row = p.start_position().row;
630                    prev = p.prev_sibling();
631                }
632                if lines.is_empty() {
633                    return None;
634                }
635                lines.reverse();
636                let cleaned: Vec<String> = lines
637                    .iter()
638                    .flat_map(|c| c.lines())
639                    .map(|l| {
640                        l.trim()
641                            .trim_start_matches("///")
642                            .trim_start_matches("//!")
643                            .trim_start_matches("//")
644                            .trim_start_matches("--") // SQL line comments
645                            .trim_start_matches("/**")
646                            .trim_start_matches("/*")
647                            .trim_end_matches("*/")
648                            .trim_start_matches('*')
649                            .trim_start_matches('#') // Ruby line comments
650                            .trim()
651                            .to_string()
652                    })
653                    .filter(|l| !l.is_empty())
654                    .collect();
655                if cleaned.is_empty() {
656                    None
657                } else {
658                    Some(cleaned.join(" ").chars().take(300).collect())
659                }
660            }
661        }
662    }
663}
664
665/// SQL `CREATE TABLE`/`VIEW`/`FUNCTION`/`PROCEDURE` name: the first
666/// `object_reference` child; keep its bare `name` field (drops any schema
667/// qualifier so `dbo.users` and `users` resolve to the same bucket).
668fn sql_def_name(node: Node, src: &str) -> Option<String> {
669    let mut cursor = node.walk();
670    let obj = node
671        .children(&mut cursor)
672        .find(|c| c.kind() == "object_reference")?;
673    let name = obj.child_by_field_name("name").unwrap_or(obj);
674    Some(src[name.byte_range()].to_string())
675}
676
677/// `impl Foo`, `impl Foo<T>`, `impl Trait for Foo<T>` → "Foo".
678fn base_type_name(ty: Node, src: &str) -> String {
679    match ty.kind() {
680        "generic_type" => ty
681            .child_by_field_name("type")
682            .map(|t| src[t.byte_range()].to_string())
683            .unwrap_or_else(|| src[ty.byte_range()].to_string()),
684        _ => src[ty.byte_range()].to_string(),
685    }
686}
687
688/// Ruby class/module name: a bare `constant` or `A::B` scope_resolution —
689/// keep the last segment as the local name.
690fn const_name(node: Node, src: &str) -> String {
691    let full = src[node.byte_range()].to_string();
692    full.rsplit("::").next().unwrap_or(&full).to_string()
693}
694
695/// C function name: unwrap pointer/function declarators down to the
696/// identifier — `*foo(...)`, `foo(...)`, `(*foo)(...)` all yield "foo".
697fn c_declarator_name(node: Node, src: &str) -> Option<String> {
698    match node.kind() {
699        "identifier" => Some(src[node.byte_range()].to_string()),
700        "function_declarator" | "pointer_declarator" | "parenthesized_declarator" => {
701            c_declarator_name(node.child_by_field_name("declarator")?, src)
702        }
703        _ => {
704            // Fall back to the first identifier descendant.
705            let mut cursor = node.walk();
706            for child in node.children(&mut cursor) {
707                if let Some(name) = c_declarator_name(child, src) {
708                    return Some(name);
709                }
710            }
711            None
712        }
713    }
714}
715
716/// Go receiver `(s *Server)` → "Server".
717fn receiver_type(receiver: Node, src: &str) -> Option<String> {
718    let mut cursor = receiver.walk();
719    for child in receiver.children(&mut cursor) {
720        if child.kind() == "parameter_declaration" {
721            let ty = child.child_by_field_name("type")?;
722            let base = match ty.kind() {
723                "pointer_type" => ty.named_child(0)?,
724                _ => ty,
725            };
726            return Some(src[base.byte_range()].to_string());
727        }
728    }
729    None
730}
731
732/// Rust use-tree walker: `use a::{b::C, d as E};` → C←a::b::C, E←a::d.
733fn rust_use_tree(node: Node, src: &str, prefix: &str, out: &mut Vec<ImportRef>) {
734    let text = |n: Node| src[n.byte_range()].to_string();
735    let join = |prefix: &str, seg: &str| {
736        if prefix.is_empty() {
737            seg.to_string()
738        } else {
739            format!("{prefix}::{seg}")
740        }
741    };
742    match node.kind() {
743        "identifier" | "crate" | "self" | "super" => {
744            let seg = text(node);
745            out.push(ImportRef {
746                local: seg.clone(),
747                source: join(prefix, &seg),
748            });
749        }
750        "scoped_identifier" => {
751            let full = join(prefix, &text(node));
752            let local = node
753                .child_by_field_name("name")
754                .map(text)
755                .unwrap_or_default();
756            if !local.is_empty() {
757                out.push(ImportRef {
758                    local,
759                    source: full,
760                });
761            }
762        }
763        "use_as_clause" => {
764            let alias = node.child_by_field_name("alias").map(text);
765            let path = node.child_by_field_name("path").map(text);
766            if let (Some(alias), Some(path)) = (alias, path) {
767                out.push(ImportRef {
768                    local: alias,
769                    source: join(prefix, &path),
770                });
771            }
772        }
773        "scoped_use_list" => {
774            let new_prefix = node
775                .child_by_field_name("path")
776                .map(|p| join(prefix, &text(p)))
777                .unwrap_or_else(|| prefix.to_string());
778            if let Some(list) = node.child_by_field_name("list") {
779                let mut cursor = list.walk();
780                for child in list.named_children(&mut cursor) {
781                    rust_use_tree(child, src, &new_prefix, out);
782                }
783            }
784        }
785        "use_list" => {
786            let mut cursor = node.walk();
787            for child in node.named_children(&mut cursor) {
788                rust_use_tree(child, src, prefix, out);
789            }
790        }
791        // use_wildcard and attributes: nothing useful to bind.
792        _ => {}
793    }
794}