Skip to main content

rexact_syntax/
flags.rs

1//! Dialect flags (FR-A03, docs/13 section 5).
2
3use crate::dialect::Dialect;
4use crate::error::{ErrorCode, RexactError};
5
6/// Set of semantic flags. Flags that do not change the language (ES `g`, `y`,
7/// `d`) are accepted by [`Flags::from_letters`] and reported as notes.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub struct Flags {
10    /// `i` case-insensitive.
11    pub i: bool,
12    /// `m` multiline.
13    pub m: bool,
14    /// `s` dot matches newline.
15    pub s: bool,
16    /// `u` Unicode.
17    pub u: bool,
18    /// `x` extended / verbose.
19    pub x: bool,
20    /// `a` ASCII-only classes.
21    pub a: bool,
22}
23
24impl Flags {
25    /// No flags.
26    pub fn none() -> Self {
27        Self::default()
28    }
29
30    /// Union of two flag sets.
31    pub fn union(self, o: Flags) -> Flags {
32        Flags {
33            i: self.i | o.i,
34            m: self.m | o.m,
35            s: self.s | o.s,
36            u: self.u | o.u,
37            x: self.x | o.x,
38            a: self.a | o.a,
39        }
40    }
41
42    /// `self` with the letters of `o` removed.
43    pub fn minus(self, o: Flags) -> Flags {
44        Flags {
45            i: self.i & !o.i,
46            m: self.m & !o.m,
47            s: self.s & !o.s,
48            u: self.u & !o.u,
49            x: self.x & !o.x,
50            a: self.a & !o.a,
51        }
52    }
53
54    fn set(&mut self, c: char) -> bool {
55        let slot = match c {
56            'i' => &mut self.i,
57            'm' => &mut self.m,
58            's' => &mut self.s,
59            'u' => &mut self.u,
60            'x' => &mut self.x,
61            'a' => &mut self.a,
62            _ => return false,
63        };
64        *slot = true;
65        true
66    }
67
68    /// Flags valid for `dialect`, as shown in `RX-P003`.
69    pub fn valid_letters(dialect: Dialect) -> &'static str {
70        match dialect {
71            Dialect::Ecma => "i m s u (g y d accepted and ignored)",
72            Dialect::Python => "i m s x a (u implicit)",
73            Dialect::Go => "i m s (U accepted and ignored)",
74            Dialect::Rust => "i m s u x a (a = ASCII mode, like (?-u))",
75            Dialect::Pcre2 => "i m s u x",
76            Dialect::Java => "i m s u x (u = UNICODE_CASE and UNICODE_CHARACTER_CLASS)",
77        }
78    }
79
80    /// Parse option letters for a dialect. Returns the flags and notes about
81    /// letters that were accepted but do not change the language.
82    pub fn from_letters(
83        dialect: Dialect,
84        letters: &str,
85    ) -> Result<(Flags, Vec<String>), RexactError> {
86        let bad = |why: String| {
87            let mut e = RexactError::new(ErrorCode::P003);
88            e.detail = Some(format!(
89                "{why}; valid flags: {}",
90                Flags::valid_letters(dialect)
91            ));
92            e
93        };
94        let mut f = Flags::none();
95        let mut notes = Vec::new();
96        let mut seen = String::new();
97        for c in letters.chars() {
98            if seen.contains(c) && dialect == Dialect::Ecma {
99                return Err(bad(format!("duplicate flag '{c}'")));
100            }
101            seen.push(c);
102            let allowed = match dialect {
103                Dialect::Ecma => matches!(c, 'i' | 'm' | 's' | 'u'),
104                Dialect::Python => matches!(c, 'i' | 'm' | 's' | 'x' | 'a'),
105                Dialect::Go => matches!(c, 'i' | 'm' | 's'),
106                Dialect::Rust => matches!(c, 'i' | 'm' | 's' | 'u' | 'x' | 'a'),
107                Dialect::Pcre2 | Dialect::Java => matches!(c, 'i' | 'm' | 's' | 'u' | 'x'),
108            };
109            if allowed {
110                f.set(c);
111            } else if dialect == Dialect::Ecma && matches!(c, 'g' | 'y' | 'd') {
112                notes.push(format!(
113                    "flag '{c}' does not change the language and is ignored"
114                ));
115            } else if dialect == Dialect::Ecma && c == 'v' {
116                let mut e = RexactError::new(ErrorCode::U007);
117                e.detail = Some("flag 'v' (set notation) is not yet supported".into());
118                return Err(e);
119            } else if dialect == Dialect::Go && c == 'U' {
120                notes
121                    .push("flag 'U' only swaps greediness and does not change the language".into());
122            } else if dialect == Dialect::Python && c == 'u' {
123                notes.push("flag 'u' is implicit for Python str patterns".into());
124            } else {
125                return Err(bad(format!(
126                    "flag '{c}' is not valid for {}",
127                    dialect.name()
128                )));
129            }
130        }
131        Ok((f, notes))
132    }
133}