Skip to main content

lean_ctx/core/deep_queries/
mod.rs

1//! Tree-sitter deep queries for extracting imports, call sites, and type definitions.
2//!
3//! Replaces regex-based extraction in `deps.rs` with precise AST parsing.
4//! Supported languages are gated by `get_language` (and kept in sync with
5//! `core::language_capabilities`): the TypeScript/JavaScript family, Python,
6//! Rust, Go, Java, C/C++, Ruby, C#, Kotlin, Swift, PHP, Bash, Dart, Scala,
7//! Elixir, Zig, and GDScript.
8
9mod calls;
10mod ext_methods;
11mod imports;
12mod type_defs;
13mod type_uses;
14mod types;
15
16pub use types::*;
17
18#[cfg(feature = "tree-sitter")]
19use tree_sitter::{Language, Node, Parser};
20
21pub fn analyze(content: &str, ext: &str) -> DeepAnalysis {
22    #[cfg(feature = "tree-sitter")]
23    {
24        if let Some(result) = analyze_with_tree_sitter(content, ext) {
25            return result;
26        }
27    }
28
29    let _ = (content, ext);
30    DeepAnalysis::empty()
31}
32
33#[cfg(feature = "tree-sitter")]
34fn analyze_with_tree_sitter(content: &str, ext: &str) -> Option<DeepAnalysis> {
35    let language = get_language(ext)?;
36
37    thread_local! {
38        static PARSER: std::cell::RefCell<Parser> = std::cell::RefCell::new(Parser::new());
39    }
40
41    let tree = PARSER.with(|p| {
42        let mut parser = p.borrow_mut();
43        let _ = parser.set_language(&language);
44        parser.parse(content.as_bytes(), None)
45    })?;
46    let root = tree.root_node();
47
48    let imports = imports::extract_imports(root, content, ext);
49    let calls = calls::extract_calls(root, content, ext);
50    let types = type_defs::extract_types(root, content, ext);
51    let exports = type_defs::extract_exports(root, content, ext);
52    let type_uses = type_uses::extract_type_uses(root, content, ext);
53    let ext_methods = ext_methods::extract_ext_methods(root, content, ext);
54
55    Some(DeepAnalysis {
56        imports,
57        calls,
58        types,
59        exports,
60        type_uses,
61        ext_methods,
62    })
63}
64
65/// Map a file extension to its tree-sitter [`Language`], or `None` when
66/// unsupported. `pub(crate)` so the post-edit syntax gate
67/// ([`crate::core::syntax_validate`]) reuses the exact same grammar set (#1008).
68#[cfg(feature = "tree-sitter")]
69pub(crate) fn get_language(ext: &str) -> Option<Language> {
70    match ext {
71        "rs" => Some(tree_sitter_rust::LANGUAGE.into()),
72        "ts" | "tsx" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
73        "js" | "jsx" => Some(tree_sitter_javascript::LANGUAGE.into()),
74        "py" => Some(tree_sitter_python::LANGUAGE.into()),
75        "go" => Some(tree_sitter_go::LANGUAGE.into()),
76        "java" => Some(tree_sitter_java::LANGUAGE.into()),
77        "c" | "h" => Some(tree_sitter_c::LANGUAGE.into()),
78        "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some(tree_sitter_cpp::LANGUAGE.into()),
79        "rb" => Some(tree_sitter_ruby::LANGUAGE.into()),
80        "cs" => Some(tree_sitter_c_sharp::LANGUAGE.into()),
81        "kt" | "kts" => Some(tree_sitter_kotlin_ng::LANGUAGE.into()),
82        "swift" => Some(tree_sitter_swift::LANGUAGE.into()),
83        "php" => Some(tree_sitter_php::LANGUAGE_PHP.into()),
84        "sh" | "bash" => Some(tree_sitter_bash::LANGUAGE.into()),
85        "dart" => Some(tree_sitter_dart::LANGUAGE.into()),
86        "scala" | "sc" => Some(tree_sitter_scala::LANGUAGE.into()),
87        "ex" | "exs" => Some(tree_sitter_elixir::LANGUAGE.into()),
88        "zig" => Some(tree_sitter_zig::LANGUAGE.into()),
89        "gd" => Some(tree_sitter_gdscript::LANGUAGE.into()),
90        "lua" => Some(tree_sitter_lua::LANGUAGE.into()),
91        "luau" => Some(tree_sitter_luau::LANGUAGE.into()),
92        _ => None,
93    }
94}
95
96// ---------------------------------------------------------------------------
97// Shared helpers (accessible by child modules via `super::`)
98// ---------------------------------------------------------------------------
99
100#[cfg(feature = "tree-sitter")]
101fn node_text<'a>(node: Node, src: &'a str) -> &'a str {
102    &src[node.byte_range()]
103}
104
105#[cfg(feature = "tree-sitter")]
106fn find_child_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
107    let mut cursor = node.walk();
108
109    node.children(&mut cursor).find(|c| c.kind() == kind)
110}
111
112#[cfg(feature = "tree-sitter")]
113fn find_descendant_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
114    // Iterative (heap-stack) search — see core::ast_walk (#378 SIGABRT).
115    crate::core::ast_walk::find_descendant_by_kind(node, kind)
116}
117
118// ---------------------------------------------------------------------------
119// Tests
120// ---------------------------------------------------------------------------
121
122#[cfg(test)]
123#[cfg(feature = "tree-sitter")]
124mod tests {
125    use super::*;
126
127    /// Indexing a deeply nested AST must not overflow the worker-thread stack
128    /// (the #378 SIGABRT) through the real `analyze` entry point. The depth is
129    /// well past what a recursive walk survives on a default stack, yet because
130    /// every walk is iterative now it returns normally. (The dedicated, much
131    /// deeper overflow guard lives in `core::ast_walk`.)
132    #[test]
133    fn deeply_nested_source_does_not_overflow() {
134        let depth = 12_000;
135        // Nested Rust call expressions drive the call walk through the real
136        // entry point at a depth far past what a recursive walk survives on a
137        // default stack; it returns normally because the walks are iterative.
138        let rs = format!(
139            "fn m() {{ let _ = {}0{}; }}",
140            "f(".repeat(depth),
141            ")".repeat(depth)
142        );
143        let analysis = analyze(&rs, "rs");
144        assert!(!analysis.calls.is_empty());
145    }
146
147    #[test]
148    fn ts_named_import() {
149        let src = r"import { useState, useEffect } from 'react';";
150        let analysis = analyze(src, "ts");
151        assert_eq!(analysis.imports.len(), 1);
152        assert_eq!(analysis.imports[0].source, "react");
153        assert_eq!(analysis.imports[0].names, vec!["useState", "useEffect"]);
154    }
155
156    #[test]
157    fn ts_default_import() {
158        let src = r"import React from 'react';";
159        let analysis = analyze(src, "ts");
160        assert_eq!(analysis.imports.len(), 1);
161        assert_eq!(analysis.imports[0].kind, ImportKind::Default);
162        assert_eq!(analysis.imports[0].names, vec!["React"]);
163    }
164
165    #[test]
166    fn ts_star_import() {
167        let src = r"import * as path from 'path';";
168        let analysis = analyze(src, "ts");
169        assert_eq!(analysis.imports.len(), 1);
170        assert_eq!(analysis.imports[0].kind, ImportKind::Star);
171    }
172
173    #[test]
174    fn ts_side_effect_import() {
175        let src = r"import './styles.css';";
176        let analysis = analyze(src, "ts");
177        assert_eq!(analysis.imports.len(), 1);
178        assert_eq!(analysis.imports[0].kind, ImportKind::SideEffect);
179        assert_eq!(analysis.imports[0].source, "./styles.css");
180    }
181
182    #[test]
183    fn ts_type_only_import() {
184        let src = r"import type { User } from './types';";
185        let analysis = analyze(src, "ts");
186        assert_eq!(analysis.imports.len(), 1);
187        assert!(analysis.imports[0].is_type_only);
188    }
189
190    #[test]
191    fn ts_reexport() {
192        let src = r"export { foo, bar } from './utils';";
193        let analysis = analyze(src, "ts");
194        assert_eq!(analysis.imports.len(), 1);
195        assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
196    }
197
198    #[test]
199    fn ts_call_sites() {
200        let src = r"
201const x = foo(1);
202const y = obj.method(2);
203";
204        let analysis = analyze(src, "ts");
205        assert!(analysis.calls.len() >= 2);
206        let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
207        assert!(fns.contains(&"foo"));
208        assert!(fns.contains(&"method"));
209    }
210
211    #[test]
212    fn ts_interface() {
213        let src = r"
214export interface User {
215    name: string;
216    age: number;
217}
218";
219        let analysis = analyze(src, "ts");
220        assert_eq!(analysis.types.len(), 1);
221        assert_eq!(analysis.types[0].name, "User");
222        assert_eq!(analysis.types[0].kind, TypeDefKind::Interface);
223    }
224
225    #[test]
226    fn ts_type_alias_union() {
227        let src = r"type Result = Success | Error;";
228        let analysis = analyze(src, "ts");
229        assert_eq!(analysis.types.len(), 1);
230        assert_eq!(analysis.types[0].kind, TypeDefKind::Union);
231    }
232
233    #[test]
234    fn rust_use_statements() {
235        let src = r"
236use crate::core::session;
237use anyhow::Result;
238use std::collections::HashMap;
239";
240        let analysis = analyze(src, "rs");
241        assert_eq!(analysis.imports.len(), 2);
242        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
243        assert!(sources.contains(&"crate::core::session"));
244        assert!(sources.contains(&"anyhow::Result"));
245    }
246
247    #[test]
248    fn rust_pub_use_reexport() {
249        let src = r"pub use crate::tools::ctx_read;";
250        let analysis = analyze(src, "rs");
251        assert_eq!(analysis.imports.len(), 1);
252        assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
253    }
254
255    #[test]
256    fn rust_struct_and_trait() {
257        let src = r"
258pub struct Config {
259    pub name: String,
260}
261
262pub trait Service {
263    fn run(&self);
264}
265";
266        let analysis = analyze(src, "rs");
267        assert_eq!(analysis.types.len(), 2);
268        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
269        assert!(names.contains(&"Config"));
270        assert!(names.contains(&"Service"));
271    }
272
273    #[test]
274    fn rust_call_sites() {
275        let src = r"
276fn main() {
277    let x = calculate(42);
278    let y = self.process();
279    Vec::new();
280}
281";
282        let analysis = analyze(src, "rs");
283        assert!(analysis.calls.len() >= 2);
284        let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
285        assert!(fns.contains(&"calculate"));
286    }
287
288    #[test]
289    fn python_imports() {
290        let src = r"
291import os
292from pathlib import Path
293from . import utils
294from ..models import User, Role
295";
296        let analysis = analyze(src, "py");
297        assert!(analysis.imports.len() >= 3);
298    }
299
300    #[test]
301    fn python_class_protocol() {
302        let src = r"
303class MyProtocol(Protocol):
304    def method(self) -> None: ...
305
306class User:
307    name: str
308";
309        let analysis = analyze(src, "py");
310        assert_eq!(analysis.types.len(), 2);
311        assert_eq!(analysis.types[0].kind, TypeDefKind::Protocol);
312        assert_eq!(analysis.types[1].kind, TypeDefKind::Class);
313    }
314
315    #[test]
316    fn python_call_sites() {
317        // Regression for GH #365: Python uses a bare `call` node, so class
318        // instantiation and method calls must both be extracted as call sites.
319        let src = r"
320from models.engine import Engine
321
322def boot():
323    engine = Engine(power=100)
324    engine.run()
325    return engine
326";
327        let analysis = analyze(src, "py");
328        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
329        assert!(
330            callees.contains(&"Engine"),
331            "class instantiation should be a call site, got {callees:?}"
332        );
333        assert!(
334            callees.contains(&"run"),
335            "method call must resolve to the method name (not the receiver), got {callees:?}"
336        );
337    }
338
339    #[test]
340    fn java_object_creation_is_a_call_site() {
341        let src = r"
342class App {
343    void boot() {
344        Engine e = new Engine(100);
345        e.run();
346    }
347}
348";
349        let analysis = analyze(src, "java");
350        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
351        assert!(
352            callees.contains(&"Engine"),
353            "`new Engine()` should be a call site, got {callees:?}"
354        );
355        assert!(
356            callees.contains(&"run"),
357            "method call expected, got {callees:?}"
358        );
359    }
360
361    #[test]
362    fn go_imports() {
363        let src = r#"
364package main
365
366import (
367    "fmt"
368    "net/http"
369    _ "github.com/lib/pq"
370)
371"#;
372        let analysis = analyze(src, "go");
373        assert!(analysis.imports.len() >= 3);
374        let side_effect = analysis.imports.iter().find(|i| i.source.contains("pq"));
375        assert!(side_effect.is_some());
376        assert_eq!(side_effect.unwrap().kind, ImportKind::SideEffect);
377    }
378
379    #[test]
380    fn go_struct_and_interface() {
381        let src = r"
382package main
383
384type Server struct {
385    Port int
386}
387
388type Handler interface {
389    Handle(r *Request)
390}
391";
392        let analysis = analyze(src, "go");
393        assert_eq!(analysis.types.len(), 2);
394        let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
395        assert!(kinds.contains(&&TypeDefKind::Struct));
396        assert!(kinds.contains(&&TypeDefKind::Interface));
397    }
398
399    #[test]
400    fn java_imports() {
401        let src = r"
402import java.util.List;
403import java.util.Map;
404import static org.junit.Assert.*;
405";
406        let analysis = analyze(src, "java");
407        assert!(analysis.imports.len() >= 2);
408    }
409
410    #[test]
411    fn java_class_and_interface() {
412        let src = r"
413public class UserService {
414    public void save(User u) {}
415}
416
417public interface Repository<T> {
418    T findById(int id);
419}
420
421public enum Status { ACTIVE, INACTIVE }
422
423public record Point(int x, int y) {}
424";
425        let analysis = analyze(src, "java");
426        assert!(analysis.types.len() >= 3);
427        let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
428        assert!(kinds.contains(&&TypeDefKind::Class));
429        assert!(kinds.contains(&&TypeDefKind::Interface));
430        assert!(kinds.contains(&&TypeDefKind::Enum));
431    }
432
433    #[test]
434    fn kotlin_imports_and_aliases() {
435        let src = r"
436package com.example.app
437
438import com.example.services.UserService
439import com.example.factories.WidgetFactory as Factory
440import com.example.shared.*
441";
442        let analysis = analyze(src, "kt");
443        assert_eq!(analysis.imports.len(), 3);
444        assert_eq!(
445            analysis.imports[0].source,
446            "com.example.services.UserService"
447        );
448        assert_eq!(analysis.imports[1].names, vec!["Factory"]);
449        assert_eq!(analysis.imports[2].kind, ImportKind::Star);
450    }
451
452    #[test]
453    fn kotlin_call_sites() {
454        let src = r"
455class UserService {
456    fun run() {
457        prepare()
458        repository.save(user)
459        Factory.create()
460    }
461}
462";
463        let analysis = analyze(src, "kt");
464        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
465        assert!(callees.contains(&"prepare"));
466        assert!(callees.contains(&"save"));
467        assert!(callees.contains(&"create"));
468    }
469
470    #[test]
471    fn kotlin_types_and_visibility() {
472        let src = r"
473sealed interface Handler
474data class User(val id: String)
475enum class Status { ACTIVE, INACTIVE }
476object Registry
477private typealias UserId = String
478";
479        let analysis = analyze(src, "kt");
480        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
481        assert!(names.contains(&"Handler"));
482        assert!(names.contains(&"User"));
483        assert!(names.contains(&"Status"));
484        assert!(names.contains(&"Registry"));
485        assert!(names.contains(&"UserId"));
486        let handler = analysis.types.iter().find(|t| t.name == "Handler").unwrap();
487        assert_eq!(handler.kind, TypeDefKind::Interface);
488        let alias = analysis.types.iter().find(|t| t.name == "UserId").unwrap();
489        assert!(!alias.is_exported);
490    }
491
492    #[test]
493    fn ts_generics_extracted() {
494        let src = r"interface Result<T, E> { ok: T; err: E; }";
495        let analysis = analyze(src, "ts");
496        assert_eq!(analysis.types.len(), 1);
497        assert!(!analysis.types[0].generics.is_empty());
498    }
499
500    #[test]
501    fn mixed_analysis_ts() {
502        let src = r"
503import { Request, Response } from 'express';
504import type { User } from './models';
505
506export interface Handler {
507    handle(req: Request): Response;
508}
509
510export class Router {
511    register(path: string, handler: Handler) {
512        this.handlers.set(path, handler);
513    }
514}
515
516const app = express();
517app.listen(3000);
518";
519        let analysis = analyze(src, "ts");
520        assert!(analysis.imports.len() >= 2, "Should find imports");
521        assert!(!analysis.types.is_empty(), "Should find types");
522        assert!(!analysis.calls.is_empty(), "Should find calls");
523    }
524
525    #[test]
526    fn empty_file() {
527        let analysis = analyze("", "ts");
528        assert!(analysis.imports.is_empty());
529        assert!(analysis.calls.is_empty());
530        assert!(analysis.types.is_empty());
531    }
532
533    #[test]
534    fn unsupported_extension() {
535        let analysis = analyze("some content", "txt");
536        assert!(analysis.imports.is_empty());
537    }
538
539    #[test]
540    fn c_include_import() {
541        let src = r#"
542#include "foo/bar.h"
543#include <stdio.h>
544"#;
545        let analysis = analyze(src, "c");
546        assert!(analysis.imports.iter().any(|i| i.source == "foo/bar.h"));
547    }
548
549    #[test]
550    fn bash_source_import() {
551        let src = r#"
552source "./scripts/env.sh"
553. ../common.sh
554"#;
555        let analysis = analyze(src, "sh");
556        assert!(
557            analysis
558                .imports
559                .iter()
560                .any(|i| i.source.contains("scripts/env.sh")),
561            "expected source import"
562        );
563    }
564
565    #[test]
566    fn zig_at_import() {
567        let src = r#"
568const m = @import("lib/math.zig");
569const std = @import("std");
570"#;
571        let analysis = analyze(src, "zig");
572        assert!(analysis.imports.iter().any(|i| i.source == "lib/math.zig"));
573    }
574
575    #[test]
576    fn gdscript_imports_extends_and_preload() {
577        let src = r#"
578extends "res://actors/base_actor.gd"
579
580const Bullet = preload("res://weapons/bullet.gd")
581var sfx = load("res://audio/shot.wav")
582"#;
583        let analysis = analyze(src, "gd");
584        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
585        assert!(
586            sources.contains(&"res://actors/base_actor.gd"),
587            "expected extends import, got {sources:?}"
588        );
589        assert!(
590            sources.contains(&"res://weapons/bullet.gd"),
591            "expected preload import, got {sources:?}"
592        );
593        assert!(
594            sources.contains(&"res://audio/shot.wav"),
595            "expected load import, got {sources:?}"
596        );
597    }
598
599    #[test]
600    fn gdscript_types_class_name_and_enum() {
601        let src = r"
602class_name Player
603
604enum State { IDLE, RUNNING }
605
606class Inventory:
607    var items = []
608";
609        let analysis = analyze(src, "gd");
610        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
611        assert!(
612            names.contains(&"Player"),
613            "expected class_name, got {names:?}"
614        );
615        assert!(names.contains(&"State"), "expected enum, got {names:?}");
616        assert!(
617            names.contains(&"Inventory"),
618            "expected inner class, got {names:?}"
619        );
620        let player = analysis.types.iter().find(|t| t.name == "Player").unwrap();
621        assert_eq!(player.kind, TypeDefKind::Class);
622        assert!(player.is_exported);
623        let state = analysis.types.iter().find(|t| t.name == "State").unwrap();
624        assert_eq!(state.kind, TypeDefKind::Enum);
625    }
626
627    #[test]
628    fn csharp_imports_all_using_forms() {
629        let src = r"
630using System;
631using System.Collections.Generic;
632global using MyApp.Core;
633using static System.Math;
634using Json = Newtonsoft.Json;
635namespace MyApp.Services {
636    using MyApp.Data.Repositories;
637}
638";
639        let analysis = analyze(src, "cs");
640        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
641        assert!(sources.contains(&"System"), "plain using, got {sources:?}");
642        assert!(
643            sources.contains(&"System.Collections.Generic"),
644            "dotted using, got {sources:?}"
645        );
646        assert!(
647            sources.contains(&"MyApp.Core"),
648            "global using must drop the `global` keyword, got {sources:?}"
649        );
650        assert!(
651            sources.contains(&"System.Math"),
652            "using static must drop the `static` keyword, got {sources:?}"
653        );
654        assert!(
655            sources.contains(&"Newtonsoft.Json"),
656            "alias using must keep the right-hand namespace, got {sources:?}"
657        );
658        assert!(
659            sources.contains(&"MyApp.Data.Repositories"),
660            "using nested inside a namespace block must be found, got {sources:?}"
661        );
662    }
663
664    /// GH #398: types consumed without any `using` (same-namespace visibility)
665    /// must surface as `type_uses` so the property graph can build TypeRef
666    /// edges. Covers fields, ctor parameters, return types, base list,
667    /// generic arguments, casts and `typeof`.
668    #[test]
669    fn csharp_type_uses_without_using_directive() {
670        let src = r"
671namespace App.Core;
672
673public class Motor : VehiclePart, IStartable
674{
675    private readonly Engine _engine;
676    public List<Sensor> Sensors { get; set; }
677
678    public Motor(Engine engine) { _engine = engine; }
679
680    public Gearbox BuildGearbox(Clutch clutch)
681    {
682        var t = typeof(Telemetry);
683        var d = (Dashboard)GetPart();
684        return null;
685    }
686}
687";
688        let analysis = analyze(src, "cs");
689        let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
690        for expected in [
691            "Engine",
692            "VehiclePart",
693            "IStartable",
694            "List",
695            "Sensor",
696            "Gearbox",
697            "Clutch",
698            "Telemetry",
699            "Dashboard",
700        ] {
701            assert!(names.contains(&expected), "missing {expected}: {names:?}");
702        }
703        // Predefined types carry no identifier node and must not appear.
704        assert!(!names.contains(&"var"), "var is not a type use: {names:?}");
705    }
706
707    /// GH #398 follow-up: types consumed only in *expression position* —
708    /// static calls/fields, enum values and attributes — carry no `type`
709    /// field, so the declaration-position walk missed them. They must still
710    /// surface as `type_uses` so the property graph can link consumer ->
711    /// definer. Instance receivers (lowercase locals) must stay out.
712    #[test]
713    fn csharp_type_uses_in_expression_position() {
714        let src = r#"
715namespace App.Core;
716
717[ApiController]
718[Route("api")]
719public class Garage
720{
721    public void Boot()
722    {
723        var engine = Engine.Create();
724        var fallback = Engine.Default;
725        var status = Status.Active;
726        var limit = Constants.Max;
727        engine.Start();
728    }
729}
730"#;
731        let analysis = analyze(src, "cs");
732        let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
733        for expected in ["Engine", "Status", "Constants", "ApiController", "Route"] {
734            assert!(names.contains(&expected), "missing {expected}: {names:?}");
735        }
736        // `[Foo]` resolves to the class `FooAttribute`; the canonical class
737        // name must be emitted too so the def index matches either form.
738        assert!(
739            names.contains(&"ApiControllerAttribute"),
740            "attribute suffix variant must be present: {names:?}"
741        );
742        // Instance receivers are values, not types, and must be skipped.
743        assert!(
744            !names.contains(&"engine"),
745            "instance receiver must not be a type use: {names:?}"
746        );
747    }
748
749    /// GH #398 (Java flavour): same-package types are visible without import;
750    /// `type_identifier` nodes cover fields, params, returns and extends.
751    #[test]
752    fn java_type_uses_without_import() {
753        let src = r"
754package app.core;
755
756public class Motor extends VehiclePart {
757    private Engine engine;
758    public Gearbox build(Clutch clutch) { return null; }
759}
760";
761        let analysis = analyze(src, "java");
762        let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
763        for expected in ["VehiclePart", "Engine", "Gearbox", "Clutch"] {
764            assert!(names.contains(&expected), "missing {expected}: {names:?}");
765        }
766    }
767
768    /// GH #398 bug class (Go): same-package types are referenced without any
769    /// import. `type_identifier` nodes cover struct fields, params, results and
770    /// slices; a cross-package `pkg.Type` (`qualified_type`) must be skipped —
771    /// that dependency rides the package import, not a same-package type edge.
772    #[test]
773    fn go_type_uses_same_package_skip_qualified() {
774        let src = r"
775package core
776
777type Motor struct {
778    engine Engine
779    parts  []Sensor
780}
781
782func (m *Motor) Build(c Clutch) Gearbox {
783    var g Gearbox
784    var w other.Widget
785    _ = w
786    return g
787}
788";
789        let analysis = analyze(src, "go");
790        let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
791        for expected in ["Engine", "Sensor", "Clutch", "Gearbox"] {
792            assert!(names.contains(&expected), "missing {expected}: {names:?}");
793        }
794        // A cross-package `other.Widget` is the import resolver's job, so its
795        // bare name must not leak in as a same-package type use.
796        assert!(
797            !names.contains(&"Widget"),
798            "qualified pkg.Type must be skipped: {names:?}"
799        );
800    }
801
802    /// GH #398 bug class (Kotlin): same-package types need no import. `user_type`
803    /// nodes (with a possibly-qualified `identifier`) cover properties, params,
804    /// returns, supertypes and generic arguments; only the last dotted segment
805    /// names the type. The declaring class name is an `identifier`, never a
806    /// `user_type`, so it is not collected as a self-use.
807    #[test]
808    fn kotlin_type_uses_same_package() {
809        let src = r"
810package app.core
811
812class Motor(private val engine: Engine) : VehiclePart(), Startable {
813    val sensors: List<Sensor> = emptyList()
814    fun build(clutch: Clutch): Gearbox = throw RuntimeException()
815    fun reset(d: com.app.ui.Dashboard) {}
816}
817";
818        let analysis = analyze(src, "kt");
819        let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
820        for expected in [
821            "Engine",
822            "VehiclePart",
823            "Startable",
824            "List",
825            "Sensor",
826            "Clutch",
827            "Gearbox",
828            "Dashboard",
829        ] {
830            assert!(names.contains(&expected), "missing {expected}: {names:?}");
831        }
832        // Only the last dotted segment is the type; package qualifiers drop out.
833        assert!(
834            !names.contains(&"com"),
835            "package qualifier must be dropped: {names:?}"
836        );
837    }
838
839    /// Kotlin types carry their file's package as `namespace` (parity with C#),
840    /// so directory-independent same-package resolution can confirm a match.
841    #[test]
842    fn kotlin_type_def_namespace_from_package_header() {
843        let analysis = analyze("package app.core\nclass Engine", "kt");
844        let ns = analysis
845            .types
846            .iter()
847            .find(|t| t.name == "Engine")
848            .and_then(|t| t.namespace.clone());
849        assert_eq!(ns.as_deref(), Some("app.core"));
850    }
851
852    /// Languages with mandatory explicit imports skip type-use extraction —
853    /// their dependencies are fully covered by the import resolver.
854    #[test]
855    fn type_uses_empty_for_import_based_languages() {
856        let rs = analyze("struct Foo { e: Engine }", "rs");
857        assert!(rs.type_uses.is_empty(), "rust: {:?}", rs.type_uses);
858        let ts = analyze("const e: Engine = make();", "ts");
859        assert!(ts.type_uses.is_empty(), "ts: {:?}", ts.type_uses);
860    }
861
862    #[test]
863    fn csharp_types_and_visibility() {
864        let src = r"
865namespace App
866{
867    public class UserService { }
868    internal class Helper { }
869    public interface IRepository { }
870    public struct Point { public int X; }
871    public enum Status { Active, Inactive }
872    public record Money(decimal Amount, string Currency);
873}
874";
875        let analysis = analyze(src, "cs");
876        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
877        assert!(names.contains(&"UserService"), "class, got {names:?}");
878        assert!(names.contains(&"Helper"), "internal class, got {names:?}");
879        assert!(names.contains(&"IRepository"), "interface, got {names:?}");
880        assert!(names.contains(&"Point"), "struct, got {names:?}");
881        assert!(names.contains(&"Status"), "enum, got {names:?}");
882        assert!(names.contains(&"Money"), "record, got {names:?}");
883
884        let kind_of = |n: &str| {
885            analysis
886                .types
887                .iter()
888                .find(|t| t.name == n)
889                .map(|t| t.kind.clone())
890        };
891        assert_eq!(kind_of("UserService"), Some(TypeDefKind::Class));
892        assert_eq!(kind_of("IRepository"), Some(TypeDefKind::Interface));
893        assert_eq!(kind_of("Point"), Some(TypeDefKind::Struct));
894        assert_eq!(kind_of("Status"), Some(TypeDefKind::Enum));
895        assert_eq!(kind_of("Money"), Some(TypeDefKind::Record));
896
897        let exported = |n: &str| {
898            analysis
899                .types
900                .iter()
901                .find(|t| t.name == n)
902                .is_some_and(|t| t.is_exported)
903        };
904        assert!(exported("UserService"), "public class is exported");
905        assert!(
906            !exported("Helper"),
907            "internal class must not be marked exported"
908        );
909        assert!(analysis.exports.contains(&"UserService".to_string()));
910    }
911
912    #[test]
913    fn csharp_call_sites() {
914        let src = r"
915namespace App
916{
917    public class Boot
918    {
919        public void Run()
920        {
921            Prepare();
922            _repository.Save(user);
923            var engine = new Engine(100);
924            Factory.Create<Widget>();
925        }
926    }
927}
928";
929        let analysis = analyze(src, "cs");
930        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
931        assert!(
932            callees.contains(&"Prepare"),
933            "direct invocation, got {callees:?}"
934        );
935        assert!(
936            callees.contains(&"Save"),
937            "member invocation must resolve to the method name, got {callees:?}"
938        );
939        assert!(
940            callees.contains(&"Engine"),
941            "`new Engine()` should reference the constructed type, got {callees:?}"
942        );
943        assert!(
944            callees.contains(&"Create"),
945            "generic member call must reduce to the identifier, got {callees:?}"
946        );
947
948        let save = analysis.calls.iter().find(|c| c.callee == "Save").unwrap();
949        assert_eq!(save.receiver.as_deref(), Some("_repository"));
950        assert!(save.is_method);
951    }
952
953    #[test]
954    fn gdscript_calls_method_and_instantiation() {
955        let src = r"
956func _ready():
957    var mgr = MapDataManager.new()
958    mgr.load_map_data()
959    update_state()
960";
961        let analysis = analyze(src, "gd");
962        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
963        // `MapDataManager.new()` registers a reference to the class itself.
964        assert!(
965            callees.contains(&"MapDataManager"),
966            "expected instantiation to reference class, got {callees:?}"
967        );
968        assert!(
969            callees.contains(&"load_map_data"),
970            "expected method call, got {callees:?}"
971        );
972        assert!(
973            callees.contains(&"update_state"),
974            "expected direct call, got {callees:?}"
975        );
976    }
977
978    #[test]
979    fn lua_require_imports() {
980        let src = r#"
981local mod = require("foo.bar")
982local helper = require "baz"
983local rel = require('a/b')
984"#;
985        let analysis = analyze(src, "lua");
986        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
987        assert!(
988            sources.contains(&"foo.bar"),
989            "dotted require, got {sources:?}"
990        );
991        assert!(
992            sources.contains(&"baz"),
993            "paren-less require, got {sources:?}"
994        );
995        assert!(sources.contains(&"a/b"), "slash require, got {sources:?}");
996    }
997
998    #[test]
999    fn lua_call_sites() {
1000        let src = r"
1001local function run()
1002    helper()
1003    obj.method(1)
1004    obj:method2(2)
1005end
1006";
1007        let analysis = analyze(src, "lua");
1008        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
1009        assert!(callees.contains(&"helper"), "direct call, got {callees:?}");
1010        assert!(callees.contains(&"method"), "dot call, got {callees:?}");
1011        assert!(callees.contains(&"method2"), "method call, got {callees:?}");
1012        let m = analysis
1013            .calls
1014            .iter()
1015            .find(|c| c.callee == "method2")
1016            .unwrap();
1017        assert_eq!(m.receiver.as_deref(), Some("obj"));
1018        assert!(m.is_method);
1019    }
1020
1021    #[test]
1022    fn luau_require_and_calls() {
1023        let src = r#"
1024local mod = require("shared/util")
1025local function go()
1026    mod.run()
1027end
1028"#;
1029        let analysis = analyze(src, "luau");
1030        assert!(
1031            analysis.imports.iter().any(|i| i.source == "shared/util"),
1032            "got {:?}",
1033            analysis.imports
1034        );
1035        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
1036        assert!(callees.contains(&"run"), "got {callees:?}");
1037    }
1038
1039    #[test]
1040    fn luau_type_aliases() {
1041        let src = r"
1042type Account = { balance: number }
1043export type Vec = { x: number, y: number }
1044";
1045        let analysis = analyze(src, "luau");
1046        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
1047        assert!(names.contains(&"Account"), "plain type, got {names:?}");
1048        assert!(names.contains(&"Vec"), "export type, got {names:?}");
1049        let vec = analysis.types.iter().find(|t| t.name == "Vec").unwrap();
1050        assert!(vec.is_exported, "`export type` must be exported");
1051        let acc = analysis.types.iter().find(|t| t.name == "Account").unwrap();
1052        assert!(!acc.is_exported, "plain `type` is module-local");
1053    }
1054
1055    #[test]
1056    fn lua_has_no_types() {
1057        // Lua (unlike Luau) has no type system — only functions/calls/imports.
1058        let analysis = analyze("type Account = {}", "lua");
1059        assert!(analysis.types.is_empty());
1060    }
1061
1062    /// GH #398 follow-up (#641): every C# type definition records the namespace
1063    /// it lives in — file-scoped (`namespace A.B;`), block-scoped
1064    /// (`namespace A { … }`) and nested block namespaces (joined outer→inner).
1065    /// Other languages leave `namespace` as `None`.
1066    #[test]
1067    fn csharp_type_namespace_extraction() {
1068        let file_scoped = analyze("namespace App.Core;\n\npublic class Engine { }\n", "cs");
1069        let engine = file_scoped
1070            .types
1071            .iter()
1072            .find(|t| t.name == "Engine")
1073            .expect("Engine type");
1074        assert_eq!(engine.namespace.as_deref(), Some("App.Core"));
1075
1076        let block = analyze(
1077            "namespace App.Data\n{\n    public class Repo { }\n}\n",
1078            "cs",
1079        );
1080        let repo = block
1081            .types
1082            .iter()
1083            .find(|t| t.name == "Repo")
1084            .expect("Repo type");
1085        assert_eq!(repo.namespace.as_deref(), Some("App.Data"));
1086
1087        let nested = analyze(
1088            "namespace App\n{\n    namespace Services\n    {\n        public class Bus { }\n    }\n}\n",
1089            "cs",
1090        );
1091        let bus = nested
1092            .types
1093            .iter()
1094            .find(|t| t.name == "Bus")
1095            .expect("Bus type");
1096        assert_eq!(bus.namespace.as_deref(), Some("App.Services"));
1097
1098        // Java carries no namespace on the type def (package handling differs).
1099        let java = analyze("package app;\npublic class Motor { }\n", "java");
1100        let motor = java
1101            .types
1102            .iter()
1103            .find(|t| t.name == "Motor")
1104            .expect("Motor type");
1105        assert_eq!(motor.namespace, None);
1106    }
1107
1108    /// GH #398 follow-up (#642): a C# method whose first parameter carries the
1109    /// `this` modifier is an extension method and is captured in `ext_methods`;
1110    /// ordinary methods are not.
1111    #[test]
1112    fn csharp_extension_methods_detected() {
1113        let src = r"
1114namespace App.Extensions;
1115
1116public static class Helpers
1117{
1118    public static int WordCount(this string s) => s.Length;
1119    public static string Shout(string s) => s.ToUpper();
1120}
1121";
1122        let analysis = analyze(src, "cs");
1123        let names: Vec<&str> = analysis
1124            .ext_methods
1125            .iter()
1126            .map(|m| m.name.as_str())
1127            .collect();
1128        assert!(
1129            names.contains(&"WordCount"),
1130            "`this`-parameter method must be an extension method, got {names:?}"
1131        );
1132        assert!(
1133            !names.contains(&"Shout"),
1134            "ordinary method must not be an extension method, got {names:?}"
1135        );
1136    }
1137
1138    /// Extension-method extraction is C#-specific; other languages stay empty.
1139    #[test]
1140    fn ext_methods_empty_for_non_csharp() {
1141        let cs = analyze(
1142            "namespace N;\npublic static class E { public static void F(this int x) {} }\n",
1143            "cs",
1144        );
1145        assert!(!cs.ext_methods.is_empty(), "C# baseline must detect one");
1146        let java = analyze("class A { void f(int x) {} }", "java");
1147        assert!(java.ext_methods.is_empty(), "java: {:?}", java.ext_methods);
1148        let ts = analyze("function f(x: number) {}", "ts");
1149        assert!(ts.ext_methods.is_empty(), "ts: {:?}", ts.ext_methods);
1150    }
1151}