#![cfg(feature = "lang-gdscript")]
use std::collections::HashMap;
use std::fs;
use tempfile::TempDir;
use tokensave::tokensave::TokenSave;
use tokensave::types::{Edge, Node};
async fn setup_godot_project() -> (TempDir, TokenSave) {
let dir = TempDir::new().unwrap();
let project = dir.path();
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("src/registry.cpp"),
r#"
#include <godot_cpp/classes/object.hpp>
using namespace godot;
class MyRegistry : public Object {
GDCLASS(MyRegistry, Object)
static MyRegistry *singleton;
protected:
static void _bind_methods();
public:
static MyRegistry *get_singleton() { return singleton; }
void register_item(const Dictionary &cfg) {
int x = 1;
}
static void register_global(const Dictionary &cfg) {
int y = 2;
}
};
MyRegistry *MyRegistry::singleton = nullptr;
void MyRegistry::_bind_methods() {
ClassDB::bind_method(D_METHOD("register_item", "cfg"), &MyRegistry::register_item);
ClassDB::bind_static_method("MyRegistry", D_METHOD("register_global", "cfg"), &MyRegistry::register_global);
}
"#,
)
.unwrap();
fs::write(
project.join("src/game.gd"),
r#"
extends Node
func _ready():
MyRegistry.get_singleton().register_item({ "id": "example" })
MyRegistry.register_global({ "id": "global" })
"#,
)
.unwrap();
let cg = TokenSave::init(project).await.unwrap();
cg.index_all().await.unwrap();
(dir, cg)
}
fn has_call_edge(edges: &[Edge], by_id: &HashMap<String, &Node>, from: &str, to: &str) -> bool {
edges.iter().any(|e| {
by_id.get(&e.source).map(|n| n.name.as_str()) == Some(from)
&& by_id.get(&e.target).map(|n| n.name.as_str()) == Some(to)
})
}
#[tokio::test]
async fn gdscript_call_resolves_to_bound_cpp_method() {
let (_dir, cg) = setup_godot_project().await;
let nodes = cg.get_all_nodes().await.unwrap();
let edges = cg.get_all_edges().await.unwrap();
let by_id: HashMap<String, &Node> = nodes.iter().map(|n| (n.id.clone(), n)).collect();
assert!(
has_call_edge(&edges, &by_id, "_ready", "register_item"),
"GDScript call should resolve to the C++ MyRegistry::register_item method"
);
assert!(
has_call_edge(&edges, &by_id, "_ready", "register_global"),
"GDScript call should resolve to the C++ MyRegistry::register_global method"
);
}
#[tokio::test]
async fn bind_methods_links_to_exposed_methods() {
let (_dir, cg) = setup_godot_project().await;
let nodes = cg.get_all_nodes().await.unwrap();
let edges = cg.get_all_edges().await.unwrap();
let by_id: HashMap<String, &Node> = nodes.iter().map(|n| (n.id.clone(), n)).collect();
assert!(
has_call_edge(&edges, &by_id, "_bind_methods", "register_item"),
"_bind_methods should have a Calls edge to register_item"
);
assert!(
has_call_edge(&edges, &by_id, "_bind_methods", "register_global"),
"_bind_methods should have a Calls edge to register_global (bind_static_method)"
);
}
#[tokio::test]
async fn binding_boilerplate_not_reported_dead() {
let (_dir, cg) = setup_godot_project().await;
let dead = cg.find_dead_code(&[], true, true).await.unwrap();
let dead_names: Vec<&str> = dead.iter().map(|n| n.name.as_str()).collect();
assert!(
!dead_names.contains(&"_bind_methods"),
"_bind_methods is an engine-invoked callback and must not be dead code, got {dead_names:?}"
);
assert!(
!dead_names.contains(&"register_item"),
"bound method register_item must not be dead code, got {dead_names:?}"
);
assert!(
!dead_names.contains(&"register_global"),
"bound method register_global must not be dead code, got {dead_names:?}"
);
assert!(
!dead_names.contains(&"get_singleton"),
"get_singleton is called from GDScript and must not be dead code, got {dead_names:?}"
);
}