Skip to main content

lean_ctx_syntax/deep_queries/
mod.rs

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