Skip to main content

lean_ctx/core/deep_queries/
mod.rs

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