yog-script-analyze 0.2.1

Yog Script (.ys, .yns) analyzed: its lexer, parser and syntax tree, names resolved, the class index model — what the build and the editors read a source with.
Documentation
//! Names resolved and misplaced words found (`yog_script_analyze::check`).

use yog_ids::script_problem;
use yog_script_analyze::check;
use yog_script_analyze::resolve::DefKind;

/// The type checker's problems.
const TYPES: [&str; 5] = [
    script_problem::TYPE_MISMATCH,
    script_problem::NULLABLE,
    script_problem::UNKNOWN_MEMBER,
    script_problem::WRONG_SIDE,
    script_problem::WRONG_ARGS,
];

fn codes(name: &str, src: &str) -> Vec<String> {
    check(name, src)
        .problems
        .into_iter()
        .filter_map(|d| d.code.map(|c| c.to_string()))
        .collect()
}

/// The grammar's complete examples (§7) and the small mod resolve without
/// a problem: nothing in them is out of place.
#[test]
fn the_examples_resolve_cleanly() {
    let doc = include_str!("../../../../docs/design/ys-grammar.md");
    let section = doc
        .split("## 7.")
        .nth(1)
        .and_then(|s| s.split("\n## ").next())
        .unwrap();
    let mut parts = section.split("```");
    parts.next();
    while let (Some(block), _) = (parts.next(), parts.next()) {
        let (lang, code) = block.split_once('\n').unwrap();
        // The examples also show parts of Yog's surface still to come
        // (`Level`, blocks as objects, opening screens): what the type
        // checker says of those isn't the resolver's to answer for.
        let checked = check(&format!("example.{}", lang.trim()), code);
        let problems: Vec<_> = checked
            .problems
            .into_iter()
            .filter(|d| !d.code.as_deref().is_some_and(|c| TYPES.contains(&c)))
            .collect();
        assert!(problems.is_empty(), "{lang}: {problems:#?}");
    }
    let glassy = check("glassy.ys", include_str!("fixtures/glassy.ys"));
    assert!(glassy.problems.is_empty(), "{:#?}", glassy.problems);
}

#[test]
fn a_name_declared_twice_is_found() {
    let src = "i32 Charge() => 1;\nint Charge() => 2;\n";
    assert_eq!(codes("a.ys", src), [script_problem::DUPLICATE]);
    // A block may bind a name again: the later one hides the earlier.
    assert!(
        codes(
            "a.ys",
            "void F() {\n    let x = 1;\n    let x = x + 1;\n}\n"
        )
        .is_empty()
    );
}

#[test]
fn break_belongs_in_a_loop() {
    assert_eq!(
        codes("a.ys", "void F() {\n    break;\n}\n"),
        [script_problem::OUTSIDE_LOOP]
    );
    assert!(
        codes(
            "a.ys",
            "void F() {\n    while (true) {\n        break;\n    }\n}\n"
        )
        .is_empty()
    );
    // A lambda's body is its own: the loop around it isn't.
    let src =
        "void F(List<i32> xs) {\n    while (true) {\n        xs.ForEach { break; };\n    }\n}\n";
    assert_eq!(codes("a.ys", src), [script_problem::OUTSIDE_LOOP]);
}

#[test]
fn it_and_this_belong_where_they_mean_something() {
    assert_eq!(
        codes("a.ys", "i32 F() => it;\n"),
        [script_problem::IT_OUTSIDE_LAMBDA]
    );
    assert!(
        codes(
            "a.ys",
            "List<i32> F(List<i32> xs) => xs.Select { it * 2 };\n"
        )
        .is_empty()
    );
    assert_eq!(
        codes("a.ys", "i32 F() => this;\n"),
        [script_problem::THIS_OUTSIDE_MEMBER]
    );
    assert!(
        codes(
            "a.ys",
            "record Pos(i32 X) {\n    i32 Twice() => this.X * 2;\n}\n"
        )
        .is_empty()
    );
}

#[test]
fn only_what_can_change_is_assigned() {
    let src = "const i32 Max = 3;\nvoid F() {\n    Max = 2;\n}\n";
    assert_eq!(codes("a.ys", src), [script_problem::ASSIGN_TO_CONSTANT]);
    let src = "void F(List<i32> xs) {\n    foreach (let x in xs) x = 1;\n}\n";
    assert_eq!(codes("a.ys", src), [script_problem::ASSIGN_TO_CONSTANT]);
    assert!(
        codes(
            "a.ys",
            "void F(i32 n) {\n    let mut a = 1;\n    a = 2;\n    n += 1;\n}\n"
        )
        .is_empty()
    );
    // `let` is never assigned again; `ref` lends only a `let mut`.
    let src = "void F() {\n    let a = 1;\n    a = 2;\n}\n";
    assert_eq!(codes("a.ys", src), [script_problem::ASSIGN_TO_CONSTANT]);
    let src = "void G(ref i32 x) {}\nvoid F() {\n    let a = 1;\n    G(ref a);\n}\n";
    assert_eq!(codes("a.ys", src), [script_problem::ASSIGN_TO_CONSTANT]);
}

#[test]
fn skip_belongs_in_a_before() {
    let before = "targets mc 1.20.1..26.2;\npatch Player {\n    before void Tick() {\n        skip;\n    }\n}\n";
    assert!(
        codes("a.yns", before).is_empty(),
        "{:?}",
        codes("a.yns", before)
    );
    let after = "targets mc 1.20.1..26.2;\npatch Player {\n    after void Tick() {\n        skip;\n    }\n}\n";
    assert_eq!(codes("a.yns", after), [script_problem::SKIP_OUTSIDE_BEFORE]);
}

#[test]
fn await_belongs_in_async() {
    let src = "void Arm() {\n    await Task.Delay(5s);\n}\n";
    assert_eq!(codes("a.ys", src), [script_problem::AWAIT_OUTSIDE_ASYNC]);
    assert!(codes("a.ys", "async void Arm() {\n    await Task.Delay(5s);\n}\n").is_empty());
}

/// Names in C#'s case, warned otherwise.
#[test]
fn names_follow_csharps_style() {
    let src = "i32 area(i32 W, i32 h) => W * h;\ninterface Chargeable { }\nclass Box<Item> { }\n";
    assert_eq!(
        codes("a.ys", src),
        [
            script_problem::NAMING,
            script_problem::NAMING,
            script_problem::NAMING,
            script_problem::NAMING
        ]
    );
    let fine = "i32 Area(i32 w, i32 h) => w * h;\ninterface IChargeable { }\nclass Box<TItem> { }\n\
                record Pos(i32 X, i32 Y);\nvoid F() {\n    let _ = 1;\n    let chargeLevel = 2;\n}\n";
    assert!(codes("a.ys", fine).is_empty(), "{:?}", codes("a.ys", fine));
}

#[test]
fn a_use_names_its_definition() {
    let src = "i32 Area(i32 w, i32 h) => w * h;\nint Big() => Area(10, 20);\n";
    let checked = check("a.ys", src);
    let at = src.find("Area(10").unwrap() as u32;
    let def = checked.symbols.def_at(at).unwrap();
    assert_eq!((def.name.as_str(), def.kind), ("Area", DefKind::Method));
    assert_eq!(def.span.start, src.find("Area").unwrap() as u32);
}