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 imports;
11mod type_defs;
12mod type_uses;
13mod types;
14
15pub use types::*;
16
17#[cfg(feature = "tree-sitter")]
18use tree_sitter::{Language, Node, Parser};
19
20pub fn analyze(content: &str, ext: &str) -> DeepAnalysis {
21    #[cfg(feature = "tree-sitter")]
22    {
23        if let Some(result) = analyze_with_tree_sitter(content, ext) {
24            return result;
25        }
26    }
27
28    let _ = (content, ext);
29    DeepAnalysis::empty()
30}
31
32#[cfg(feature = "tree-sitter")]
33fn analyze_with_tree_sitter(content: &str, ext: &str) -> Option<DeepAnalysis> {
34    let language = get_language(ext)?;
35
36    thread_local! {
37        static PARSER: std::cell::RefCell<Parser> = std::cell::RefCell::new(Parser::new());
38    }
39
40    let tree = PARSER.with(|p| {
41        let mut parser = p.borrow_mut();
42        let _ = parser.set_language(&language);
43        parser.parse(content.as_bytes(), None)
44    })?;
45    let root = tree.root_node();
46
47    let imports = imports::extract_imports(root, content, ext);
48    let calls = calls::extract_calls(root, content, ext);
49    let types = type_defs::extract_types(root, content, ext);
50    let exports = type_defs::extract_exports(root, content, ext);
51    let type_uses = type_uses::extract_type_uses(root, content, ext);
52
53    Some(DeepAnalysis {
54        imports,
55        calls,
56        types,
57        exports,
58        type_uses,
59    })
60}
61
62#[cfg(feature = "tree-sitter")]
63fn get_language(ext: &str) -> Option<Language> {
64    match ext {
65        "rs" => Some(tree_sitter_rust::LANGUAGE.into()),
66        "ts" | "tsx" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
67        "js" | "jsx" => Some(tree_sitter_javascript::LANGUAGE.into()),
68        "py" => Some(tree_sitter_python::LANGUAGE.into()),
69        "go" => Some(tree_sitter_go::LANGUAGE.into()),
70        "java" => Some(tree_sitter_java::LANGUAGE.into()),
71        "c" | "h" => Some(tree_sitter_c::LANGUAGE.into()),
72        "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some(tree_sitter_cpp::LANGUAGE.into()),
73        "rb" => Some(tree_sitter_ruby::LANGUAGE.into()),
74        "cs" => Some(tree_sitter_c_sharp::LANGUAGE.into()),
75        "kt" | "kts" => Some(tree_sitter_kotlin_ng::LANGUAGE.into()),
76        "swift" => Some(tree_sitter_swift::LANGUAGE.into()),
77        "php" => Some(tree_sitter_php::LANGUAGE_PHP.into()),
78        "sh" | "bash" => Some(tree_sitter_bash::LANGUAGE.into()),
79        "dart" => Some(tree_sitter_dart::LANGUAGE.into()),
80        "scala" | "sc" => Some(tree_sitter_scala::LANGUAGE.into()),
81        "ex" | "exs" => Some(tree_sitter_elixir::LANGUAGE.into()),
82        "zig" => Some(tree_sitter_zig::LANGUAGE.into()),
83        "gd" => Some(tree_sitter_gdscript::LANGUAGE.into()),
84        _ => None,
85    }
86}
87
88// ---------------------------------------------------------------------------
89// Shared helpers (accessible by child modules via `super::`)
90// ---------------------------------------------------------------------------
91
92#[cfg(feature = "tree-sitter")]
93fn node_text<'a>(node: Node, src: &'a str) -> &'a str {
94    &src[node.byte_range()]
95}
96
97#[cfg(feature = "tree-sitter")]
98fn find_child_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
99    let mut cursor = node.walk();
100    let result = node.children(&mut cursor).find(|c| c.kind() == kind);
101    result
102}
103
104#[cfg(feature = "tree-sitter")]
105fn find_descendant_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
106    // Iterative (heap-stack) search — see core::ast_walk (#378 SIGABRT).
107    crate::core::ast_walk::find_descendant_by_kind(node, kind)
108}
109
110// ---------------------------------------------------------------------------
111// Tests
112// ---------------------------------------------------------------------------
113
114#[cfg(test)]
115#[cfg(feature = "tree-sitter")]
116mod tests {
117    use super::*;
118
119    /// Indexing a deeply nested AST must not overflow the worker-thread stack
120    /// (the #378 SIGABRT) through the real `analyze` entry point. The depth is
121    /// well past what a recursive walk survives on a default stack, yet because
122    /// every walk is iterative now it returns normally. (The dedicated, much
123    /// deeper overflow guard lives in `core::ast_walk`.)
124    #[test]
125    fn deeply_nested_source_does_not_overflow() {
126        let depth = 12_000;
127        // Nested Rust call expressions drive the call walk through the real
128        // entry point at a depth far past what a recursive walk survives on a
129        // default stack; it returns normally because the walks are iterative.
130        let rs = format!(
131            "fn m() {{ let _ = {}0{}; }}",
132            "f(".repeat(depth),
133            ")".repeat(depth)
134        );
135        let analysis = analyze(&rs, "rs");
136        assert!(!analysis.calls.is_empty());
137    }
138
139    #[test]
140    fn ts_named_import() {
141        let src = r"import { useState, useEffect } from 'react';";
142        let analysis = analyze(src, "ts");
143        assert_eq!(analysis.imports.len(), 1);
144        assert_eq!(analysis.imports[0].source, "react");
145        assert_eq!(analysis.imports[0].names, vec!["useState", "useEffect"]);
146    }
147
148    #[test]
149    fn ts_default_import() {
150        let src = r"import React from 'react';";
151        let analysis = analyze(src, "ts");
152        assert_eq!(analysis.imports.len(), 1);
153        assert_eq!(analysis.imports[0].kind, ImportKind::Default);
154        assert_eq!(analysis.imports[0].names, vec!["React"]);
155    }
156
157    #[test]
158    fn ts_star_import() {
159        let src = r"import * as path from 'path';";
160        let analysis = analyze(src, "ts");
161        assert_eq!(analysis.imports.len(), 1);
162        assert_eq!(analysis.imports[0].kind, ImportKind::Star);
163    }
164
165    #[test]
166    fn ts_side_effect_import() {
167        let src = r"import './styles.css';";
168        let analysis = analyze(src, "ts");
169        assert_eq!(analysis.imports.len(), 1);
170        assert_eq!(analysis.imports[0].kind, ImportKind::SideEffect);
171        assert_eq!(analysis.imports[0].source, "./styles.css");
172    }
173
174    #[test]
175    fn ts_type_only_import() {
176        let src = r"import type { User } from './types';";
177        let analysis = analyze(src, "ts");
178        assert_eq!(analysis.imports.len(), 1);
179        assert!(analysis.imports[0].is_type_only);
180    }
181
182    #[test]
183    fn ts_reexport() {
184        let src = r"export { foo, bar } from './utils';";
185        let analysis = analyze(src, "ts");
186        assert_eq!(analysis.imports.len(), 1);
187        assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
188    }
189
190    #[test]
191    fn ts_call_sites() {
192        let src = r"
193const x = foo(1);
194const y = obj.method(2);
195";
196        let analysis = analyze(src, "ts");
197        assert!(analysis.calls.len() >= 2);
198        let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
199        assert!(fns.contains(&"foo"));
200        assert!(fns.contains(&"method"));
201    }
202
203    #[test]
204    fn ts_interface() {
205        let src = r"
206export interface User {
207    name: string;
208    age: number;
209}
210";
211        let analysis = analyze(src, "ts");
212        assert_eq!(analysis.types.len(), 1);
213        assert_eq!(analysis.types[0].name, "User");
214        assert_eq!(analysis.types[0].kind, TypeDefKind::Interface);
215    }
216
217    #[test]
218    fn ts_type_alias_union() {
219        let src = r"type Result = Success | Error;";
220        let analysis = analyze(src, "ts");
221        assert_eq!(analysis.types.len(), 1);
222        assert_eq!(analysis.types[0].kind, TypeDefKind::Union);
223    }
224
225    #[test]
226    fn rust_use_statements() {
227        let src = r"
228use crate::core::session;
229use anyhow::Result;
230use std::collections::HashMap;
231";
232        let analysis = analyze(src, "rs");
233        assert_eq!(analysis.imports.len(), 2);
234        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
235        assert!(sources.contains(&"crate::core::session"));
236        assert!(sources.contains(&"anyhow::Result"));
237    }
238
239    #[test]
240    fn rust_pub_use_reexport() {
241        let src = r"pub use crate::tools::ctx_read;";
242        let analysis = analyze(src, "rs");
243        assert_eq!(analysis.imports.len(), 1);
244        assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
245    }
246
247    #[test]
248    fn rust_struct_and_trait() {
249        let src = r"
250pub struct Config {
251    pub name: String,
252}
253
254pub trait Service {
255    fn run(&self);
256}
257";
258        let analysis = analyze(src, "rs");
259        assert_eq!(analysis.types.len(), 2);
260        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
261        assert!(names.contains(&"Config"));
262        assert!(names.contains(&"Service"));
263    }
264
265    #[test]
266    fn rust_call_sites() {
267        let src = r"
268fn main() {
269    let x = calculate(42);
270    let y = self.process();
271    Vec::new();
272}
273";
274        let analysis = analyze(src, "rs");
275        assert!(analysis.calls.len() >= 2);
276        let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
277        assert!(fns.contains(&"calculate"));
278    }
279
280    #[test]
281    fn python_imports() {
282        let src = r"
283import os
284from pathlib import Path
285from . import utils
286from ..models import User, Role
287";
288        let analysis = analyze(src, "py");
289        assert!(analysis.imports.len() >= 3);
290    }
291
292    #[test]
293    fn python_class_protocol() {
294        let src = r"
295class MyProtocol(Protocol):
296    def method(self) -> None: ...
297
298class User:
299    name: str
300";
301        let analysis = analyze(src, "py");
302        assert_eq!(analysis.types.len(), 2);
303        assert_eq!(analysis.types[0].kind, TypeDefKind::Protocol);
304        assert_eq!(analysis.types[1].kind, TypeDefKind::Class);
305    }
306
307    #[test]
308    fn python_call_sites() {
309        // Regression for GH #365: Python uses a bare `call` node, so class
310        // instantiation and method calls must both be extracted as call sites.
311        let src = r"
312from models.engine import Engine
313
314def boot():
315    engine = Engine(power=100)
316    engine.run()
317    return engine
318";
319        let analysis = analyze(src, "py");
320        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
321        assert!(
322            callees.contains(&"Engine"),
323            "class instantiation should be a call site, got {callees:?}"
324        );
325        assert!(
326            callees.contains(&"run"),
327            "method call must resolve to the method name (not the receiver), got {callees:?}"
328        );
329    }
330
331    #[test]
332    fn java_object_creation_is_a_call_site() {
333        let src = r"
334class App {
335    void boot() {
336        Engine e = new Engine(100);
337        e.run();
338    }
339}
340";
341        let analysis = analyze(src, "java");
342        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
343        assert!(
344            callees.contains(&"Engine"),
345            "`new Engine()` should be a call site, got {callees:?}"
346        );
347        assert!(
348            callees.contains(&"run"),
349            "method call expected, got {callees:?}"
350        );
351    }
352
353    #[test]
354    fn go_imports() {
355        let src = r#"
356package main
357
358import (
359    "fmt"
360    "net/http"
361    _ "github.com/lib/pq"
362)
363"#;
364        let analysis = analyze(src, "go");
365        assert!(analysis.imports.len() >= 3);
366        let side_effect = analysis.imports.iter().find(|i| i.source.contains("pq"));
367        assert!(side_effect.is_some());
368        assert_eq!(side_effect.unwrap().kind, ImportKind::SideEffect);
369    }
370
371    #[test]
372    fn go_struct_and_interface() {
373        let src = r"
374package main
375
376type Server struct {
377    Port int
378}
379
380type Handler interface {
381    Handle(r *Request)
382}
383";
384        let analysis = analyze(src, "go");
385        assert_eq!(analysis.types.len(), 2);
386        let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
387        assert!(kinds.contains(&&TypeDefKind::Struct));
388        assert!(kinds.contains(&&TypeDefKind::Interface));
389    }
390
391    #[test]
392    fn java_imports() {
393        let src = r"
394import java.util.List;
395import java.util.Map;
396import static org.junit.Assert.*;
397";
398        let analysis = analyze(src, "java");
399        assert!(analysis.imports.len() >= 2);
400    }
401
402    #[test]
403    fn java_class_and_interface() {
404        let src = r"
405public class UserService {
406    public void save(User u) {}
407}
408
409public interface Repository<T> {
410    T findById(int id);
411}
412
413public enum Status { ACTIVE, INACTIVE }
414
415public record Point(int x, int y) {}
416";
417        let analysis = analyze(src, "java");
418        assert!(analysis.types.len() >= 3);
419        let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
420        assert!(kinds.contains(&&TypeDefKind::Class));
421        assert!(kinds.contains(&&TypeDefKind::Interface));
422        assert!(kinds.contains(&&TypeDefKind::Enum));
423    }
424
425    #[test]
426    fn kotlin_imports_and_aliases() {
427        let src = r"
428package com.example.app
429
430import com.example.services.UserService
431import com.example.factories.WidgetFactory as Factory
432import com.example.shared.*
433";
434        let analysis = analyze(src, "kt");
435        assert_eq!(analysis.imports.len(), 3);
436        assert_eq!(
437            analysis.imports[0].source,
438            "com.example.services.UserService"
439        );
440        assert_eq!(analysis.imports[1].names, vec!["Factory"]);
441        assert_eq!(analysis.imports[2].kind, ImportKind::Star);
442    }
443
444    #[test]
445    fn kotlin_call_sites() {
446        let src = r"
447class UserService {
448    fun run() {
449        prepare()
450        repository.save(user)
451        Factory.create()
452    }
453}
454";
455        let analysis = analyze(src, "kt");
456        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
457        assert!(callees.contains(&"prepare"));
458        assert!(callees.contains(&"save"));
459        assert!(callees.contains(&"create"));
460    }
461
462    #[test]
463    fn kotlin_types_and_visibility() {
464        let src = r"
465sealed interface Handler
466data class User(val id: String)
467enum class Status { ACTIVE, INACTIVE }
468object Registry
469private typealias UserId = String
470";
471        let analysis = analyze(src, "kt");
472        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
473        assert!(names.contains(&"Handler"));
474        assert!(names.contains(&"User"));
475        assert!(names.contains(&"Status"));
476        assert!(names.contains(&"Registry"));
477        assert!(names.contains(&"UserId"));
478        let handler = analysis.types.iter().find(|t| t.name == "Handler").unwrap();
479        assert_eq!(handler.kind, TypeDefKind::Interface);
480        let alias = analysis.types.iter().find(|t| t.name == "UserId").unwrap();
481        assert!(!alias.is_exported);
482    }
483
484    #[test]
485    fn ts_generics_extracted() {
486        let src = r"interface Result<T, E> { ok: T; err: E; }";
487        let analysis = analyze(src, "ts");
488        assert_eq!(analysis.types.len(), 1);
489        assert!(!analysis.types[0].generics.is_empty());
490    }
491
492    #[test]
493    fn mixed_analysis_ts() {
494        let src = r"
495import { Request, Response } from 'express';
496import type { User } from './models';
497
498export interface Handler {
499    handle(req: Request): Response;
500}
501
502export class Router {
503    register(path: string, handler: Handler) {
504        this.handlers.set(path, handler);
505    }
506}
507
508const app = express();
509app.listen(3000);
510";
511        let analysis = analyze(src, "ts");
512        assert!(analysis.imports.len() >= 2, "Should find imports");
513        assert!(!analysis.types.is_empty(), "Should find types");
514        assert!(!analysis.calls.is_empty(), "Should find calls");
515    }
516
517    #[test]
518    fn empty_file() {
519        let analysis = analyze("", "ts");
520        assert!(analysis.imports.is_empty());
521        assert!(analysis.calls.is_empty());
522        assert!(analysis.types.is_empty());
523    }
524
525    #[test]
526    fn unsupported_extension() {
527        let analysis = analyze("some content", "txt");
528        assert!(analysis.imports.is_empty());
529    }
530
531    #[test]
532    fn c_include_import() {
533        let src = r#"
534#include "foo/bar.h"
535#include <stdio.h>
536"#;
537        let analysis = analyze(src, "c");
538        assert!(analysis.imports.iter().any(|i| i.source == "foo/bar.h"));
539    }
540
541    #[test]
542    fn bash_source_import() {
543        let src = r#"
544source "./scripts/env.sh"
545. ../common.sh
546"#;
547        let analysis = analyze(src, "sh");
548        assert!(
549            analysis
550                .imports
551                .iter()
552                .any(|i| i.source.contains("scripts/env.sh")),
553            "expected source import"
554        );
555    }
556
557    #[test]
558    fn zig_at_import() {
559        let src = r#"
560const m = @import("lib/math.zig");
561const std = @import("std");
562"#;
563        let analysis = analyze(src, "zig");
564        assert!(analysis.imports.iter().any(|i| i.source == "lib/math.zig"));
565    }
566
567    #[test]
568    fn gdscript_imports_extends_and_preload() {
569        let src = r#"
570extends "res://actors/base_actor.gd"
571
572const Bullet = preload("res://weapons/bullet.gd")
573var sfx = load("res://audio/shot.wav")
574"#;
575        let analysis = analyze(src, "gd");
576        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
577        assert!(
578            sources.contains(&"res://actors/base_actor.gd"),
579            "expected extends import, got {sources:?}"
580        );
581        assert!(
582            sources.contains(&"res://weapons/bullet.gd"),
583            "expected preload import, got {sources:?}"
584        );
585        assert!(
586            sources.contains(&"res://audio/shot.wav"),
587            "expected load import, got {sources:?}"
588        );
589    }
590
591    #[test]
592    fn gdscript_types_class_name_and_enum() {
593        let src = r"
594class_name Player
595
596enum State { IDLE, RUNNING }
597
598class Inventory:
599    var items = []
600";
601        let analysis = analyze(src, "gd");
602        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
603        assert!(
604            names.contains(&"Player"),
605            "expected class_name, got {names:?}"
606        );
607        assert!(names.contains(&"State"), "expected enum, got {names:?}");
608        assert!(
609            names.contains(&"Inventory"),
610            "expected inner class, got {names:?}"
611        );
612        let player = analysis.types.iter().find(|t| t.name == "Player").unwrap();
613        assert_eq!(player.kind, TypeDefKind::Class);
614        assert!(player.is_exported);
615        let state = analysis.types.iter().find(|t| t.name == "State").unwrap();
616        assert_eq!(state.kind, TypeDefKind::Enum);
617    }
618
619    #[test]
620    fn csharp_imports_all_using_forms() {
621        let src = r"
622using System;
623using System.Collections.Generic;
624global using MyApp.Core;
625using static System.Math;
626using Json = Newtonsoft.Json;
627namespace MyApp.Services {
628    using MyApp.Data.Repositories;
629}
630";
631        let analysis = analyze(src, "cs");
632        let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
633        assert!(sources.contains(&"System"), "plain using, got {sources:?}");
634        assert!(
635            sources.contains(&"System.Collections.Generic"),
636            "dotted using, got {sources:?}"
637        );
638        assert!(
639            sources.contains(&"MyApp.Core"),
640            "global using must drop the `global` keyword, got {sources:?}"
641        );
642        assert!(
643            sources.contains(&"System.Math"),
644            "using static must drop the `static` keyword, got {sources:?}"
645        );
646        assert!(
647            sources.contains(&"Newtonsoft.Json"),
648            "alias using must keep the right-hand namespace, got {sources:?}"
649        );
650        assert!(
651            sources.contains(&"MyApp.Data.Repositories"),
652            "using nested inside a namespace block must be found, got {sources:?}"
653        );
654    }
655
656    /// GH #398: types consumed without any `using` (same-namespace visibility)
657    /// must surface as `type_uses` so the property graph can build TypeRef
658    /// edges. Covers fields, ctor parameters, return types, base list,
659    /// generic arguments, casts and `typeof`.
660    #[test]
661    fn csharp_type_uses_without_using_directive() {
662        let src = r"
663namespace App.Core;
664
665public class Motor : VehiclePart, IStartable
666{
667    private readonly Engine _engine;
668    public List<Sensor> Sensors { get; set; }
669
670    public Motor(Engine engine) { _engine = engine; }
671
672    public Gearbox BuildGearbox(Clutch clutch)
673    {
674        var t = typeof(Telemetry);
675        var d = (Dashboard)GetPart();
676        return null;
677    }
678}
679";
680        let analysis = analyze(src, "cs");
681        let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
682        for expected in [
683            "Engine",
684            "VehiclePart",
685            "IStartable",
686            "List",
687            "Sensor",
688            "Gearbox",
689            "Clutch",
690            "Telemetry",
691            "Dashboard",
692        ] {
693            assert!(names.contains(&expected), "missing {expected}: {names:?}");
694        }
695        // Predefined types carry no identifier node and must not appear.
696        assert!(!names.contains(&"var"), "var is not a type use: {names:?}");
697    }
698
699    /// GH #398 (Java flavour): same-package types are visible without import;
700    /// `type_identifier` nodes cover fields, params, returns and extends.
701    #[test]
702    fn java_type_uses_without_import() {
703        let src = r"
704package app.core;
705
706public class Motor extends VehiclePart {
707    private Engine engine;
708    public Gearbox build(Clutch clutch) { return null; }
709}
710";
711        let analysis = analyze(src, "java");
712        let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
713        for expected in ["VehiclePart", "Engine", "Gearbox", "Clutch"] {
714            assert!(names.contains(&expected), "missing {expected}: {names:?}");
715        }
716    }
717
718    /// Languages with mandatory explicit imports skip type-use extraction —
719    /// their dependencies are fully covered by the import resolver.
720    #[test]
721    fn type_uses_empty_for_import_based_languages() {
722        let rs = analyze("struct Foo { e: Engine }", "rs");
723        assert!(rs.type_uses.is_empty(), "rust: {:?}", rs.type_uses);
724        let ts = analyze("const e: Engine = make();", "ts");
725        assert!(ts.type_uses.is_empty(), "ts: {:?}", ts.type_uses);
726    }
727
728    #[test]
729    fn csharp_types_and_visibility() {
730        let src = r"
731namespace App
732{
733    public class UserService { }
734    internal class Helper { }
735    public interface IRepository { }
736    public struct Point { public int X; }
737    public enum Status { Active, Inactive }
738    public record Money(decimal Amount, string Currency);
739}
740";
741        let analysis = analyze(src, "cs");
742        let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
743        assert!(names.contains(&"UserService"), "class, got {names:?}");
744        assert!(names.contains(&"Helper"), "internal class, got {names:?}");
745        assert!(names.contains(&"IRepository"), "interface, got {names:?}");
746        assert!(names.contains(&"Point"), "struct, got {names:?}");
747        assert!(names.contains(&"Status"), "enum, got {names:?}");
748        assert!(names.contains(&"Money"), "record, got {names:?}");
749
750        let kind_of = |n: &str| {
751            analysis
752                .types
753                .iter()
754                .find(|t| t.name == n)
755                .map(|t| t.kind.clone())
756        };
757        assert_eq!(kind_of("UserService"), Some(TypeDefKind::Class));
758        assert_eq!(kind_of("IRepository"), Some(TypeDefKind::Interface));
759        assert_eq!(kind_of("Point"), Some(TypeDefKind::Struct));
760        assert_eq!(kind_of("Status"), Some(TypeDefKind::Enum));
761        assert_eq!(kind_of("Money"), Some(TypeDefKind::Record));
762
763        let exported = |n: &str| {
764            analysis
765                .types
766                .iter()
767                .find(|t| t.name == n)
768                .is_some_and(|t| t.is_exported)
769        };
770        assert!(exported("UserService"), "public class is exported");
771        assert!(
772            !exported("Helper"),
773            "internal class must not be marked exported"
774        );
775        assert!(analysis.exports.contains(&"UserService".to_string()));
776    }
777
778    #[test]
779    fn csharp_call_sites() {
780        let src = r"
781namespace App
782{
783    public class Boot
784    {
785        public void Run()
786        {
787            Prepare();
788            _repository.Save(user);
789            var engine = new Engine(100);
790            Factory.Create<Widget>();
791        }
792    }
793}
794";
795        let analysis = analyze(src, "cs");
796        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
797        assert!(
798            callees.contains(&"Prepare"),
799            "direct invocation, got {callees:?}"
800        );
801        assert!(
802            callees.contains(&"Save"),
803            "member invocation must resolve to the method name, got {callees:?}"
804        );
805        assert!(
806            callees.contains(&"Engine"),
807            "`new Engine()` should reference the constructed type, got {callees:?}"
808        );
809        assert!(
810            callees.contains(&"Create"),
811            "generic member call must reduce to the identifier, got {callees:?}"
812        );
813
814        let save = analysis.calls.iter().find(|c| c.callee == "Save").unwrap();
815        assert_eq!(save.receiver.as_deref(), Some("_repository"));
816        assert!(save.is_method);
817    }
818
819    #[test]
820    fn gdscript_calls_method_and_instantiation() {
821        let src = r"
822func _ready():
823    var mgr = MapDataManager.new()
824    mgr.load_map_data()
825    update_state()
826";
827        let analysis = analyze(src, "gd");
828        let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
829        // `MapDataManager.new()` registers a reference to the class itself.
830        assert!(
831            callees.contains(&"MapDataManager"),
832            "expected instantiation to reference class, got {callees:?}"
833        );
834        assert!(
835            callees.contains(&"load_map_data"),
836            "expected method call, got {callees:?}"
837        );
838        assert!(
839            callees.contains(&"update_state"),
840            "expected direct call, got {callees:?}"
841        );
842    }
843}