pomsky 0.12.0

A new regular expression language
Documentation
use pomsky_syntax::{
    Span,
    diagnose::{
        CharClassError, CharStringError, DeprecationWarning, ParseErrorKind, ParseWarningKind,
        RepetitionError,
    },
};

use super::{CompileErrorKind, IllegalNegationKind};

pub(super) fn get_parser_help(
    kind: &ParseErrorKind,
    slice: &str,
    span: &mut Span,
) -> Option<String> {
    match kind {
        ParseErrorKind::LexErrorWithMessage(msg) => msg.get_help(slice),
        ParseErrorKind::RangeIsNotIncreasing => {
            let dash_pos = slice.find('-').unwrap();
            let (part1, part2) = slice.split_at(dash_pos);
            let part2 = part2.trim_start_matches('-');
            Some(format!("Switch the numbers: {}-{}", part2.trim(), part1.trim()))
        }
        ParseErrorKind::RangeLeadingZeroesVariableLength => {
            fn get_number(s: &str) -> &str {
                let digits = s.trim_matches(|c| matches!(c, ' ' | '\'' | '"'));
                let removed_leading = digits.trim_start_matches('0');
                if removed_leading.is_empty() {
                    "0"
                } else {
                    removed_leading
                }
            }

            let dash_pos = slice.find('-').unwrap();
            let (part1, part2) = slice.split_at(dash_pos);
            let part2 = part2.trim_start_matches('-');

            Some(format!(
                "Precede with a repeated zero: '0'* range '{}'-'{}'",
                get_number(part1),
                get_number(part2)
            ))
        }
        ParseErrorKind::CharClass(CharClassError::UnknownNamedClass {
            extra_in_prefix: true,
            ..
        }) => Some("When using the `block:` or `blk:` prefix, the `In` at the beginning needs to be removed".into()),
        #[cfg(feature = "suggestions")]
        ParseErrorKind::CharClass(CharClassError::UnknownNamedClass {
            similar: Some(similar),
            ..
        }) => Some(format!("Perhaps you meant `{similar}`")),
        ParseErrorKind::CharClass(CharClassError::NonAscendingRange(c1, c2)) => {
            if c1 == c2 {
                Some(format!("Use a single character: '{c1}'"))
            } else {
                let dash_pos = slice.find('-').unwrap();
                let (part1, part2) = slice.split_at(dash_pos);
                let part2 = part2.trim_start_matches('-');
                Some(format!("Switch the characters: {}-{}", part2.trim(), part1.trim()))
            }
        }
        ParseErrorKind::CharClass(CharClassError::CaretInGroup) => {
            Some("Use `![...]` to negate a character class".into())
        }
        ParseErrorKind::CharString(CharStringError::TooManyCodePoints)
            if slice.trim_matches(&['"', '\''][..]).chars().all(|c| c.is_ascii_digit()) =>
        {
            Some(
                "Try a `range` expression instead:\n\
                https://pomsky-lang.org/docs/language-tour/ranges/"
                    .into(),
            )
        }
        ParseErrorKind::KeywordAfterLet(_) => Some("Use a different variable name".into()),
        ParseErrorKind::UnallowedMultiNot(n) => Some(if n % 2 == 0 {
            "The number of exclamation marks is even, so you can remove all of them".into()
        } else {
            "The number of exclamation marks is odd, so you can remove all of them but one".into()
        }),
        ParseErrorKind::LetBindingExists => Some("Use a different name".into()),
        ParseErrorKind::MissingLetKeyword => Some(format!("Try `let {slice} ...`")),
        ParseErrorKind::Repetition(RepetitionError::QmSuffix) => Some(
            "If you meant to make the repetition lazy, append the `lazy` keyword instead.\n\
                If this is intentional, consider adding parentheses around the inner repetition."
                .into(),
        ),
        ParseErrorKind::Repetition(RepetitionError::Multi) => {
            Some("Add parentheses around the first repetition.".into())
        }
        ParseErrorKind::LonePipe => Some("Add an empty string ('') to match nothing".into()),
        ParseErrorKind::InvalidEscapeInStringAt(offset) => {
            let span_start = span.range_unchecked().start;
            *span = Span::new(span_start + offset - 1, span_start + offset + 1);
            None
        }
        ParseErrorKind::MultipleStringsInTestCase => {
            Some(r#"Use `in "some string"` to match substrings in a haystack"#.into())
        }
        ParseErrorKind::RecursionLimit => Some(
            "Try a less nested expression. It helps to refactor it using variables:\n\
                https://pomsky-lang.org/docs/language-tour/variables/"
                .into(),
        ),
        _ => None,
    }
}

pub(crate) fn get_parse_warning_help(kind: &ParseWarningKind) -> Option<String> {
    let ParseWarningKind::Deprecation(d) = kind;
    match d {
        DeprecationWarning::ShorthandInRange(c) => {
            let (desc, name) = match c {
                '\n' => ("a line feed", "n"),
                '\r' => ("a carriage return", "r"),
                '\t' => ("a tab character", "t"),
                '\u{07}' => ("an alert/bell character", "a"),
                '\u{1b}' => ("an escape character", "e"),
                '\u{0c}' => ("a form feed", "f"),
                _ => return None,
            };
            Some(format!("This shorthand matches {desc}, not a '{name}'"))
        }
        _ => None,
    }
}

pub(super) fn get_compiler_help(kind: &CompileErrorKind, _span: Span) -> Option<String> {
    match kind {
        CompileErrorKind::UnknownVariable { found, .. }
            if found.starts_with('U') && found[1..].chars().all(|c| c.is_ascii_hexdigit()) =>
        {
            Some(format!("Perhaps you meant a code point: `U+{cp}`", cp = &found[1..]))
        }

        #[cfg(feature = "suggestions")]
        CompileErrorKind::UnknownVariable { similar: Some(similar), .. }
        | CompileErrorKind::UnknownReferenceName { similar: Some(similar), .. } => {
            Some(format!("Perhaps you meant `{similar}`"))
        }

        CompileErrorKind::EmptyClassNegated { group1, group2 } => Some(format!(
            "The group is empty because it contains both `{group1:?}` and `{group2:?}`, \
            which together match every code point",
        )),

        CompileErrorKind::NameUsedMultipleTimes(_) => {
            Some("Give this group a different name".into())
        }

        CompileErrorKind::UnknownReferenceNumber(0) => {
            Some("Capturing group numbers start with 1".into())
        }
        CompileErrorKind::RelativeRefZero => Some(
            "Perhaps you meant `::-1` to refer to the previous or surrounding capturing group"
                .into(),
        ),

        CompileErrorKind::DotNetNumberedRefWithMixedGroups => Some(
            "Use a named reference, or don't mix named and unnamed capturing groups".to_string(),
        ),
        CompileErrorKind::NegativeShorthandInAsciiMode | CompileErrorKind::UnicodeInAsciiMode => {
            Some("Enable Unicode for this expression".into())
        }
        CompileErrorKind::IllegalNegation { kind }
            if !matches!(kind, IllegalNegationKind::DotNetChar(_)) =>
        {
            Some(
                "Only the following expressions can be negated:\n\
                - character sets\n\
                - string literals and alternations that match exactly one code point\n\
                - lookarounds\n\
                - the `%` word boundary"
                    .to_string(),
            )
        }
        CompileErrorKind::BadIntersection => Some(
            "One character sets can be intersected.\n\
            Parentheses may be required to clarify the parsing order."
                .to_string(),
        ),
        CompileErrorKind::InfiniteRecursion => Some(
            "A recursive expression must have a branch that \
            doesn't reach the `recursion`, or can repeat 0 times"
                .to_string(),
        ),

        _ => None,
    }
}