1mod 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#[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#[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 crate::ast_walk::find_descendant_by_kind(node, kind)
118}
119
120#[cfg(test)]
125#[cfg(feature = "tree-sitter")]
126mod tests {
127 use super::*;
128
129 #[test]
135 fn deeply_nested_source_does_not_overflow() {
136 let depth = 12_000;
137 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 #[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 .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 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 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 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 #[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 assert!(!names.contains(&"var"), "var is not a type use: {names:?}");
833 }
834
835 #[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 assert!(
867 names.contains(&"ApiControllerAttribute"),
868 "attribute suffix variant must be present: {names:?}"
869 );
870 assert!(
872 !names.contains(&"engine"),
873 "instance receiver must not be a type use: {names:?}"
874 );
875 }
876
877 #[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 #[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 assert!(
925 !names.contains(&"Widget"),
926 "qualified pkg.Type must be skipped: {names:?}"
927 );
928 }
929
930 #[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 assert!(
962 !names.contains(&"com"),
963 "package qualifier must be dropped: {names:?}"
964 );
965 }
966
967 #[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 #[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 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 let analysis = analyze("type Account = {}", "lua");
1187 assert!(analysis.types.is_empty());
1188 }
1189
1190 #[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 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 #[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 #[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}