Skip to main content

rexact_syntax/
matrix.rs

1//! Syntax-support matrix data (docs/13, RULE-17).
2//!
3//! Status strings are exactly the cells of docs/13 (`S`, `R`, `U`, `–`, ...),
4//! in dialect order ES, PY, GO, RS, PC. `code` is the error a construct is
5//! rejected with when Rexact does not analyse it. A test fails if the rows
6//! drift from the documentation or if a rejection sample stops producing its
7//! code.
8
9use crate::dialect::Dialect;
10use crate::error::ErrorCode;
11
12/// One row of the matrix.
13#[derive(Debug, Clone, Copy)]
14pub struct MatrixRow {
15    /// Stable short identifier.
16    pub id: &'static str,
17    /// Status cells for ES, PY, GO, RS, PC.
18    pub status: [&'static str; 5],
19    /// Error code used when the construct is rejected.
20    pub code: Option<ErrorCode>,
21}
22
23const fn row(id: &'static str, status: [&'static str; 5], code: Option<ErrorCode>) -> MatrixRow {
24    MatrixRow { id, status, code }
25}
26
27const SSSSS: [&str; 5] = ["S", "S", "S", "S", "S"];
28const RSV: &str = "S in recent versions";
29
30/// All rows, in documentation order.
31pub const MATRIX: &[MatrixRow] = &[
32    row("literals-escapes", SSSSS, None),
33    row("any", SSSSS, None),
34    row("concat-alt", SSSSS, None),
35    row("classes", SSSSS, None),
36    row("class-escapes", SSSSS, None),
37    row("posix-classes", ["–", "–", "S", "S", "S"], None),
38    row("class-set-ops", ["–", "–", "–", "S", "–"], None),
39    row(
40        "unicode-properties",
41        ["R", "–", "S", "S", "S"],
42        Some(ErrorCode::U007),
43    ),
44    row("astral-escapes", ["R", "S", "S", "S", "S"], None),
45    row("quantifiers", SSSSS, None),
46    row("counted-quantifiers", SSSSS, None),
47    row(
48        "possessive",
49        ["–", RSV, "–", "–", "U"],
50        Some(ErrorCode::U004),
51    ),
52    row("groups", SSSSS, None),
53    row("named-groups", SSSSS, None),
54    row("inline-flag-groups", ["–", "S", "S", "S", "S"], None),
55    row("atomic", ["–", RSV, "–", "–", "U"], Some(ErrorCode::U004)),
56    row(
57        "conditional",
58        ["–", "U", "–", "–", "U"],
59        Some(ErrorCode::U003),
60    ),
61    row(
62        "branch-reset",
63        ["–", "–", "–", "–", "U"],
64        Some(ErrorCode::U007),
65    ),
66    row(
67        "recursion",
68        ["–", "–", "–", "–", "U"],
69        Some(ErrorCode::U002),
70    ),
71    row("callouts", ["–", "–", "–", "–", "U"], Some(ErrorCode::U006)),
72    row("anchors", SSSSS, None),
73    row("text-anchors", ["–", "S", "S", "S", "S"], None),
74    row("word-boundary", ["R", "R", "R", "R", "R"], None),
75    row(
76        "lookahead",
77        ["R", "R", "–", "–", "R"],
78        Some(ErrorCode::U005),
79    ),
80    row(
81        "lookbehind",
82        ["R", "R", "–", "–", "R"],
83        Some(ErrorCode::U005),
84    ),
85    row(
86        "backreferences",
87        ["U", "U", "–", "–", "U"],
88        Some(ErrorCode::U001),
89    ),
90    row("flag-i", SSSSS, None),
91    row("flag-m", SSSSS, None),
92    row("flag-s", SSSSS, None),
93    row(
94        "flag-u",
95        ["S", "implicit", "implicit", "implicit", "S"],
96        None,
97    ),
98    row("flag-x", ["–", "S", "–", "S", "S"], None),
99    row("flag-a", ["–", "S", "–", "S", "–"], None),
100    row(
101        "flags-gyd-v",
102        ["R", "–", "–", "–", "–"],
103        Some(ErrorCode::U007),
104    ),
105];
106
107/// Constructs that must be rejected, with the code that proves it. Used by
108/// the tests and available to `rexact info`.
109pub const REJECTION_SAMPLES: &[(Dialect, &str, ErrorCode)] = &[
110    (Dialect::Ecma, r"(a)\1", ErrorCode::U001),
111    (Dialect::Ecma, r"(?<n>a)\k<n>", ErrorCode::U001),
112    (Dialect::Python, r"(a)\1", ErrorCode::U001),
113    (Dialect::Python, r"(?P<n>a)(?P=n)", ErrorCode::U001),
114    (Dialect::Python, "(?>a+)b", ErrorCode::U004),
115    (Dialect::Python, "a*+", ErrorCode::U004),
116    (Dialect::Python, "(a)(?(1)b|c)", ErrorCode::U003),
117];
118
119#[cfg(test)]
120#[allow(clippy::expect_used)]
121mod tests {
122    use super::*;
123    use crate::flags::Flags;
124    use crate::parser::Pattern;
125
126    #[test]
127    fn rows_match_documentation_status_cells() {
128        let path = concat!(
129            env!("CARGO_MANIFEST_DIR"),
130            "/../../docs/13-syntax-support-matrix.md"
131        );
132        let doc = std::fs::read_to_string(path).expect("docs/13 readable");
133        let mut documented: Vec<Vec<String>> = Vec::new();
134        for line in doc.lines() {
135            // Escaped pipes inside backticks are part of the element text.
136            let line = line.replace("\\|", "\u{1}");
137            let cells: Vec<String> = line.split('|').map(|c| c.trim().to_string()).collect();
138            // cells: "", element, ES, PY, GO, RS, PC, notes, "" for 5-dialect tables
139            if cells.len() == 9
140                && !cells[1].is_empty()
141                && cells[1] != "Element"
142                && cells[1] != "Flag"
143                && !cells[1].starts_with("---")
144            {
145                documented.push(cells[2..7].to_vec());
146            }
147        }
148        let data: Vec<Vec<String>> = MATRIX
149            .iter()
150            .map(|r| r.status.iter().map(|s| s.to_string()).collect())
151            .collect();
152        assert_eq!(documented, data);
153    }
154
155    #[test]
156    fn ids_are_unique() {
157        let mut ids: Vec<_> = MATRIX.iter().map(|r| r.id).collect();
158        ids.sort_unstable();
159        ids.dedup();
160        assert_eq!(ids.len(), MATRIX.len());
161    }
162
163    #[test]
164    fn every_rejection_sample_yields_its_code() {
165        for (d, text, code) in REJECTION_SAMPLES {
166            let e = Pattern::parse(text, *d, Flags::none()).expect_err(text);
167            assert_eq!(e.kind, *code, "{} / {text}", d.name());
168        }
169    }
170}