workshop-rs 0.3.7

Canonical multi-locale Overwatch Workshop semantic core: catalog, parser, WIR, validation, emitter.
Documentation
use workshop_rs::catalog::{Catalog, Locale};
use workshop_rs::source::{FileId, Position, SourceFile, Span};
use workshop_rs::{parser, roundtrip};

fn catalog() -> Catalog {
    Catalog::builtin().expect("builtin catalog")
}

#[test]
fn raw_parse_retains_comments_and_attaches_inner_comments_by_span() {
    let source = "// file header\nrule (\"comments\") { // rule header\n    event { Ongoing - Global; } // event note\n    actions { Wait(1, Ignore Condition); } // action note\n}\n// file footer\n";
    let program = parser::parse(source, &catalog(), &Locale::new("en-US")).expect("parses");
    let document = program
        .source(workshop_rs::source::FileId::from_index(0))
        .expect("raw parsing retains source");
    assert_eq!(document.text(), source);
    let comments: Vec<_> = document.comments().collect();
    assert_eq!(comments.len(), 5);
    assert_eq!(comments[0].text(document), "// file header");
    assert_eq!(comments[4].text(document), "// file footer");

    let rule_comments: Vec<_> = document
        .comments_for(program.rule_span(0).expect("public rule span"))
        .collect();
    assert_eq!(rule_comments.len(), 3);
    assert_eq!(rule_comments[0].text(document), "// rule header");
    assert_eq!(rule_comments[2].text(document), "// action note");
}

#[test]
fn source_span_operations_fail_closed_for_another_file() {
    let source =
        "rule (\"one\") { event { Ongoing - Global; } actions { Wait(1, Ignore Condition); } }";
    let program = parser::parse(source, &catalog(), &Locale::new("en-US")).expect("parses");
    let document = program.source(FileId::from_index(0)).unwrap();
    let rule_span = program.rule_span(0).expect("public rule span");
    let foreign_span = Span::new(FileId::from_index(1), rule_span.start, rule_span.end);
    assert!(document.byte_range(foreign_span).is_none());
    assert!(document.comments_for(foreign_span).next().is_none());
    assert!(document.edit_span(foreign_span, "").is_err());
}

#[test]
fn late_source_attachment_preserves_bound_file_identity() {
    let source = "// late source\nrule (\"late\") { event { Ongoing - Global; } actions { Wait(1, Ignore Condition); } }";
    let mut program = workshop_rs::wir::Program::default();
    let file = program.add_file(SourceFile::new("late.ws"));
    program.files.get_mut(file).unwrap().set_source(source);

    let document = program.source(file).expect("late source attachment");
    let span = Span::new(file, Position::new(2, 1), Position::new(2, 5));
    assert_eq!(document.byte_range(span), Some(15..19));
    let edit = document.edit_span(span, "Rule").expect("bound span edit");
    assert!(
        document
            .apply(&[edit])
            .unwrap()
            .text()
            .contains("Rule (\"late\")")
    );
}

#[test]
fn disabled_rule_span_includes_modifier_and_intervening_comment() {
    let source = "disabled // modifier note\nrule (\"disabled\") { event { Ongoing - Global; } actions { Wait(1, Ignore Condition); } }";
    let program = parser::parse(source, &catalog(), &Locale::new("en-US")).expect("parses");
    assert!(program.rules[0].disabled);
    let document = program.source(FileId::from_index(0)).unwrap();
    let span = program.rule_span(0).expect("public rule span");
    assert_eq!(&document.text()[document.byte_range(span).unwrap()], source);
    assert_eq!(document.comments_for(span).count(), 1);
}

#[test]
fn source_edit_preserves_unrelated_trivia_and_reparses_semantics() {
    let source = "// retain this\nrule (\"edit\") {\n  event { Ongoing - Global; }\n  actions { Wait(1, Ignore Condition); } // retain this too\n}\n";
    let catalog = catalog();
    let program = parser::parse(source, &catalog, &Locale::new("en-US")).expect("parses");
    let span = program
        .action_argument_span(0, 0, 0)
        .expect("public action argument span");
    let edit = program.edit_source(span, "2").expect("public number edit");
    let document = program
        .source(workshop_rs::source::FileId::from_index(0))
        .unwrap();
    let updated = document.apply(&[edit]).expect("edit applies");
    assert!(updated.text().contains("// retain this"));
    assert!(updated.text().contains("// retain this too"));
    assert!(updated.text().contains("Wait(2, Ignore Condition)"));

    let reparsed = parser::parse(updated.text(), &catalog, &Locale::new("en-US"))
        .expect("edited source reparses");
    assert!(!roundtrip::equivalent(&program, &reparsed));
    assert!(reparsed.action_argument_span(0, 0, 0).is_some());
}

#[test]
fn synthesized_control_flow_markers_have_no_enclosing_block_span() {
    let source = "rule (\"control\") {\n    event { Ongoing - Global; }\n    actions {\n        If(True);\n            Wait(1, Ignore Condition);\n        Else;\n            Wait(2, Ignore Condition);\n        End;\n    }\n}\n";
    let program = parser::parse(source, &catalog(), &Locale::new("en-US")).expect("parses");

    assert!(program.action_span(0, 0).is_some());
    assert!(program.action_span(0, 1).is_some());
    assert!(program.action_span(0, 2).is_none());
    assert!(program.action_span(0, 3).is_some());
    assert!(program.action_span(0, 4).is_none());
    assert!(program.action_argument_span(0, 3, 0).is_some());
}

#[test]
fn unsupported_mixed_source_is_rejected_without_fabricated_workshop_semantics() {
    let mixed = "rule (\"raw\") { event { Ongoing - Global; } actions { Wait(1, Ignore Condition); } }\n@if deltin_only_construct\n";
    let error = parser::parse(mixed, &catalog(), &Locale::new("en-US"))
        .expect_err("mixed source is outside raw Workshop parsing");
    assert!(matches!(
        error,
        workshop_rs::WorkshopError::Malformed { .. }
    ));
}