use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
const HOSTS: [&str; 3] = ["fabric", "forge", "neoforge"];
fn repo() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..")
}
fn runtime_sources() -> String {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut out = String::new();
for e in fs::read_dir(dir).unwrap() {
let p = e.unwrap().path();
if p.extension().is_some_and(|x| x == "rs") {
out.push_str(&fs::read_to_string(p).unwrap());
}
}
out
}
fn literal(s: &str) -> Option<(&str, &str)> {
let s = s.trim_start().strip_prefix('"')?;
let end = s.find('"')?;
Some((&s[..end], &s[end + 1..]))
}
fn list_len(s: &str) -> Option<usize> {
let s = s.trim_start().strip_prefix('[')?;
let (mut depth, mut items, mut any) = (0i32, 0usize, false);
for c in s.chars() {
match c {
'(' | '[' | '{' => depth += 1,
')' | '}' => depth -= 1,
']' if depth == 0 => return Some(items + any as usize),
']' => depth -= 1,
',' if depth == 0 => {
items += any as usize;
any = false;
continue;
}
_ => {}
}
if !c.is_whitespace() && depth >= 0 {
any = true;
}
}
None
}
fn calls(src: &str) -> BTreeSet<(String, String, usize)> {
let mut out = BTreeSet::new();
for (i, _) in src.match_indices("bridge!(") {
let rest = &src[i + "bridge!(".len()..];
let Some((name, rest)) = literal(rest) else {
continue;
};
let Some(args) = rest.trim_start().strip_prefix(',') else {
continue;
};
if let Some(n) = list_len(args) {
out.insert(("NativeBridge".to_owned(), name.to_owned(), n));
}
}
for (i, _) in src.match_indices("\"dev/yog/") {
let Some((class, rest)) = literal(&src[i..]) else {
continue;
};
let class = &class["dev/yog/".len()..];
let Some(rest) = rest.trim_start().strip_prefix(',') else {
continue;
};
let Some((name, rest)) = literal(rest) else {
continue;
};
let Some(args) = rest.trim_start().strip_prefix(',') else {
continue;
};
if let Some(n) = list_len(args) {
out.insert((class.to_owned(), name.to_owned(), n));
}
}
out
}
fn java_statics(java: &str) -> BTreeSet<(String, usize)> {
let mut out = BTreeSet::new();
for (i, _) in java.match_indices("public static ") {
let decl = &java[i..];
let Some(open) = decl.find('(') else { continue };
let head = &decl[..open];
if head.contains(['=', ';', '{']) || head.contains(" class ") {
continue;
}
let Some(name) = head.split_whitespace().last() else {
continue;
};
let Some(close) = decl[open..].find(')') else {
continue;
};
let params = &decl[open + 1..open + close];
let (mut depth, mut commas) = (0i32, 0usize);
for c in params.chars() {
match c {
'<' => depth += 1,
'>' => depth -= 1,
',' if depth == 0 => commas += 1,
_ => {}
}
}
let arity = if params.trim().is_empty() {
0
} else {
commas + 1
};
out.insert((name.to_owned(), arity));
}
out
}
fn class_statics(
root: &Path,
host: &str,
platform: &Path,
class: &str,
) -> Option<BTreeSet<(String, usize)>> {
let file = format!("{class}.java");
[
platform.join("src/main/java/dev/yog"),
root.join(host).join("src/main/java/dev/yog"),
root.join("java-common/src/main/java/dev/yog"),
]
.iter()
.find_map(|d| fs::read_to_string(d.join(&file)).ok())
.map(|j| java_statics(&j))
}
#[test]
fn every_host_has_every_bridge_method() {
let root = repo();
if !root.join("fabric").is_dir() {
return;
}
let calls = calls(&runtime_sources());
assert!(
calls.len() > 50,
"found only {} bridge calls — parser broken?",
calls.len()
);
let mut missing = Vec::new();
for host in HOSTS {
let Ok(platforms) = fs::read_dir(root.join(host).join("platforms")) else {
continue;
};
for p in platforms {
let platform = p.unwrap().path();
for (class, name, arity) in &calls {
let ok = class_statics(&root, host, &platform, class)
.is_some_and(|s| s.contains(&(name.clone(), *arity)));
if !ok {
missing.push(format!("{}: {class}.{name}/{arity}", platform.display()));
}
}
}
}
assert!(
missing.is_empty(),
"missing Java bridge methods:\n{}",
missing.join("\n")
);
}
#[test]
fn argument_count() {
assert_eq!(list_len("[S(dim), I(pos.x), f(a, b)]"), Some(3));
assert_eq!(list_len("[]"), Some(0));
assert_eq!(list_len("[S(x),\n]"), Some(1));
}
#[test]
fn every_native_method_is_exported() {
let root = repo();
if !root.join("fabric").is_dir() {
return;
}
let src = runtime_sources();
let mut missing = BTreeSet::new();
let mut seen = 0;
for host in HOSTS {
let Ok(platforms) = fs::read_dir(root.join(host).join("platforms")) else {
continue;
};
for p in platforms {
let file = p.unwrap().path().join("src/main/java/dev/yog/NativeBridge.java");
let Ok(java) = fs::read_to_string(&file) else {
continue;
};
for line in java.lines() {
let Some(rest) = line.split(" static native ").nth(1) else {
continue;
};
let Some(name) = rest.split('(').next().and_then(|s| s.split_whitespace().last()) else {
continue;
};
seen += 1;
if !src.contains(&format!("fn Java_dev_yog_NativeBridge_{name}")) {
missing.insert(format!("{}: {name}", file.display()));
}
}
}
}
assert!(seen > 100, "found only {seen} native methods — parser broken?");
assert!(missing.is_empty(), "native methods with no runtime export:\n{}", missing.into_iter().collect::<Vec<_>>().join("\n"));
}