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