use yog_ids::script_problem;
use yog_script_analyze::index::{Class, ClassIndex, Member, Side, access};
use yog_script_analyze::{check, check_with};
fn codes(src: &str) -> Vec<String> {
codes_of(check("a.ys", src).problems)
}
fn codes_of(problems: Vec<yog_data::Diagnostic>) -> Vec<String> {
problems
.into_iter()
.filter_map(|d| d.code.map(|c| c.to_string()))
.collect()
}
#[test]
fn values_fit_their_types() {
assert_eq!(
codes("void F() {\n i32 a = \"x\";\n}\n"),
[script_problem::TYPE_MISMATCH]
);
assert_eq!(codes("i32 F() => true;\n"), [script_problem::TYPE_MISMATCH]);
assert_eq!(
codes("void F(bool b) {\n if (1 + 2) b = false;\n}\n"),
[script_problem::TYPE_MISMATCH]
);
let fine =
"f64 F(i32 a) {\n i64 b = a;\n f32 c = 2.0;\n Ticks t = 5s;\n return a;\n}\n";
assert!(codes(fine).is_empty(), "{:?}", codes(fine));
assert_eq!(
codes("void F() {\n Seconds s = 5t;\n}\n"),
[script_problem::TYPE_MISMATCH]
);
}
#[test]
fn null_needs_a_nullable() {
assert_eq!(
codes("record Pos(i32 X);\nvoid F() {\n Pos p = null;\n}\n"),
[script_problem::NULLABLE]
);
assert!(codes("record Pos(i32 X);\nvoid F() {\n Pos? p = null;\n}\n").is_empty());
let src = "record Pos(i32 X);\nint F(Pos? p) => p.X;\n";
assert_eq!(codes(src), [script_problem::NULLABLE]);
assert!(codes("record Pos(i32 X);\nint F(Pos? p) => p?.X ?? 0;\n").is_empty());
}
#[test]
fn members_and_calls_are_the_types() {
let src = "record Pos(i32 X) {\n i32 Twice() => X * 2;\n}\nint F(Pos p) => p.Y;\n";
assert_eq!(codes(src), [script_problem::UNKNOWN_MEMBER]);
let src = "i32 Area(i32 w, i32 h) => w * h;\nint F() => Area(1);\n";
assert_eq!(codes(src), [script_problem::WRONG_ARGS]);
let src = "i32 Area(i32 w, i32 h) => w * h;\nint F() => Area(1, \"2\");\n";
assert_eq!(codes(src), [script_problem::TYPE_MISMATCH]);
let src = "component Charge(i32 capacity) {\n public i32 Energy = 0;\n}\n\
block Charger has Charge(10) { }\nint F(Charger c) => c.Energy + c.Charge.Energy;\n";
assert!(codes(src).is_empty(), "{:?}", codes(src));
}
fn game() -> ClassIndex {
let m = |name: &str, desc: &str| Member {
name: name.into(),
desc: desc.into(),
access: access::PUBLIC,
};
let class = |name: &str, side, methods| Class {
name: name.into(),
side,
access: access::PUBLIC,
extends: None,
implements: vec![],
fields: vec![],
methods,
};
ClassIndex {
mc: "1.21.1".into(),
classes: vec![
class(
"net/minecraft/world/entity/player/Player",
Side::Shared,
vec![m("getHealth", "()F"), m("isAlive", "()Z")],
),
class("net/minecraft/client/Minecraft", Side::Client, vec![]),
],
}
}
#[test]
fn the_games_classes_are_a_yns_files_only() {
let index = game();
let yns = |src: &str| {
let src = format!(
"targets mc 1.21.1..1.21.1;\nusing Player = System.Game.World.Entity.Player.Player;\nusing System.Game.Client;\n{src}"
);
codes_of(check_with("a.yns", &src, Some(&index)).problems)
};
assert!(yns("f32 F(Player p) => p.Health;\nbool G(Player p) => p.IsAlive;\n").is_empty());
assert_eq!(
yns("i32 F(Player p) => p.Mana;\n"),
[script_problem::UNKNOWN_MEMBER]
);
assert_eq!(
yns("i32 F(Player p) => p.Health;\n"),
[script_problem::TYPE_MISMATCH]
);
assert_eq!(
yns("server void F(Minecraft mc) { }\n"),
[script_problem::WRONG_SIDE]
);
assert!(yns("client void F(Minecraft mc) { }\n").is_empty());
assert!(
yns("extend Player {\n public i32 Visits = 0;\n}\nint F(Player p) => p.Visits;\n")
.is_empty()
);
let ambiguous = "targets mc 1.21.1..1.21.1;\nusing System.Game.World.Entity.Player;\nfloat F(Player p) => 1;\n";
let found = codes_of(check_with("a.yns", ambiguous, Some(&index)).problems);
assert_eq!(found, [script_problem::AMBIGUOUS]);
assert!(yns("f32 F(System.Game.World.Entity.Player.Player p) => p.Health;\n").is_empty());
assert_eq!(
yns("i32 F(System.Yog.Player p) => p.Health;\n"),
[script_problem::UNKNOWN_MEMBER]
);
let ys = codes_of(check_with("a.ys", "i32 F(Player p) => p.Mana;\n", Some(&index)).problems);
assert_eq!(ys, [script_problem::UNKNOWN_MEMBER]);
let ys = codes_of(
check_with(
"a.ys",
"void F(System.Game.Client.Minecraft mc) { }\n",
Some(&index),
)
.problems,
);
assert_eq!(ys, [script_problem::TARGETS]);
}
#[test]
fn yogs_surface_is_typed() {
let fine = "server void F(Player p, World w) {\n bool given = p.GiveItem(\"minecraft:diamond\", 1);\n \
let info = p.Info();\n i32 level = info?.XpLevel ?? 0;\n bool flies = info!.MayFly;\n \
w.SetTime(6000);\n Chat.Broadcast(\"hello\");\n let online = Player.Online();\n}\n";
assert!(codes(fine).is_empty(), "{:?}", codes(fine));
assert_eq!(
codes("void F(Player p) {\n p.GiveItem(1);\n}\n"),
[script_problem::WRONG_ARGS]
);
assert_eq!(
codes("void F(Player p) {\n p.Fly();\n}\n"),
[script_problem::UNKNOWN_MEMBER]
);
assert_eq!(
codes("void F(Player p) {\n i32 k = p.Kick(\"bye\");\n}\n"),
[script_problem::TYPE_MISMATCH]
);
assert_eq!(
codes("i32 F(Player p) => p.Info()!.Mana;\n"),
[script_problem::UNKNOWN_MEMBER]
);
assert_eq!(
codes("void F() {\n Chat.Shout(\"hi\");\n}\n"),
[script_problem::UNKNOWN_MEMBER]
);
}
#[test]
fn namespaces_and_usings_work_as_in_csharp() {
let src = "namespace Charge.Machines;\n\nrecord Pos(i32 X);\nint F(Pos p) => p.X;\nint G(Charge.Machines.Pos p) => p.Y;\n";
assert_eq!(codes(src), [script_problem::UNKNOWN_MEMBER]);
let src = "namespace Charge {\n record Pos(i32 X);\n}\nnamespace Other {\n using Charge;\n i32 F(Pos p) => p.Y;\n}\n";
assert_eq!(codes(src), [script_problem::UNKNOWN_MEMBER]);
let src = "using Y = System.Yog;\nusing static System.Yog.Chat;\nvoid F(Y.Player p) {\n bool sent = Broadcast(\"hi\");\n Broadcast(1, 2);\n}\n";
assert_eq!(codes(src), [script_problem::WRONG_ARGS]);
}
#[test]
fn events_work_as_in_csharp() {
let fine = "mod M {\n M() {\n Player.Join += e => Chat.Broadcast(e.PlayerName);\n \
Block.Break += OnBreak;\n }\n void OnBreak(Block.BreakArgs e) { }\n}\n";
assert!(codes(fine).is_empty(), "{:?}", codes(fine));
assert_eq!(
codes("mod M {\n M() {\n Player.Join += e => e.Mana;\n }\n}\n"),
[script_problem::UNKNOWN_MEMBER]
);
let src = "mod M {\n M() {\n Player.Join += OnBreak;\n }\n void OnBreak(Block.BreakArgs e) { }\n}\n";
assert_eq!(codes(src), [script_problem::TYPE_MISMATCH]);
let own = "class Battery {\n public event Action<i32> Charged;\n void Fill() {\n Charged += n => { i32 twice = n * 2; };\n \
Charged?.Invoke(3);\n Charged(\"three\");\n }\n}\n";
assert_eq!(codes(own), [script_problem::TYPE_MISMATCH]);
}
#[test]
fn classes_inherit_as_in_csharp() {
let types = "interface ITick { void Tick(); }\nclass Machine : ITick { public i32 Rate = 1; public void Tick() {} }\n\
class Battery : Machine { public i32 Boost() => base.Rate + Rate; }\nclass Other {}\n";
let ok = format!(
"{types}void F() {{ let mut b = new Battery(); i32 r = b.Rate; b.Tick(); Machine m = move b; ITick t = new Battery(); }}\n"
);
assert!(codes(&ok).is_empty(), "{:?}", codes(&ok));
let bad =
format!("{types}void F() {{ Battery b = new Machine(); Machine m = new Other(); }}\n");
assert_eq!(
codes(&bad),
[script_problem::TYPE_MISMATCH, script_problem::TYPE_MISMATCH]
);
}
#[test]
fn inheritance_follows_csharps_rules() {
let base = "interface ITick { void Tick(); }\nabstract class Machine { public abstract i32 Rate(); \
public virtual void Glow() {} }\nsealed class Final {}\n";
let ok = format!(
"{base}class Battery : Machine, ITick {{ public override i32 Rate() => 4; public override void Glow() {{}} \
public void Tick() {{}} }}\n"
);
assert!(codes(&ok).is_empty(), "{:?}", codes(&ok));
let cases = [
"class A : Machine, ITick {}",
"class A : ITick, Machine { public override i32 Rate() => 1; public void Tick() {} }",
"class A : Final {}",
"class A { public override void Glow() {} }",
"struct A : Final {}",
];
let counts = [2, 1, 1, 1, 1];
for (case, count) in cases.iter().zip(counts) {
let found = codes(&format!("{base}{case}\n"));
assert_eq!(found, vec![script_problem::INHERIT; count], "{case}");
}
assert_eq!(
codes(&format!("{base}void F() {{ let m = new Machine(); }}\n")),
[script_problem::INHERIT]
);
}
#[test]
fn enums_are_rusts() {
let mode = "enum Mode { Idle, Charging(i32 Rate), Full }\n";
let ok = format!(
"{mode}string F(Mode m) => m switch {{ Mode.Idle => \"idle\", Mode.Charging(let r) when r > 3 => \"fast\", \
Mode.Charging(let r) => \"slow\", Mode.Full => \"full\" }};\nMode G() => Mode.Charging(4);\n"
);
assert!(codes(&ok).is_empty(), "{:?}", codes(&ok));
assert_eq!(
codes(&format!("{mode}Mode G() => Mode.Charging(\"x\");\n")),
[script_problem::TYPE_MISMATCH]
);
assert_eq!(
codes(&format!(
"{mode}string F(Mode m) => m switch {{ Mode.Idle => \"a\", Mode.Charging(let r) when r > 3 => \"b\", Mode.Full => \"c\" }};\n"
)),
[script_problem::NOT_EXHAUSTIVE]
);
assert!(
codes(&format!(
"{mode}string F(Mode m) => m switch {{ Mode.Idle => \"a\", _ => \"b\" }};\n"
))
.is_empty()
);
assert_eq!(
codes(&format!(
"{mode}string F(Mode m) => m switch {{ Mode.Charging(let r) => r, _ => \"b\" }};\n"
)),
[script_problem::TYPE_MISMATCH]
);
}
#[test]
fn generics_are_csharps() {
let types = "class Box<T> { public T Value; public void Put(T v) {} public T Get() => Value; }\n\
static T Max<T>(T a, T b) => a;\n";
let ok = format!(
"{types}void F(Box<i32> b) {{ i32 v = b.Value; i32 g = b.Get(); b.Put(3); i32 m = Max(1, 2); string s = Max(\"a\", \"b\"); }}\n"
);
assert!(codes(&ok).is_empty(), "{:?}", codes(&ok));
let bad = format!(
"{types}void F(Box<i32> b) {{ string v = b.Value; b.Put(\"x\"); string m = Max(1, 2); }}\n"
);
assert_eq!(
codes(&bad),
[
script_problem::TYPE_MISMATCH,
script_problem::TYPE_MISMATCH,
script_problem::TYPE_MISMATCH
]
);
}
#[test]
fn values_copy_and_objects_move() {
let types = "record struct Pos(i32 X, i32 Y);\nstruct Bag(List<i32> items) { public List<i32> Items = items; }\n\
class Box {}\nstatic void Keep(Box b) {}\n";
assert!(
codes(&format!(
"{types}void F(Pos p) {{ let q = p; Pos r = p; }}\n"
))
.is_empty()
);
let bad = format!(
"{types}void F(Box b, Bag g, List<i32> xs) {{ let c = b; let h = g; let ys = xs; Keep(b); }}\n"
);
assert_eq!(codes(&bad), vec![script_problem::PASS; 4]);
let ok = format!("{types}void F(Box b, Box c) {{ let d = move b; Keep(move c); }}\n");
assert!(codes(&ok).is_empty(), "{:?}", codes(&ok));
}
#[test]
fn what_is_lent_read_only_isnt_changed() {
let types = "class Box { public i32 N; public readonly i32 Get() => N; public void Bump() { N += 1; } \
public readonly void Peek() { Bump(); } }\nreadonly struct P(i32 x) { public i32 X = x; public void M() { X = 1; } }\n";
let bad = format!(
"{types}void F(in Box i) {{ let b = new Box(); b.N = 1; b.Bump(); i.Bump(); i.N = 2; }}\n"
);
assert_eq!(
codes(&bad),
vec![script_problem::BORROW; 6],
"{:?}",
codes(&bad)
);
let ok = format!(
"{types}void F(in Box i, ref Box r) {{ let mut b = new Box(); b.N = 1; b.Bump(); i32 n = i.Get(); r.Bump(); }}\n"
);
assert_eq!(
codes(&ok),
vec![script_problem::BORROW; 2],
"{:?}",
codes(&ok)
);
}
#[test]
fn records_never_change() {
let types = "record Pos(i32 X, i32 Y);\n";
let src = format!("{types}void F(Pos p) {{ let q = p; let mut r = p; r.X = 2; }}\n");
assert_eq!(codes(&src), [script_problem::BORROW], "{:?}", codes(&src));
let own = "record Pos(i32 X) { public void Move() { X = 1; } }\n";
assert_eq!(codes(own), [script_problem::ASSIGN_TO_CONSTANT]);
}
#[test]
fn sending_to_the_server_is_declared() {
let src = "void F(List<u8> bytes) { bool sent = Net.SendToServer(\"m:c\", move bytes); }\n";
let undeclared = yog_script_analyze::check_in("a.ys", src, yog_script_analyze::Env::default());
assert_eq!(codes_of(undeclared.problems), [script_problem::UNDECLARED]);
let env = yog_script_analyze::Env {
to_server: true,
..Default::default()
};
let declared = yog_script_analyze::check_in("a.ys", src, env);
assert!(codes_of(declared.problems).is_empty());
}
#[test]
fn an_indexer_is_called_by_brackets() {
let grid = "class Grid {\n public i32 this[i32 i] { get => i * 2; }\n}\n";
let fine = format!("{grid}i32 F(Grid g) => g[3];\n");
assert!(codes(&fine).is_empty(), "{:?}", codes(&fine));
assert_eq!(
codes(&format!("{grid}i32 F(Grid g) => g[\"a\"];\n")),
[script_problem::TYPE_MISMATCH]
);
assert_eq!(
codes(&format!("{grid}string F(Grid g) => g[1];\n")),
[script_problem::TYPE_MISMATCH]
);
}
#[test]
fn a_get_only_indexer_isnt_written() {
let grid = "class Grid {\n public i32 this[i32 i] { get => i; }\n}\n";
assert_eq!(
codes(&format!("{grid}void F(Grid g) {{\n g[0] = 1;\n}}\n")),
[script_problem::ASSIGN_TO_CONSTANT]
);
let set = "class Grid {\n public i32 this[i32 i] { get => i; set { } }\n}\n";
let fine = format!("{set}void F(Grid g) {{\n g[0] = 1;\n}}\n");
assert!(codes(&fine).is_empty(), "{:?}", codes(&fine));
}
#[test]
fn a_setter_takes_its_value() {
let src = "class Grid {\n List<i32> _cells = [0, 0];\n public i32 this[i32 i] {\n get => _cells[i];\n set { _cells[i] = value; }\n }\n}\n";
assert!(codes(src).is_empty(), "{:?}", codes(src));
}