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