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