rexact-syntax 0.2.0

Regex parsers for ECMAScript, Python, Go, Rust, PCRE2 and Java dialects, with an error catalogue
Documentation
//! Dialect flags (FR-A03, docs/13 section 5).

use crate::dialect::Dialect;
use crate::error::{ErrorCode, RexactError};

/// Set of semantic flags. Flags that do not change the language (ES `g`, `y`,
/// `d`) are accepted by [`Flags::from_letters`] and reported as notes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Flags {
    /// `i` case-insensitive.
    pub i: bool,
    /// `m` multiline.
    pub m: bool,
    /// `s` dot matches newline.
    pub s: bool,
    /// `u` Unicode.
    pub u: bool,
    /// `x` extended / verbose.
    pub x: bool,
    /// `a` ASCII-only classes.
    pub a: bool,
}

impl Flags {
    /// No flags.
    pub fn none() -> Self {
        Self::default()
    }

    /// Union of two flag sets.
    pub fn union(self, o: Flags) -> Flags {
        Flags {
            i: self.i | o.i,
            m: self.m | o.m,
            s: self.s | o.s,
            u: self.u | o.u,
            x: self.x | o.x,
            a: self.a | o.a,
        }
    }

    /// `self` with the letters of `o` removed.
    pub fn minus(self, o: Flags) -> Flags {
        Flags {
            i: self.i & !o.i,
            m: self.m & !o.m,
            s: self.s & !o.s,
            u: self.u & !o.u,
            x: self.x & !o.x,
            a: self.a & !o.a,
        }
    }

    fn set(&mut self, c: char) -> bool {
        let slot = match c {
            'i' => &mut self.i,
            'm' => &mut self.m,
            's' => &mut self.s,
            'u' => &mut self.u,
            'x' => &mut self.x,
            'a' => &mut self.a,
            _ => return false,
        };
        *slot = true;
        true
    }

    /// Flags valid for `dialect`, as shown in `RX-P003`.
    pub fn valid_letters(dialect: Dialect) -> &'static str {
        match dialect {
            Dialect::Ecma => "i m s u (g y d accepted and ignored)",
            Dialect::Python => "i m s x a (u implicit)",
            Dialect::Go => "i m s (U accepted and ignored)",
            Dialect::Rust => "i m s u x a (a = ASCII mode, like (?-u))",
            Dialect::Pcre2 => "i m s u x",
            Dialect::Java => "i m s u x (u = UNICODE_CASE and UNICODE_CHARACTER_CLASS)",
        }
    }

    /// Parse option letters for a dialect. Returns the flags and notes about
    /// letters that were accepted but do not change the language.
    pub fn from_letters(
        dialect: Dialect,
        letters: &str,
    ) -> Result<(Flags, Vec<String>), RexactError> {
        let bad = |why: String| {
            let mut e = RexactError::new(ErrorCode::P003);
            e.detail = Some(format!(
                "{why}; valid flags: {}",
                Flags::valid_letters(dialect)
            ));
            e
        };
        let mut f = Flags::none();
        let mut notes = Vec::new();
        let mut seen = String::new();
        for c in letters.chars() {
            if seen.contains(c) && dialect == Dialect::Ecma {
                return Err(bad(format!("duplicate flag '{c}'")));
            }
            seen.push(c);
            let allowed = match dialect {
                Dialect::Ecma => matches!(c, 'i' | 'm' | 's' | 'u'),
                Dialect::Python => matches!(c, 'i' | 'm' | 's' | 'x' | 'a'),
                Dialect::Go => matches!(c, 'i' | 'm' | 's'),
                Dialect::Rust => matches!(c, 'i' | 'm' | 's' | 'u' | 'x' | 'a'),
                Dialect::Pcre2 | Dialect::Java => matches!(c, 'i' | 'm' | 's' | 'u' | 'x'),
            };
            if allowed {
                f.set(c);
            } else if dialect == Dialect::Ecma && matches!(c, 'g' | 'y' | 'd') {
                notes.push(format!(
                    "flag '{c}' does not change the language and is ignored"
                ));
            } else if dialect == Dialect::Ecma && c == 'v' {
                let mut e = RexactError::new(ErrorCode::U007);
                e.detail = Some("flag 'v' (set notation) is not yet supported".into());
                return Err(e);
            } else if dialect == Dialect::Go && c == 'U' {
                notes
                    .push("flag 'U' only swaps greediness and does not change the language".into());
            } else if dialect == Dialect::Python && c == 'u' {
                notes.push("flag 'u' is implicit for Python str patterns".into());
            } else {
                return Err(bad(format!(
                    "flag '{c}' is not valid for {}",
                    dialect.name()
                )));
            }
        }
        Ok((f, notes))
    }
}