open-cypher 0.5.0

An unofficial Rust parser for the openCypher query language.
Documentation
use open_cypher::{
    Diagnostic, DiagnosticCode, ParseErrors, Severity, Span, parse, parse_recovering,
};

const MAX_RECOVERY_DIAGNOSTICS: usize = 32;

#[test]
fn diagnostic_codes_are_stable_and_machine_readable() {
    assert_eq!(DiagnosticCode::InvalidToken.as_str(), "OCY-L001");
    assert_eq!(DiagnosticCode::UnexpectedToken.as_str(), "OCY-P001");
    assert_eq!(DiagnosticCode::UnexpectedEof.as_str(), "OCY-P002");
}

#[test]
fn parse_errors_are_nonempty_and_source_ordered() {
    assert!(ParseErrors::try_new(Vec::new()).is_none());

    let later = Diagnostic::new(
        DiagnosticCode::UnexpectedToken,
        Severity::Error,
        "later",
        Span::new(8, 9),
    );
    let earlier = Diagnostic::new(
        DiagnosticCode::InvalidToken,
        Severity::Error,
        "earlier",
        Span::new(1, 2),
    );
    let errors = ParseErrors::new(vec![later, earlier]);

    assert_eq!(errors.len(), 2);
    assert_eq!(errors.diagnostics()[0].message, "earlier");
    assert_eq!(errors.diagnostics()[1].message, "later");
}

#[test]
fn strict_parse_rejects_invalid_input() {
    let result = parse("MATCH (n RETURN n");
    assert!(result.is_err());
}

#[test]
fn recovering_parse_reports_a_useful_diagnostic() {
    let source = "MATCH (n RETURN n";
    let outcome = parse_recovering(source);
    let diagnostic = outcome
        .diagnostics
        .first()
        .expect("malformed input should produce a diagnostic");

    assert!(
        !diagnostic.message.trim().is_empty(),
        "diagnostic messages must be suitable for display"
    );
    assert!(
        !format!("{:?}", diagnostic.code).is_empty(),
        "diagnostics must carry a stable code"
    );
    assert!(
        diagnostic.primary_span.end <= source.len()
            && source.is_char_boundary(diagnostic.primary_span.start)
            && source.is_char_boundary(diagnostic.primary_span.end),
        "diagnostics must identify a valid UTF-8 source span: {:?}",
        diagnostic.primary_span
    );
}

#[test]
fn unexpected_eof_is_reported() {
    let outcome = parse_recovering("RETURN [1, 2,");

    assert!(!outcome.diagnostics.is_empty());
    assert!(
        outcome
            .diagnostics
            .iter()
            .any(|diagnostic| !diagnostic.message.trim().is_empty())
    );
}

#[test]
fn recovery_can_report_more_than_one_mistake() {
    let outcome = parse_recovering("MATCH (n RETURN n, WITH RETURN ]");

    assert!(
        outcome.diagnostics.len() >= 2,
        "expected recovery to continue after the first error: {:#?}",
        outcome.diagnostics
    );
}

#[test]
fn recovery_caps_diagnostics_on_adversarial_input() {
    let source = "RETURN ] ,".repeat(1_000);
    let outcome = parse_recovering(&source);

    assert!(
        outcome.diagnostics.len() <= MAX_RECOVERY_DIAGNOSTICS,
        "diagnostic floods must be capped"
    );
}

#[test]
fn diagnostics_are_deterministic() {
    let source = "MATCH (n WHERE n.value >= RETURN n";
    let first = parse_recovering(source);
    let second = parse_recovering(source);

    assert_eq!(first.diagnostics, second.diagnostics);
}

#[test]
fn unicode_before_an_error_is_safe_to_diagnose() {
    let outcome = parse_recovering("MATCH (café) RETURN café.");

    assert!(!outcome.diagnostics.is_empty());
    for diagnostic in outcome.diagnostics {
        assert!(!diagnostic.message.trim().is_empty());
    }
}

#[test]
fn rendered_diagnostics_include_stable_machine_and_human_context() {
    let source = "RETURN ]";
    let outcome = parse_recovering(source);
    let diagnostic = outcome
        .diagnostics
        .first()
        .expect("malformed input should produce a diagnostic");
    let rendered = diagnostic.render(source);

    assert!(rendered.contains(diagnostic.code.as_str()));
    assert!(rendered.contains(&diagnostic.message));
    assert!(rendered.contains("RETURN ]"));
}