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