Skip to main content

mimir_syntax/
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    Cpp,
21    Kotlin,
22    Swift,
23    Php,
24}
25
26impl Lang {
27    pub fn from_path(path: &str) -> Option<Lang> {
28        let ext = path.rsplit('.').next()?;
29        Some(match ext {
30            "rs" => Lang::Rust,
31            "ts" | "mts" | "cts" => Lang::TypeScript,
32            "tsx" | "jsx" | "js" | "mjs" | "cjs" => Lang::Tsx,
33            "py" | "pyi" => Lang::Python,
34            "go" => Lang::Go,
35            "java" => Lang::Java,
36            "rb" | "rake" => Lang::Ruby,
37            "c" => Lang::C,
38            // `.h` is deliberately Cpp, not C: the C++ grammar parses
39            // C-style headers acceptably, but the reverse isn't true (C
40            // can't parse `class`/`namespace`/templates at all). `.c` stays
41            // its own bucket since a `.c` file is never C++.
42            "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "h" => Lang::Cpp,
43            "kt" | "kts" => Lang::Kotlin,
44            "swift" => Lang::Swift,
45            "php" => Lang::Php,
46            "cs" | "csx" => Lang::CSharp,
47            "sql" => Lang::Sql,
48            _ => return None,
49        })
50    }
51
52    pub fn name(&self) -> &'static str {
53        match self {
54            Lang::Rust => "rust",
55            Lang::TypeScript | Lang::Tsx => "typescript",
56            Lang::Python => "python",
57            Lang::Go => "go",
58            Lang::Java => "java",
59            Lang::Ruby => "ruby",
60            Lang::C => "c",
61            Lang::CSharp => "csharp",
62            Lang::Sql => "sql",
63            Lang::Cpp => "cpp",
64            Lang::Kotlin => "kotlin",
65            Lang::Swift => "swift",
66            Lang::Php => "php",
67        }
68    }
69
70    pub fn language(&self) -> tree_sitter::Language {
71        match self {
72            Lang::Rust => tree_sitter_rust::LANGUAGE.into(),
73            Lang::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
74            Lang::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
75            Lang::Python => tree_sitter_python::LANGUAGE.into(),
76            Lang::Go => tree_sitter_go::LANGUAGE.into(),
77            Lang::Java => tree_sitter_java::LANGUAGE.into(),
78            Lang::Ruby => tree_sitter_ruby::LANGUAGE.into(),
79            Lang::C => tree_sitter_c::LANGUAGE.into(),
80            Lang::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
81            Lang::Sql => tree_sitter_sequel_tsql::LANGUAGE.into(),
82            Lang::Cpp => tree_sitter_cpp::LANGUAGE.into(),
83            Lang::Kotlin => tree_sitter_kotlin_ng::LANGUAGE.into(),
84            Lang::Swift => tree_sitter_swift::LANGUAGE.into(),
85            Lang::Php => tree_sitter_php::LANGUAGE_PHP.into(),
86        }
87    }
88
89    pub fn separator(&self) -> String {
90        "::".into()
91    }
92
93    /// Is this node a symbol definition? Returns (name, kind). The name may
94    /// be pre-qualified with `::` (Go methods carry their receiver).
95    pub fn definition(&self, node: Node, src: &str) -> Option<(String, &'static str)> {
96        let text = |n: Node| src[n.byte_range()].to_string();
97        match self {
98            Lang::Rust => match node.kind() {
99                "function_item" => Some((text(node.child_by_field_name("name")?), "function")),
100                "struct_item" => Some((text(node.child_by_field_name("name")?), "struct")),
101                "enum_item" => Some((text(node.child_by_field_name("name")?), "enum")),
102                "trait_item" => Some((text(node.child_by_field_name("name")?), "trait")),
103                "union_item" => Some((text(node.child_by_field_name("name")?), "struct")),
104                _ => None,
105            },
106            Lang::TypeScript | Lang::Tsx => match node.kind() {
107                "function_declaration" | "generator_function_declaration" => {
108                    Some((text(node.child_by_field_name("name")?), "function"))
109                }
110                "class_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
111                "method_definition" => {
112                    let name = text(node.child_by_field_name("name")?);
113                    if name == "constructor" {
114                        return None;
115                    }
116                    Some((name, "method"))
117                }
118                "interface_declaration" => {
119                    Some((text(node.child_by_field_name("name")?), "interface"))
120                }
121                "enum_declaration" => Some((text(node.child_by_field_name("name")?), "enum")),
122                "type_alias_declaration" => Some((text(node.child_by_field_name("name")?), "type")),
123                // const f = (..) => ..  /  const f = function(..) {..}
124                "variable_declarator" => {
125                    let value = node.child_by_field_name("value")?;
126                    if matches!(value.kind(), "arrow_function" | "function_expression") {
127                        let name = node.child_by_field_name("name")?;
128                        if name.kind() == "identifier" {
129                            return Some((text(name), "function"));
130                        }
131                    }
132                    None
133                }
134                _ => None,
135            },
136            Lang::Python => match node.kind() {
137                "function_definition" => {
138                    Some((text(node.child_by_field_name("name")?), "function"))
139                }
140                "class_definition" => Some((text(node.child_by_field_name("name")?), "class")),
141                _ => None,
142            },
143            Lang::Go => match node.kind() {
144                "function_declaration" => {
145                    Some((text(node.child_by_field_name("name")?), "function"))
146                }
147                "method_declaration" => {
148                    let name = text(node.child_by_field_name("name")?);
149                    let recv = node
150                        .child_by_field_name("receiver")
151                        .and_then(|r| receiver_type(r, src));
152                    Some((
153                        match recv {
154                            Some(t) => format!("{t}::{name}"),
155                            None => name,
156                        },
157                        "method",
158                    ))
159                }
160                "type_spec" => {
161                    let name = text(node.child_by_field_name("name")?);
162                    let kind = match node.child_by_field_name("type").map(|t| t.kind()) {
163                        Some("struct_type") => "struct",
164                        Some("interface_type") => "interface",
165                        _ => "type",
166                    };
167                    Some((name, kind))
168                }
169                _ => None,
170            },
171            Lang::Java => match node.kind() {
172                "class_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
173                "interface_declaration" => {
174                    Some((text(node.child_by_field_name("name")?), "interface"))
175                }
176                "enum_declaration" => Some((text(node.child_by_field_name("name")?), "enum")),
177                "record_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
178                "method_declaration" => Some((text(node.child_by_field_name("name")?), "method")),
179                "constructor_declaration" => {
180                    Some((text(node.child_by_field_name("name")?), "method"))
181                }
182                _ => None,
183            },
184            Lang::Ruby => match node.kind() {
185                "class" => Some((const_name(node.child_by_field_name("name")?, src), "class")),
186                "module" => Some((const_name(node.child_by_field_name("name")?, src), "module")),
187                "method" => Some((text(node.child_by_field_name("name")?), "method")),
188                // def self.foo — a class method.
189                "singleton_method" => Some((text(node.child_by_field_name("name")?), "method")),
190                _ => None,
191            },
192            Lang::C => match node.kind() {
193                "function_definition" => Some((
194                    c_declarator_name(node.child_by_field_name("declarator")?, src)?,
195                    "function",
196                )),
197                "struct_specifier" => Some((text(node.child_by_field_name("name")?), "struct")),
198                "union_specifier" => Some((text(node.child_by_field_name("name")?), "struct")),
199                "enum_specifier" => Some((text(node.child_by_field_name("name")?), "enum")),
200                _ => None,
201            },
202            Lang::CSharp => match node.kind() {
203                // Both `namespace Foo { .. }` and file-scoped `namespace Foo;`
204                // — a scope that qualifies the types nested under it.
205                "namespace_declaration" | "file_scoped_namespace_declaration" => {
206                    Some((text(node.child_by_field_name("name")?), "namespace"))
207                }
208                "class_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
209                "interface_declaration" => {
210                    Some((text(node.child_by_field_name("name")?), "interface"))
211                }
212                "struct_declaration" => Some((text(node.child_by_field_name("name")?), "struct")),
213                "enum_declaration" => Some((text(node.child_by_field_name("name")?), "enum")),
214                // Records are reference types by default; treat as a class.
215                "record_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
216                "record_struct_declaration" => {
217                    Some((text(node.child_by_field_name("name")?), "struct"))
218                }
219                "method_declaration" => Some((text(node.child_by_field_name("name")?), "method")),
220                "property_declaration" => {
221                    Some((text(node.child_by_field_name("name")?), "property"))
222                }
223                // Constructor's name field is the enclosing type identifier.
224                "constructor_declaration" => {
225                    Some((text(node.child_by_field_name("name")?), "constructor"))
226                }
227                _ => None,
228            },
229            Lang::Sql => match node.kind() {
230                "create_table" => sql_def_name(node, src).map(|n| (n, "table")),
231                "create_view" => sql_def_name(node, src).map(|n| (n, "view")),
232                "create_function" => sql_def_name(node, src).map(|n| (n, "function")),
233                "create_procedure" => sql_def_name(node, src).map(|n| (n, "procedure")),
234                _ => None,
235            },
236            Lang::Cpp => match node.kind() {
237                // Bare declarator name, or (out-of-line) `Type::method` —
238                // the qualifier is kept so `Greeter::Format` defined outside
239                // its class body still nests under "Greeter" and reads as a
240                // method regardless of enclosing namespace scope, mirroring
241                // Go's receiver-type prefix for the same reason.
242                "function_definition" => {
243                    let name = c_declarator_name(node.child_by_field_name("declarator")?, src)?;
244                    let kind = if name.contains("::") {
245                        "method"
246                    } else {
247                        "function"
248                    };
249                    Some((name, kind))
250                }
251                "class_specifier" => Some((text(node.child_by_field_name("name")?), "class")),
252                "struct_specifier" => Some((text(node.child_by_field_name("name")?), "struct")),
253                "union_specifier" => Some((text(node.child_by_field_name("name")?), "struct")),
254                "enum_specifier" => Some((text(node.child_by_field_name("name")?), "enum")),
255                "namespace_definition" => {
256                    Some((text(node.child_by_field_name("name")?), "namespace"))
257                }
258                _ => None,
259            },
260            // kotlin-ng's declaration nodes carry a `name` field (only its
261            // calls/imports/navigation are fieldless — see call()/imports()
262            // below), so definitions read the same as any other adapter.
263            Lang::Kotlin => match node.kind() {
264                "class_declaration" => {
265                    let name = text(node.child_by_field_name("name")?);
266                    let mut cursor = node.walk();
267                    let kind = if node
268                        .children(&mut cursor)
269                        .any(|c| c.kind() == "enum_class_body")
270                    {
271                        "enum"
272                    } else {
273                        "class"
274                    };
275                    Some((name, kind))
276                }
277                "object_declaration" => Some((text(node.child_by_field_name("name")?), "object")),
278                "function_declaration" => {
279                    Some((text(node.child_by_field_name("name")?), "function"))
280                }
281                _ => None,
282            },
283            // Swift's `class_declaration` covers class/struct/actor/extension,
284            // disambiguated by its `declaration_kind` field (the literal
285            // keyword). `function_declaration`'s `name` field matches twice
286            // in this grammar (once for the identifier, once for an
287            // optionally-present return type reusing the same field id) —
288            // `child_by_field_name` returns the first match in document
289            // order, which is always the identifier since it precedes any
290            // `-> ReturnType`.
291            Lang::Swift => match node.kind() {
292                "class_declaration" => {
293                    let kind = match node
294                        .child_by_field_name("declaration_kind")
295                        .map(text)
296                        .as_deref()
297                    {
298                        Some("struct") => "struct",
299                        Some("enum") => "enum",
300                        _ => "class",
301                    };
302                    Some((text(node.child_by_field_name("name")?), kind))
303                }
304                "protocol_declaration" => {
305                    Some((text(node.child_by_field_name("name")?), "interface"))
306                }
307                "function_declaration" | "protocol_function_declaration" => {
308                    Some((text(node.child_by_field_name("name")?), "function"))
309                }
310                // `init`'s `name` field is the literal `init` keyword token.
311                "init_declaration" => Some(("init".to_string(), "constructor")),
312                _ => None,
313            },
314            Lang::Php => match node.kind() {
315                "function_definition" => {
316                    Some((text(node.child_by_field_name("name")?), "function"))
317                }
318                "method_declaration" => Some((text(node.child_by_field_name("name")?), "method")),
319                "class_declaration" => Some((text(node.child_by_field_name("name")?), "class")),
320                "interface_declaration" => {
321                    Some((text(node.child_by_field_name("name")?), "interface"))
322                }
323                "trait_declaration" => Some((text(node.child_by_field_name("name")?), "trait")),
324                "enum_declaration" => Some((text(node.child_by_field_name("name")?), "enum")),
325                "namespace_definition" => {
326                    Some((text(node.child_by_field_name("name")?), "namespace"))
327                }
328                _ => None,
329            },
330        }
331    }
332
333    /// Containers that qualify children without being symbols themselves.
334    pub fn scope_only(&self, node: Node, src: &str) -> Option<String> {
335        match self {
336            Lang::Rust => match node.kind() {
337                // impl Foo { .. } / impl Trait for Foo { .. } → scope "Foo"
338                "impl_item" => {
339                    let ty = node.child_by_field_name("type")?;
340                    Some(base_type_name(ty, src))
341                }
342                "mod_item" => Some(src[node.child_by_field_name("name")?.byte_range()].to_string()),
343                _ => None,
344            },
345            _ => None,
346        }
347    }
348
349    /// Field holding the body (cut point for signatures), per node kind.
350    pub fn body_field(&self) -> Option<&'static str> {
351        // All supported definition kinds use "body" except TS declarators,
352        // which signature_text handles via the generic fallback.
353        Some("body")
354    }
355
356    /// If this node is a call, return the bare callee name.
357    pub fn call(&self, node: Node, src: &str) -> Option<String> {
358        let text = |n: Node| src[n.byte_range()].to_string();
359        match self {
360            Lang::Rust => {
361                if node.kind() != "call_expression" {
362                    return None;
363                }
364                let f = node.child_by_field_name("function")?;
365                match f.kind() {
366                    "identifier" => Some(text(f)),
367                    "field_expression" => f.child_by_field_name("field").map(text),
368                    "scoped_identifier" => f.child_by_field_name("name").map(text),
369                    "generic_function" => {
370                        let inner = f.child_by_field_name("function")?;
371                        match inner.kind() {
372                            "identifier" => Some(text(inner)),
373                            "scoped_identifier" => inner.child_by_field_name("name").map(text),
374                            _ => None,
375                        }
376                    }
377                    _ => None,
378                }
379            }
380            Lang::TypeScript | Lang::Tsx => {
381                if node.kind() != "call_expression" {
382                    return None;
383                }
384                let f = node.child_by_field_name("function")?;
385                match f.kind() {
386                    "identifier" => Some(text(f)),
387                    "member_expression" => f.child_by_field_name("property").map(text),
388                    _ => None,
389                }
390            }
391            Lang::Python => {
392                if node.kind() != "call" {
393                    return None;
394                }
395                let f = node.child_by_field_name("function")?;
396                match f.kind() {
397                    "identifier" => Some(text(f)),
398                    "attribute" => f.child_by_field_name("attribute").map(text),
399                    _ => None,
400                }
401            }
402            Lang::Go => {
403                if node.kind() != "call_expression" {
404                    return None;
405                }
406                let f = node.child_by_field_name("function")?;
407                match f.kind() {
408                    "identifier" => Some(text(f)),
409                    "selector_expression" => f.child_by_field_name("field").map(text),
410                    _ => None,
411                }
412            }
413            Lang::Java => {
414                if node.kind() != "method_invocation" {
415                    return None;
416                }
417                node.child_by_field_name("name").map(text)
418            }
419            Lang::Ruby => {
420                // `foo(...)`, `obj.foo(...)`, `obj.foo` — the method name.
421                if node.kind() != "call" {
422                    return None;
423                }
424                node.child_by_field_name("method").map(text)
425            }
426            Lang::C => {
427                if node.kind() != "call_expression" {
428                    return None;
429                }
430                let f = node.child_by_field_name("function")?;
431                match f.kind() {
432                    "identifier" => Some(text(f)),
433                    _ => None,
434                }
435            }
436            Lang::CSharp => {
437                if node.kind() != "invocation_expression" {
438                    return None;
439                }
440                let f = node.child_by_field_name("function")?;
441                match f.kind() {
442                    "identifier" => Some(text(f)),
443                    // obj.Method() / Type.Method() — the rightmost name.
444                    "member_access_expression" => f.child_by_field_name("name").map(text),
445                    _ => None,
446                }
447            }
448            Lang::Sql => {
449                // SQL has no calls; reuse the call edge for table dependencies.
450                // A table reference is an `object_reference` sitting in a query
451                // relation (FROM/JOIN), a DELETE/UPDATE `from` target, or a
452                // column's `REFERENCES` (foreign key) clause. The enclosing
453                // CREATE … is the caller, so the edge is view/proc/table → table.
454                if node.kind() != "object_reference" {
455                    return None;
456                }
457                match node.parent()?.kind() {
458                    "relation" | "from" | "column_definition" => {
459                        Some(text(node.child_by_field_name("name").unwrap_or(node)))
460                    }
461                    _ => None,
462                }
463            }
464            Lang::Cpp => {
465                if node.kind() != "call_expression" {
466                    return None;
467                }
468                let f = node.child_by_field_name("function")?;
469                match f.kind() {
470                    "identifier" | "field_identifier" => Some(text(f)),
471                    // obj.method() / obj->method() — the field name.
472                    "field_expression" => f.child_by_field_name("field").map(text),
473                    // Foo::bar() — a namespace/static-qualified call.
474                    "qualified_identifier" => f.child_by_field_name("name").map(text),
475                    _ => None,
476                }
477            }
478            // kotlin-ng's call_expression and navigation_expression are
479            // fieldless — the callee is always the first named child
480            // (identifier for a bare call, navigation_expression for a
481            // `recv.method(...)` call, whose own last named child is the
482            // member name).
483            Lang::Kotlin => {
484                if node.kind() != "call_expression" {
485                    return None;
486                }
487                let callee = node.named_child(0)?;
488                match callee.kind() {
489                    "identifier" => Some(text(callee)),
490                    "navigation_expression" => {
491                        let n = u32::try_from(callee.named_child_count())
492                            .ok()?
493                            .checked_sub(1)?;
494                        let last = callee.named_child(n)?;
495                        (last.kind() == "identifier").then(|| text(last))
496                    }
497                    _ => None,
498                }
499            }
500            Lang::Swift => {
501                if node.kind() != "call_expression" {
502                    return None;
503                }
504                let callee = node.named_child(0)?;
505                match callee.kind() {
506                    "simple_identifier" => Some(text(callee)),
507                    // `recv.method(...)` — navigation_expression's `suffix`
508                    // field is a navigation_suffix, itself carrying the
509                    // member name under its own `suffix` field.
510                    "navigation_expression" => {
511                        let suffix = callee.child_by_field_name("suffix")?;
512                        suffix.child_by_field_name("suffix").map(text)
513                    }
514                    _ => None,
515                }
516            }
517            Lang::Php => match node.kind() {
518                "function_call_expression" => node.child_by_field_name("function").map(text),
519                // obj->method(...) — the method name.
520                "member_call_expression" => node.child_by_field_name("name").map(text),
521                // Type::method(...) — a static/scoped call.
522                "scoped_call_expression" => node.child_by_field_name("name").map(text),
523                _ => None,
524            },
525        }
526    }
527
528    /// Collect imports declared by this node.
529    pub fn imports(&self, node: Node, src: &str, out: &mut Vec<ImportRef>) {
530        let text = |n: Node| src[n.byte_range()].to_string();
531        match self {
532            Lang::Rust => {
533                if node.kind() == "use_declaration" {
534                    if let Some(arg) = node.child_by_field_name("argument") {
535                        rust_use_tree(arg, src, "", out);
536                    }
537                }
538            }
539            Lang::TypeScript | Lang::Tsx => {
540                if node.kind() != "import_statement" {
541                    return;
542                }
543                let Some(source) = node
544                    .child_by_field_name("source")
545                    .map(|s| text(s).trim_matches(['"', '\'']).to_string())
546                else {
547                    return;
548                };
549                let mut cursor = node.walk();
550                for child in node.children(&mut cursor) {
551                    if child.kind() != "import_clause" {
552                        continue;
553                    }
554                    let mut c2 = child.walk();
555                    for part in child.children(&mut c2) {
556                        match part.kind() {
557                            "identifier" => out.push(ImportRef {
558                                local: text(part),
559                                source: source.clone(),
560                            }),
561                            "named_imports" => {
562                                let mut c3 = part.walk();
563                                for spec in part.children(&mut c3) {
564                                    if spec.kind() != "import_specifier" {
565                                        continue;
566                                    }
567                                    let local = spec
568                                        .child_by_field_name("alias")
569                                        .or_else(|| spec.child_by_field_name("name"))
570                                        .map(text);
571                                    if let Some(local) = local {
572                                        out.push(ImportRef {
573                                            local,
574                                            source: source.clone(),
575                                        });
576                                    }
577                                }
578                            }
579                            "namespace_import" => {
580                                // import * as ns from "x"
581                                let mut c3 = part.walk();
582                                for id in part.children(&mut c3) {
583                                    if id.kind() == "identifier" {
584                                        out.push(ImportRef {
585                                            local: text(id),
586                                            source: source.clone(),
587                                        });
588                                    }
589                                }
590                            }
591                            _ => {}
592                        }
593                    }
594                }
595            }
596            Lang::Python => match node.kind() {
597                "import_statement" => {
598                    let mut cursor = node.walk();
599                    for child in node.children(&mut cursor) {
600                        match child.kind() {
601                            "dotted_name" => out.push(ImportRef {
602                                local: text(child)
603                                    .rsplit('.')
604                                    .next()
605                                    .unwrap_or_default()
606                                    .to_string(),
607                                source: text(child),
608                            }),
609                            "aliased_import" => {
610                                let name = child.child_by_field_name("name").map(text);
611                                let alias = child.child_by_field_name("alias").map(text);
612                                if let (Some(name), Some(alias)) = (name, alias) {
613                                    out.push(ImportRef {
614                                        local: alias,
615                                        source: name,
616                                    });
617                                }
618                            }
619                            _ => {}
620                        }
621                    }
622                }
623                "import_from_statement" => {
624                    let Some(module) = node.child_by_field_name("module_name").map(text) else {
625                        return;
626                    };
627                    let mut cursor = node.walk();
628                    let mut past_import = false;
629                    for child in node.children(&mut cursor) {
630                        if child.kind() == "import" {
631                            past_import = true;
632                            continue;
633                        }
634                        if !past_import {
635                            continue;
636                        }
637                        match child.kind() {
638                            "dotted_name" => out.push(ImportRef {
639                                local: text(child),
640                                source: module.clone(),
641                            }),
642                            "aliased_import" => {
643                                if let Some(alias) = child.child_by_field_name("alias").map(text) {
644                                    out.push(ImportRef {
645                                        local: alias,
646                                        source: module.clone(),
647                                    });
648                                }
649                            }
650                            _ => {}
651                        }
652                    }
653                }
654                _ => {}
655            },
656            Lang::Go => {
657                if node.kind() != "import_spec" {
658                    return;
659                }
660                let Some(path) = node
661                    .child_by_field_name("path")
662                    .map(|p| text(p).trim_matches('"').to_string())
663                else {
664                    return;
665                };
666                let local = node
667                    .child_by_field_name("name")
668                    .map(text)
669                    .unwrap_or_else(|| path.rsplit('/').next().unwrap_or(&path).to_string());
670                out.push(ImportRef {
671                    local,
672                    source: path,
673                });
674            }
675            Lang::Java => {
676                // import a.b.C;  /  import static a.b.C.m;  → bind the last segment.
677                if node.kind() != "import_declaration" {
678                    return;
679                }
680                let mut cursor = node.walk();
681                let Some(scoped) = node
682                    .children(&mut cursor)
683                    .find(|c| c.kind() == "scoped_identifier")
684                else {
685                    return;
686                };
687                let source = text(scoped);
688                let local = source.rsplit('.').next().unwrap_or(&source).to_string();
689                out.push(ImportRef { local, source });
690            }
691            // C's preprocessor grammar is reused verbatim by C++, so one
692            // arm covers both `#include` forms.
693            Lang::C | Lang::Cpp => {
694                // #include "foo.h" / <foo.h> → a file→file edge by path.
695                if node.kind() != "preproc_include" {
696                    return;
697                }
698                let Some(path_node) = node.child_by_field_name("path") else {
699                    return;
700                };
701                let source = text(path_node).trim_matches(['"', '<', '>']).to_string();
702                let local = source
703                    .rsplit('/')
704                    .next()
705                    .unwrap_or(&source)
706                    .trim_end_matches(".h")
707                    .to_string();
708                out.push(ImportRef { local, source });
709            }
710            Lang::CSharp => {
711                // `using A.B;` / `using static A.B.C;` / `using X = A.B;` —
712                // bind the last namespace segment, or the alias when present.
713                if node.kind() != "using_directive" {
714                    return;
715                }
716                let mut cursor = node.walk();
717                let names: Vec<String> = node
718                    .children(&mut cursor)
719                    .filter(|c| {
720                        matches!(
721                            c.kind(),
722                            "identifier" | "qualified_name" | "alias_qualified_name"
723                        )
724                    })
725                    .map(text)
726                    .collect();
727                match names.as_slice() {
728                    // `using Alias = Some.Namespace;`
729                    [alias, source, ..] => out.push(ImportRef {
730                        local: alias.clone(),
731                        source: source.clone(),
732                    }),
733                    // `using Some.Namespace;` — local is the last segment.
734                    [source] => out.push(ImportRef {
735                        local: source.rsplit('.').next().unwrap_or(source).to_string(),
736                        source: source.clone(),
737                    }),
738                    [] => {}
739                }
740            }
741            // Ruby's `require` is a method call, not an import node; calls
742            // still resolve same-file (tier 1) and globally by name (tier 3).
743            Lang::Ruby => {}
744            // SQL has no import construct.
745            Lang::Sql => {}
746            Lang::Kotlin => {
747                if node.kind() != "import" {
748                    return;
749                }
750                let mut cursor = node.walk();
751                let children: Vec<Node> = node.children(&mut cursor).collect();
752                if children.iter().any(|c| c.kind() == "*") {
753                    return; // Wildcard import — no single symbol to bind.
754                }
755                let Some(path) = children.iter().find(|c| c.kind() == "qualified_identifier")
756                else {
757                    return;
758                };
759                let source = text(*path);
760                let local = children
761                    .iter()
762                    .find(|c| c.kind() == "identifier")
763                    .map(|c| text(*c))
764                    .unwrap_or_else(|| source.rsplit('.').next().unwrap_or(&source).to_string());
765                out.push(ImportRef { local, source });
766            }
767            Lang::Swift => {
768                if node.kind() != "import_declaration" {
769                    return;
770                }
771                let mut cursor = node.walk();
772                let Some(path) = node
773                    .children(&mut cursor)
774                    .find(|c| c.kind() == "identifier")
775                else {
776                    return;
777                };
778                let source = text(path);
779                let local = source.rsplit('.').next().unwrap_or(&source).to_string();
780                out.push(ImportRef { local, source });
781            }
782            Lang::Php => match node.kind() {
783                "namespace_use_clause" => {
784                    let mut cursor = node.walk();
785                    let Some(path) = node
786                        .children(&mut cursor)
787                        .find(|c| matches!(c.kind(), "name" | "qualified_name"))
788                    else {
789                        return;
790                    };
791                    let source = text(path);
792                    let local = node
793                        .child_by_field_name("alias")
794                        .map(text)
795                        .unwrap_or_else(|| {
796                            source.rsplit('\\').next().unwrap_or(&source).to_string()
797                        });
798                    out.push(ImportRef { local, source });
799                }
800                "require_expression"
801                | "require_once_expression"
802                | "include_expression"
803                | "include_once_expression" => {
804                    let Some(expr) = node.named_child(0) else {
805                        return;
806                    };
807                    if expr.kind() != "string" {
808                        return;
809                    }
810                    let source = text(expr).trim_matches(['\'', '"']).to_string();
811                    let local = source
812                        .rsplit('/')
813                        .next()
814                        .unwrap_or(&source)
815                        .trim_end_matches(".php")
816                        .to_string();
817                    out.push(ImportRef { local, source });
818                }
819                _ => {}
820            },
821        }
822    }
823
824    /// Doc comment attached to a definition node.
825    pub fn doc_comment(&self, node: Node, src: &str) -> Option<String> {
826        match self {
827            Lang::Python => {
828                // Docstring: first statement of the body is a string literal.
829                let body = node.child_by_field_name("body")?;
830                let first = body.named_child(0)?;
831                if first.kind() != "expression_statement" {
832                    return None;
833                }
834                let s = first.named_child(0)?;
835                if s.kind() != "string" {
836                    return None;
837                }
838                let raw = &src[s.byte_range()];
839                let cleaned = raw
840                    .trim_start_matches(['r', 'b', 'f', 'u', 'R', 'B', 'F', 'U'])
841                    .trim_matches(['"', '\''])
842                    .trim();
843                Some(cleaned.lines().next().unwrap_or("").trim().to_string())
844                    .filter(|s| !s.is_empty())
845            }
846            Lang::Rust
847            | Lang::Go
848            | Lang::TypeScript
849            | Lang::Tsx
850            | Lang::Java
851            | Lang::C
852            | Lang::CSharp
853            | Lang::Ruby
854            | Lang::Sql
855            | Lang::Cpp
856            | Lang::Kotlin
857            | Lang::Swift
858            | Lang::Php => {
859                // Contiguous comment siblings directly above the node
860                // (a blank line breaks the chain; `//!` belongs to the
861                // module, not this item).
862                // SQL wraps each `CREATE …` in a `statement`, so the comment
863                // is a sibling of that wrapper — climb to it first.
864                let mut anchor = node;
865                if matches!(self, Lang::Sql) {
866                    while let Some(p) = anchor.parent() {
867                        if p.kind() == "statement" {
868                            anchor = p;
869                        } else {
870                            break;
871                        }
872                    }
873                }
874                let mut lines: Vec<String> = Vec::new();
875                let mut expect_row = anchor.start_position().row;
876                let mut prev = anchor.prev_sibling();
877                while let Some(p) = prev {
878                    if !p.kind().contains("comment")
879                        || expect_row.saturating_sub(p.end_position().row) > 1
880                        || src[p.byte_range()].starts_with("//!")
881                    {
882                        break;
883                    }
884                    lines.push(src[p.byte_range()].to_string());
885                    expect_row = p.start_position().row;
886                    prev = p.prev_sibling();
887                }
888                if lines.is_empty() {
889                    return None;
890                }
891                lines.reverse();
892                let cleaned: Vec<String> = lines
893                    .iter()
894                    .flat_map(|c| c.lines())
895                    .map(|l| {
896                        l.trim()
897                            .trim_start_matches("///")
898                            .trim_start_matches("//!")
899                            .trim_start_matches("//")
900                            .trim_start_matches("--") // SQL line comments
901                            .trim_start_matches("/**")
902                            .trim_start_matches("/*")
903                            .trim_end_matches("*/")
904                            .trim_start_matches('*')
905                            .trim_start_matches('#') // Ruby line comments
906                            .trim()
907                            .to_string()
908                    })
909                    .filter(|l| !l.is_empty())
910                    .collect();
911                if cleaned.is_empty() {
912                    None
913                } else {
914                    Some(cleaned.join(" ").chars().take(300).collect())
915                }
916            }
917        }
918    }
919}
920
921/// SQL `CREATE TABLE`/`VIEW`/`FUNCTION`/`PROCEDURE` name: the first
922/// `object_reference` child; keep its bare `name` field (drops any schema
923/// qualifier so `dbo.users` and `users` resolve to the same bucket).
924fn sql_def_name(node: Node, src: &str) -> Option<String> {
925    let mut cursor = node.walk();
926    let obj = node
927        .children(&mut cursor)
928        .find(|c| c.kind() == "object_reference")?;
929    let name = obj.child_by_field_name("name").unwrap_or(obj);
930    Some(src[name.byte_range()].to_string())
931}
932
933/// `impl Foo`, `impl Foo<T>`, `impl Trait for Foo<T>` → "Foo".
934fn base_type_name(ty: Node, src: &str) -> String {
935    match ty.kind() {
936        "generic_type" => ty
937            .child_by_field_name("type")
938            .map(|t| src[t.byte_range()].to_string())
939            .unwrap_or_else(|| src[ty.byte_range()].to_string()),
940        _ => src[ty.byte_range()].to_string(),
941    }
942}
943
944/// Ruby class/module name: a bare `constant` or `A::B` scope_resolution —
945/// keep the last segment as the local name.
946fn const_name(node: Node, src: &str) -> String {
947    let full = src[node.byte_range()].to_string();
948    full.rsplit("::").next().unwrap_or(&full).to_string()
949}
950
951/// C function name: unwrap pointer/function declarators down to the
952/// identifier — `*foo(...)`, `foo(...)`, `(*foo)(...)` all yield "foo".
953fn c_declarator_name(node: Node, src: &str) -> Option<String> {
954    match node.kind() {
955        // `field_identifier` is C++-only: an in-class inline method's
956        // declarator (e.g. `std::string greet() { … }` inside a
957        // `class_specifier` body) uses this kind instead of plain
958        // `identifier`, and it's a leaf with no children, so without this
959        // arm the fallback below would walk into nothing and drop the
960        // symbol entirely.
961        "identifier" | "field_identifier" => Some(src[node.byte_range()].to_string()),
962        "function_declarator" | "pointer_declarator" | "parenthesized_declarator" => {
963            c_declarator_name(node.child_by_field_name("declarator")?, src)
964        }
965        // C++ out-of-line member definition: `Type::method(...)`. Keep the
966        // qualifier so the symbol still nests under its class
967        // ("Type::method"), mirroring Go's receiver-type prefix for the
968        // same reason — without it, the fallback below would walk into
969        // `scope` first and return the class name instead of the method.
970        "qualified_identifier" => {
971            let name = c_declarator_name(node.child_by_field_name("name")?, src)?;
972            match node.child_by_field_name("scope") {
973                Some(scope) => Some(format!("{}::{name}", &src[scope.byte_range()])),
974                None => Some(name),
975            }
976        }
977        _ => {
978            // Fall back to the first identifier descendant.
979            let mut cursor = node.walk();
980            for child in node.children(&mut cursor) {
981                if let Some(name) = c_declarator_name(child, src) {
982                    return Some(name);
983                }
984            }
985            None
986        }
987    }
988}
989
990/// Go receiver `(s *Server)` → "Server".
991fn receiver_type(receiver: Node, src: &str) -> Option<String> {
992    let mut cursor = receiver.walk();
993    for child in receiver.children(&mut cursor) {
994        if child.kind() == "parameter_declaration" {
995            let ty = child.child_by_field_name("type")?;
996            let base = match ty.kind() {
997                "pointer_type" => ty.named_child(0)?,
998                _ => ty,
999            };
1000            return Some(src[base.byte_range()].to_string());
1001        }
1002    }
1003    None
1004}
1005
1006/// Rust use-tree walker: `use a::{b::C, d as E};` → C←a::b::C, E←a::d.
1007fn rust_use_tree(node: Node, src: &str, prefix: &str, out: &mut Vec<ImportRef>) {
1008    let text = |n: Node| src[n.byte_range()].to_string();
1009    let join = |prefix: &str, seg: &str| {
1010        if prefix.is_empty() {
1011            seg.to_string()
1012        } else {
1013            format!("{prefix}::{seg}")
1014        }
1015    };
1016    match node.kind() {
1017        "identifier" | "crate" | "self" | "super" => {
1018            let seg = text(node);
1019            out.push(ImportRef {
1020                local: seg.clone(),
1021                source: join(prefix, &seg),
1022            });
1023        }
1024        "scoped_identifier" => {
1025            let full = join(prefix, &text(node));
1026            let local = node
1027                .child_by_field_name("name")
1028                .map(text)
1029                .unwrap_or_default();
1030            if !local.is_empty() {
1031                out.push(ImportRef {
1032                    local,
1033                    source: full,
1034                });
1035            }
1036        }
1037        "use_as_clause" => {
1038            let alias = node.child_by_field_name("alias").map(text);
1039            let path = node.child_by_field_name("path").map(text);
1040            if let (Some(alias), Some(path)) = (alias, path) {
1041                out.push(ImportRef {
1042                    local: alias,
1043                    source: join(prefix, &path),
1044                });
1045            }
1046        }
1047        "scoped_use_list" => {
1048            let new_prefix = node
1049                .child_by_field_name("path")
1050                .map(|p| join(prefix, &text(p)))
1051                .unwrap_or_else(|| prefix.to_string());
1052            if let Some(list) = node.child_by_field_name("list") {
1053                let mut cursor = list.walk();
1054                for child in list.named_children(&mut cursor) {
1055                    rust_use_tree(child, src, &new_prefix, out);
1056                }
1057            }
1058        }
1059        "use_list" => {
1060            let mut cursor = node.walk();
1061            for child in node.named_children(&mut cursor) {
1062                rust_use_tree(child, src, prefix, out);
1063            }
1064        }
1065        // use_wildcard and attributes: nothing useful to bind.
1066        _ => {}
1067    }
1068}