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 _ => None,
85 }
86}
87
88#[cfg(feature = "tree-sitter")]
93fn node_text<'a>(node: Node, src: &'a str) -> &'a str {
94 &src[node.byte_range()]
95}
96
97#[cfg(feature = "tree-sitter")]
98fn find_child_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
99 let mut cursor = node.walk();
100 let result = node.children(&mut cursor).find(|c| c.kind() == kind);
101 result
102}
103
104#[cfg(feature = "tree-sitter")]
105fn find_descendant_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
106 crate::core::ast_walk::find_descendant_by_kind(node, kind)
108}
109
110#[cfg(test)]
115#[cfg(feature = "tree-sitter")]
116mod tests {
117 use super::*;
118
119 #[test]
125 fn deeply_nested_source_does_not_overflow() {
126 let depth = 12_000;
127 let rs = format!(
131 "fn m() {{ let _ = {}0{}; }}",
132 "f(".repeat(depth),
133 ")".repeat(depth)
134 );
135 let analysis = analyze(&rs, "rs");
136 assert!(!analysis.calls.is_empty());
137 }
138
139 #[test]
140 fn ts_named_import() {
141 let src = r"import { useState, useEffect } from 'react';";
142 let analysis = analyze(src, "ts");
143 assert_eq!(analysis.imports.len(), 1);
144 assert_eq!(analysis.imports[0].source, "react");
145 assert_eq!(analysis.imports[0].names, vec!["useState", "useEffect"]);
146 }
147
148 #[test]
149 fn ts_default_import() {
150 let src = r"import React from 'react';";
151 let analysis = analyze(src, "ts");
152 assert_eq!(analysis.imports.len(), 1);
153 assert_eq!(analysis.imports[0].kind, ImportKind::Default);
154 assert_eq!(analysis.imports[0].names, vec!["React"]);
155 }
156
157 #[test]
158 fn ts_star_import() {
159 let src = r"import * as path from 'path';";
160 let analysis = analyze(src, "ts");
161 assert_eq!(analysis.imports.len(), 1);
162 assert_eq!(analysis.imports[0].kind, ImportKind::Star);
163 }
164
165 #[test]
166 fn ts_side_effect_import() {
167 let src = r"import './styles.css';";
168 let analysis = analyze(src, "ts");
169 assert_eq!(analysis.imports.len(), 1);
170 assert_eq!(analysis.imports[0].kind, ImportKind::SideEffect);
171 assert_eq!(analysis.imports[0].source, "./styles.css");
172 }
173
174 #[test]
175 fn ts_type_only_import() {
176 let src = r"import type { User } from './types';";
177 let analysis = analyze(src, "ts");
178 assert_eq!(analysis.imports.len(), 1);
179 assert!(analysis.imports[0].is_type_only);
180 }
181
182 #[test]
183 fn ts_reexport() {
184 let src = r"export { foo, bar } from './utils';";
185 let analysis = analyze(src, "ts");
186 assert_eq!(analysis.imports.len(), 1);
187 assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
188 }
189
190 #[test]
191 fn ts_call_sites() {
192 let src = r"
193const x = foo(1);
194const y = obj.method(2);
195";
196 let analysis = analyze(src, "ts");
197 assert!(analysis.calls.len() >= 2);
198 let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
199 assert!(fns.contains(&"foo"));
200 assert!(fns.contains(&"method"));
201 }
202
203 #[test]
204 fn ts_interface() {
205 let src = r"
206export interface User {
207 name: string;
208 age: number;
209}
210";
211 let analysis = analyze(src, "ts");
212 assert_eq!(analysis.types.len(), 1);
213 assert_eq!(analysis.types[0].name, "User");
214 assert_eq!(analysis.types[0].kind, TypeDefKind::Interface);
215 }
216
217 #[test]
218 fn ts_type_alias_union() {
219 let src = r"type Result = Success | Error;";
220 let analysis = analyze(src, "ts");
221 assert_eq!(analysis.types.len(), 1);
222 assert_eq!(analysis.types[0].kind, TypeDefKind::Union);
223 }
224
225 #[test]
226 fn rust_use_statements() {
227 let src = r"
228use crate::core::session;
229use anyhow::Result;
230use std::collections::HashMap;
231";
232 let analysis = analyze(src, "rs");
233 assert_eq!(analysis.imports.len(), 2);
234 let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
235 assert!(sources.contains(&"crate::core::session"));
236 assert!(sources.contains(&"anyhow::Result"));
237 }
238
239 #[test]
240 fn rust_pub_use_reexport() {
241 let src = r"pub use crate::tools::ctx_read;";
242 let analysis = analyze(src, "rs");
243 assert_eq!(analysis.imports.len(), 1);
244 assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
245 }
246
247 #[test]
248 fn rust_struct_and_trait() {
249 let src = r"
250pub struct Config {
251 pub name: String,
252}
253
254pub trait Service {
255 fn run(&self);
256}
257";
258 let analysis = analyze(src, "rs");
259 assert_eq!(analysis.types.len(), 2);
260 let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
261 assert!(names.contains(&"Config"));
262 assert!(names.contains(&"Service"));
263 }
264
265 #[test]
266 fn rust_call_sites() {
267 let src = r"
268fn main() {
269 let x = calculate(42);
270 let y = self.process();
271 Vec::new();
272}
273";
274 let analysis = analyze(src, "rs");
275 assert!(analysis.calls.len() >= 2);
276 let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
277 assert!(fns.contains(&"calculate"));
278 }
279
280 #[test]
281 fn python_imports() {
282 let src = r"
283import os
284from pathlib import Path
285from . import utils
286from ..models import User, Role
287";
288 let analysis = analyze(src, "py");
289 assert!(analysis.imports.len() >= 3);
290 }
291
292 #[test]
293 fn python_class_protocol() {
294 let src = r"
295class MyProtocol(Protocol):
296 def method(self) -> None: ...
297
298class User:
299 name: str
300";
301 let analysis = analyze(src, "py");
302 assert_eq!(analysis.types.len(), 2);
303 assert_eq!(analysis.types[0].kind, TypeDefKind::Protocol);
304 assert_eq!(analysis.types[1].kind, TypeDefKind::Class);
305 }
306
307 #[test]
308 fn python_call_sites() {
309 let src = r"
312from models.engine import Engine
313
314def boot():
315 engine = Engine(power=100)
316 engine.run()
317 return engine
318";
319 let analysis = analyze(src, "py");
320 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
321 assert!(
322 callees.contains(&"Engine"),
323 "class instantiation should be a call site, got {callees:?}"
324 );
325 assert!(
326 callees.contains(&"run"),
327 "method call must resolve to the method name (not the receiver), got {callees:?}"
328 );
329 }
330
331 #[test]
332 fn java_object_creation_is_a_call_site() {
333 let src = r"
334class App {
335 void boot() {
336 Engine e = new Engine(100);
337 e.run();
338 }
339}
340";
341 let analysis = analyze(src, "java");
342 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
343 assert!(
344 callees.contains(&"Engine"),
345 "`new Engine()` should be a call site, got {callees:?}"
346 );
347 assert!(
348 callees.contains(&"run"),
349 "method call expected, got {callees:?}"
350 );
351 }
352
353 #[test]
354 fn go_imports() {
355 let src = r#"
356package main
357
358import (
359 "fmt"
360 "net/http"
361 _ "github.com/lib/pq"
362)
363"#;
364 let analysis = analyze(src, "go");
365 assert!(analysis.imports.len() >= 3);
366 let side_effect = analysis.imports.iter().find(|i| i.source.contains("pq"));
367 assert!(side_effect.is_some());
368 assert_eq!(side_effect.unwrap().kind, ImportKind::SideEffect);
369 }
370
371 #[test]
372 fn go_struct_and_interface() {
373 let src = r"
374package main
375
376type Server struct {
377 Port int
378}
379
380type Handler interface {
381 Handle(r *Request)
382}
383";
384 let analysis = analyze(src, "go");
385 assert_eq!(analysis.types.len(), 2);
386 let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
387 assert!(kinds.contains(&&TypeDefKind::Struct));
388 assert!(kinds.contains(&&TypeDefKind::Interface));
389 }
390
391 #[test]
392 fn java_imports() {
393 let src = r"
394import java.util.List;
395import java.util.Map;
396import static org.junit.Assert.*;
397";
398 let analysis = analyze(src, "java");
399 assert!(analysis.imports.len() >= 2);
400 }
401
402 #[test]
403 fn java_class_and_interface() {
404 let src = r"
405public class UserService {
406 public void save(User u) {}
407}
408
409public interface Repository<T> {
410 T findById(int id);
411}
412
413public enum Status { ACTIVE, INACTIVE }
414
415public record Point(int x, int y) {}
416";
417 let analysis = analyze(src, "java");
418 assert!(analysis.types.len() >= 3);
419 let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
420 assert!(kinds.contains(&&TypeDefKind::Class));
421 assert!(kinds.contains(&&TypeDefKind::Interface));
422 assert!(kinds.contains(&&TypeDefKind::Enum));
423 }
424
425 #[test]
426 fn kotlin_imports_and_aliases() {
427 let src = r"
428package com.example.app
429
430import com.example.services.UserService
431import com.example.factories.WidgetFactory as Factory
432import com.example.shared.*
433";
434 let analysis = analyze(src, "kt");
435 assert_eq!(analysis.imports.len(), 3);
436 assert_eq!(
437 analysis.imports[0].source,
438 "com.example.services.UserService"
439 );
440 assert_eq!(analysis.imports[1].names, vec!["Factory"]);
441 assert_eq!(analysis.imports[2].kind, ImportKind::Star);
442 }
443
444 #[test]
445 fn kotlin_call_sites() {
446 let src = r"
447class UserService {
448 fun run() {
449 prepare()
450 repository.save(user)
451 Factory.create()
452 }
453}
454";
455 let analysis = analyze(src, "kt");
456 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
457 assert!(callees.contains(&"prepare"));
458 assert!(callees.contains(&"save"));
459 assert!(callees.contains(&"create"));
460 }
461
462 #[test]
463 fn kotlin_types_and_visibility() {
464 let src = r"
465sealed interface Handler
466data class User(val id: String)
467enum class Status { ACTIVE, INACTIVE }
468object Registry
469private typealias UserId = String
470";
471 let analysis = analyze(src, "kt");
472 let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
473 assert!(names.contains(&"Handler"));
474 assert!(names.contains(&"User"));
475 assert!(names.contains(&"Status"));
476 assert!(names.contains(&"Registry"));
477 assert!(names.contains(&"UserId"));
478 let handler = analysis.types.iter().find(|t| t.name == "Handler").unwrap();
479 assert_eq!(handler.kind, TypeDefKind::Interface);
480 let alias = analysis.types.iter().find(|t| t.name == "UserId").unwrap();
481 assert!(!alias.is_exported);
482 }
483
484 #[test]
485 fn ts_generics_extracted() {
486 let src = r"interface Result<T, E> { ok: T; err: E; }";
487 let analysis = analyze(src, "ts");
488 assert_eq!(analysis.types.len(), 1);
489 assert!(!analysis.types[0].generics.is_empty());
490 }
491
492 #[test]
493 fn mixed_analysis_ts() {
494 let src = r"
495import { Request, Response } from 'express';
496import type { User } from './models';
497
498export interface Handler {
499 handle(req: Request): Response;
500}
501
502export class Router {
503 register(path: string, handler: Handler) {
504 this.handlers.set(path, handler);
505 }
506}
507
508const app = express();
509app.listen(3000);
510";
511 let analysis = analyze(src, "ts");
512 assert!(analysis.imports.len() >= 2, "Should find imports");
513 assert!(!analysis.types.is_empty(), "Should find types");
514 assert!(!analysis.calls.is_empty(), "Should find calls");
515 }
516
517 #[test]
518 fn empty_file() {
519 let analysis = analyze("", "ts");
520 assert!(analysis.imports.is_empty());
521 assert!(analysis.calls.is_empty());
522 assert!(analysis.types.is_empty());
523 }
524
525 #[test]
526 fn unsupported_extension() {
527 let analysis = analyze("some content", "txt");
528 assert!(analysis.imports.is_empty());
529 }
530
531 #[test]
532 fn c_include_import() {
533 let src = r#"
534#include "foo/bar.h"
535#include <stdio.h>
536"#;
537 let analysis = analyze(src, "c");
538 assert!(analysis.imports.iter().any(|i| i.source == "foo/bar.h"));
539 }
540
541 #[test]
542 fn bash_source_import() {
543 let src = r#"
544source "./scripts/env.sh"
545. ../common.sh
546"#;
547 let analysis = analyze(src, "sh");
548 assert!(
549 analysis
550 .imports
551 .iter()
552 .any(|i| i.source.contains("scripts/env.sh")),
553 "expected source import"
554 );
555 }
556
557 #[test]
558 fn zig_at_import() {
559 let src = r#"
560const m = @import("lib/math.zig");
561const std = @import("std");
562"#;
563 let analysis = analyze(src, "zig");
564 assert!(analysis.imports.iter().any(|i| i.source == "lib/math.zig"));
565 }
566
567 #[test]
568 fn gdscript_imports_extends_and_preload() {
569 let src = r#"
570extends "res://actors/base_actor.gd"
571
572const Bullet = preload("res://weapons/bullet.gd")
573var sfx = load("res://audio/shot.wav")
574"#;
575 let analysis = analyze(src, "gd");
576 let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
577 assert!(
578 sources.contains(&"res://actors/base_actor.gd"),
579 "expected extends import, got {sources:?}"
580 );
581 assert!(
582 sources.contains(&"res://weapons/bullet.gd"),
583 "expected preload import, got {sources:?}"
584 );
585 assert!(
586 sources.contains(&"res://audio/shot.wav"),
587 "expected load import, got {sources:?}"
588 );
589 }
590
591 #[test]
592 fn gdscript_types_class_name_and_enum() {
593 let src = r"
594class_name Player
595
596enum State { IDLE, RUNNING }
597
598class Inventory:
599 var items = []
600";
601 let analysis = analyze(src, "gd");
602 let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
603 assert!(
604 names.contains(&"Player"),
605 "expected class_name, got {names:?}"
606 );
607 assert!(names.contains(&"State"), "expected enum, got {names:?}");
608 assert!(
609 names.contains(&"Inventory"),
610 "expected inner class, got {names:?}"
611 );
612 let player = analysis.types.iter().find(|t| t.name == "Player").unwrap();
613 assert_eq!(player.kind, TypeDefKind::Class);
614 assert!(player.is_exported);
615 let state = analysis.types.iter().find(|t| t.name == "State").unwrap();
616 assert_eq!(state.kind, TypeDefKind::Enum);
617 }
618
619 #[test]
620 fn csharp_imports_all_using_forms() {
621 let src = r"
622using System;
623using System.Collections.Generic;
624global using MyApp.Core;
625using static System.Math;
626using Json = Newtonsoft.Json;
627namespace MyApp.Services {
628 using MyApp.Data.Repositories;
629}
630";
631 let analysis = analyze(src, "cs");
632 let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
633 assert!(sources.contains(&"System"), "plain using, got {sources:?}");
634 assert!(
635 sources.contains(&"System.Collections.Generic"),
636 "dotted using, got {sources:?}"
637 );
638 assert!(
639 sources.contains(&"MyApp.Core"),
640 "global using must drop the `global` keyword, got {sources:?}"
641 );
642 assert!(
643 sources.contains(&"System.Math"),
644 "using static must drop the `static` keyword, got {sources:?}"
645 );
646 assert!(
647 sources.contains(&"Newtonsoft.Json"),
648 "alias using must keep the right-hand namespace, got {sources:?}"
649 );
650 assert!(
651 sources.contains(&"MyApp.Data.Repositories"),
652 "using nested inside a namespace block must be found, got {sources:?}"
653 );
654 }
655
656 #[test]
661 fn csharp_type_uses_without_using_directive() {
662 let src = r"
663namespace App.Core;
664
665public class Motor : VehiclePart, IStartable
666{
667 private readonly Engine _engine;
668 public List<Sensor> Sensors { get; set; }
669
670 public Motor(Engine engine) { _engine = engine; }
671
672 public Gearbox BuildGearbox(Clutch clutch)
673 {
674 var t = typeof(Telemetry);
675 var d = (Dashboard)GetPart();
676 return null;
677 }
678}
679";
680 let analysis = analyze(src, "cs");
681 let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
682 for expected in [
683 "Engine",
684 "VehiclePart",
685 "IStartable",
686 "List",
687 "Sensor",
688 "Gearbox",
689 "Clutch",
690 "Telemetry",
691 "Dashboard",
692 ] {
693 assert!(names.contains(&expected), "missing {expected}: {names:?}");
694 }
695 assert!(!names.contains(&"var"), "var is not a type use: {names:?}");
697 }
698
699 #[test]
702 fn java_type_uses_without_import() {
703 let src = r"
704package app.core;
705
706public class Motor extends VehiclePart {
707 private Engine engine;
708 public Gearbox build(Clutch clutch) { return null; }
709}
710";
711 let analysis = analyze(src, "java");
712 let names: Vec<&str> = analysis.type_uses.iter().map(|u| u.name.as_str()).collect();
713 for expected in ["VehiclePart", "Engine", "Gearbox", "Clutch"] {
714 assert!(names.contains(&expected), "missing {expected}: {names:?}");
715 }
716 }
717
718 #[test]
721 fn type_uses_empty_for_import_based_languages() {
722 let rs = analyze("struct Foo { e: Engine }", "rs");
723 assert!(rs.type_uses.is_empty(), "rust: {:?}", rs.type_uses);
724 let ts = analyze("const e: Engine = make();", "ts");
725 assert!(ts.type_uses.is_empty(), "ts: {:?}", ts.type_uses);
726 }
727
728 #[test]
729 fn csharp_types_and_visibility() {
730 let src = r"
731namespace App
732{
733 public class UserService { }
734 internal class Helper { }
735 public interface IRepository { }
736 public struct Point { public int X; }
737 public enum Status { Active, Inactive }
738 public record Money(decimal Amount, string Currency);
739}
740";
741 let analysis = analyze(src, "cs");
742 let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
743 assert!(names.contains(&"UserService"), "class, got {names:?}");
744 assert!(names.contains(&"Helper"), "internal class, got {names:?}");
745 assert!(names.contains(&"IRepository"), "interface, got {names:?}");
746 assert!(names.contains(&"Point"), "struct, got {names:?}");
747 assert!(names.contains(&"Status"), "enum, got {names:?}");
748 assert!(names.contains(&"Money"), "record, got {names:?}");
749
750 let kind_of = |n: &str| {
751 analysis
752 .types
753 .iter()
754 .find(|t| t.name == n)
755 .map(|t| t.kind.clone())
756 };
757 assert_eq!(kind_of("UserService"), Some(TypeDefKind::Class));
758 assert_eq!(kind_of("IRepository"), Some(TypeDefKind::Interface));
759 assert_eq!(kind_of("Point"), Some(TypeDefKind::Struct));
760 assert_eq!(kind_of("Status"), Some(TypeDefKind::Enum));
761 assert_eq!(kind_of("Money"), Some(TypeDefKind::Record));
762
763 let exported = |n: &str| {
764 analysis
765 .types
766 .iter()
767 .find(|t| t.name == n)
768 .is_some_and(|t| t.is_exported)
769 };
770 assert!(exported("UserService"), "public class is exported");
771 assert!(
772 !exported("Helper"),
773 "internal class must not be marked exported"
774 );
775 assert!(analysis.exports.contains(&"UserService".to_string()));
776 }
777
778 #[test]
779 fn csharp_call_sites() {
780 let src = r"
781namespace App
782{
783 public class Boot
784 {
785 public void Run()
786 {
787 Prepare();
788 _repository.Save(user);
789 var engine = new Engine(100);
790 Factory.Create<Widget>();
791 }
792 }
793}
794";
795 let analysis = analyze(src, "cs");
796 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
797 assert!(
798 callees.contains(&"Prepare"),
799 "direct invocation, got {callees:?}"
800 );
801 assert!(
802 callees.contains(&"Save"),
803 "member invocation must resolve to the method name, got {callees:?}"
804 );
805 assert!(
806 callees.contains(&"Engine"),
807 "`new Engine()` should reference the constructed type, got {callees:?}"
808 );
809 assert!(
810 callees.contains(&"Create"),
811 "generic member call must reduce to the identifier, got {callees:?}"
812 );
813
814 let save = analysis.calls.iter().find(|c| c.callee == "Save").unwrap();
815 assert_eq!(save.receiver.as_deref(), Some("_repository"));
816 assert!(save.is_method);
817 }
818
819 #[test]
820 fn gdscript_calls_method_and_instantiation() {
821 let src = r"
822func _ready():
823 var mgr = MapDataManager.new()
824 mgr.load_map_data()
825 update_state()
826";
827 let analysis = analyze(src, "gd");
828 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
829 assert!(
831 callees.contains(&"MapDataManager"),
832 "expected instantiation to reference class, got {callees:?}"
833 );
834 assert!(
835 callees.contains(&"load_map_data"),
836 "expected method call, got {callees:?}"
837 );
838 assert!(
839 callees.contains(&"update_state"),
840 "expected direct call, got {callees:?}"
841 );
842 }
843}