rexact-syntax 0.2.0

Regex parsers for ECMAScript, Python, Go, Rust, PCRE2 and Java dialects, with an error catalogue
Documentation
//! Syntax-support matrix data (docs/13, RULE-17).
//!
//! Status strings are exactly the cells of docs/13 (`S`, `R`, `U`, `–`, ...),
//! in dialect order ES, PY, GO, RS, PC. `code` is the error a construct is
//! rejected with when Rexact does not analyse it. A test fails if the rows
//! drift from the documentation or if a rejection sample stops producing its
//! code.

use crate::dialect::Dialect;
use crate::error::ErrorCode;

/// One row of the matrix.
#[derive(Debug, Clone, Copy)]
pub struct MatrixRow {
    /// Stable short identifier.
    pub id: &'static str,
    /// Status cells for ES, PY, GO, RS, PC.
    pub status: [&'static str; 5],
    /// Error code used when the construct is rejected.
    pub code: Option<ErrorCode>,
}

const fn row(id: &'static str, status: [&'static str; 5], code: Option<ErrorCode>) -> MatrixRow {
    MatrixRow { id, status, code }
}

const SSSSS: [&str; 5] = ["S", "S", "S", "S", "S"];
const RSV: &str = "S in recent versions";

/// All rows, in documentation order.
pub const MATRIX: &[MatrixRow] = &[
    row("literals-escapes", SSSSS, None),
    row("any", SSSSS, None),
    row("concat-alt", SSSSS, None),
    row("classes", SSSSS, None),
    row("class-escapes", SSSSS, None),
    row("posix-classes", ["–", "–", "S", "S", "S"], None),
    row("class-set-ops", ["–", "–", "–", "S", "–"], None),
    row(
        "unicode-properties",
        ["R", "–", "S", "S", "S"],
        Some(ErrorCode::U007),
    ),
    row("astral-escapes", ["R", "S", "S", "S", "S"], None),
    row("quantifiers", SSSSS, None),
    row("counted-quantifiers", SSSSS, None),
    row(
        "possessive",
        ["–", RSV, "–", "–", "U"],
        Some(ErrorCode::U004),
    ),
    row("groups", SSSSS, None),
    row("named-groups", SSSSS, None),
    row("inline-flag-groups", ["–", "S", "S", "S", "S"], None),
    row("atomic", ["–", RSV, "–", "–", "U"], Some(ErrorCode::U004)),
    row(
        "conditional",
        ["–", "U", "–", "–", "U"],
        Some(ErrorCode::U003),
    ),
    row(
        "branch-reset",
        ["–", "–", "–", "–", "U"],
        Some(ErrorCode::U007),
    ),
    row(
        "recursion",
        ["–", "–", "–", "–", "U"],
        Some(ErrorCode::U002),
    ),
    row("callouts", ["–", "–", "–", "–", "U"], Some(ErrorCode::U006)),
    row("anchors", SSSSS, None),
    row("text-anchors", ["–", "S", "S", "S", "S"], None),
    row("word-boundary", ["R", "R", "R", "R", "R"], None),
    row(
        "lookahead",
        ["R", "R", "–", "–", "R"],
        Some(ErrorCode::U005),
    ),
    row(
        "lookbehind",
        ["R", "R", "–", "–", "R"],
        Some(ErrorCode::U005),
    ),
    row(
        "backreferences",
        ["U", "U", "–", "–", "U"],
        Some(ErrorCode::U001),
    ),
    row("flag-i", SSSSS, None),
    row("flag-m", SSSSS, None),
    row("flag-s", SSSSS, None),
    row(
        "flag-u",
        ["S", "implicit", "implicit", "implicit", "S"],
        None,
    ),
    row("flag-x", ["–", "S", "–", "S", "S"], None),
    row("flag-a", ["–", "S", "–", "S", "–"], None),
    row(
        "flags-gyd-v",
        ["R", "–", "–", "–", "–"],
        Some(ErrorCode::U007),
    ),
];

/// Constructs that must be rejected, with the code that proves it. Used by
/// the tests and available to `rexact info`.
pub const REJECTION_SAMPLES: &[(Dialect, &str, ErrorCode)] = &[
    (Dialect::Ecma, r"(a)\1", ErrorCode::U001),
    (Dialect::Ecma, r"(?<n>a)\k<n>", ErrorCode::U001),
    (Dialect::Python, r"(a)\1", ErrorCode::U001),
    (Dialect::Python, r"(?P<n>a)(?P=n)", ErrorCode::U001),
    (Dialect::Python, "(?>a+)b", ErrorCode::U004),
    (Dialect::Python, "a*+", ErrorCode::U004),
    (Dialect::Python, "(a)(?(1)b|c)", ErrorCode::U003),
];

#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
    use super::*;
    use crate::flags::Flags;
    use crate::parser::Pattern;

    #[test]
    fn rows_match_documentation_status_cells() {
        let path = concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../docs/13-syntax-support-matrix.md"
        );
        let doc = std::fs::read_to_string(path).expect("docs/13 readable");
        let mut documented: Vec<Vec<String>> = Vec::new();
        for line in doc.lines() {
            // Escaped pipes inside backticks are part of the element text.
            let line = line.replace("\\|", "\u{1}");
            let cells: Vec<String> = line.split('|').map(|c| c.trim().to_string()).collect();
            // cells: "", element, ES, PY, GO, RS, PC, notes, "" for 5-dialect tables
            if cells.len() == 9
                && !cells[1].is_empty()
                && cells[1] != "Element"
                && cells[1] != "Flag"
                && !cells[1].starts_with("---")
            {
                documented.push(cells[2..7].to_vec());
            }
        }
        let data: Vec<Vec<String>> = MATRIX
            .iter()
            .map(|r| r.status.iter().map(|s| s.to_string()).collect())
            .collect();
        assert_eq!(documented, data);
    }

    #[test]
    fn ids_are_unique() {
        let mut ids: Vec<_> = MATRIX.iter().map(|r| r.id).collect();
        ids.sort_unstable();
        ids.dedup();
        assert_eq!(ids.len(), MATRIX.len());
    }

    #[test]
    fn every_rejection_sample_yields_its_code() {
        for (d, text, code) in REJECTION_SAMPLES {
            let e = Pattern::parse(text, *d, Flags::none()).expect_err(text);
            assert_eq!(e.kind, *code, "{} / {text}", d.name());
        }
    }
}