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