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