Skip to main content

arity_parser/parser/
diagnostics.rs

1use crate::parser::lexer::{TokKind, Token};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct ParseDiagnostic {
5    pub message: String,
6    pub start: usize,
7    pub end: usize,
8}
9
10/// Reported when two expressions sit side by side in a statement list with no
11/// newline or `;` between them. R calls this "unexpected <constant>"; the
12/// wording here names the missing separator instead, matching the parser's
13/// other "expected ..." messages.
14pub(crate) const MISSING_STATEMENT_SEPARATOR: &str = "expected a newline or ';' between statements";
15
16pub(crate) fn push_diagnostic(
17    diagnostics: &mut Vec<ParseDiagnostic>,
18    message: &str,
19    start: usize,
20    end: usize,
21) {
22    diagnostics.push(ParseDiagnostic {
23        message: message.to_string(),
24        start,
25        end,
26    });
27}
28
29pub(crate) fn push_token_diagnostic(
30    diagnostics: &mut Vec<ParseDiagnostic>,
31    message: &str,
32    token: &Token,
33) {
34    push_diagnostic(diagnostics, message, token.start, token.end);
35}
36
37/// Explain rejected pasted characters even when structural recovery swallowed
38/// their tokens. Quoted names, strings, comments, and user operators have their
39/// own token kinds, so their contents never enter this check.
40pub(crate) fn refine_unicode_diagnostics(tokens: &[Token], diagnostics: &mut Vec<ParseDiagnostic>) {
41    let specific: Vec<_> = tokens
42        .iter()
43        .filter(|token| token.kind == TokKind::Unknown)
44        .filter_map(|token| {
45            let ch = token.text.chars().next()?;
46            let (description, advice) = match ch {
47                '\u{200b}'..='\u{200d}' | '\u{180e}' | '\u{feff}' => {
48                    ("zero-width character", "remove this character")
49                }
50                '\u{ad}' => ("invisible soft hyphen", "remove this character"),
51                '\u{34f}' => (
52                    "invisible combining grapheme joiner",
53                    "remove this character",
54                ),
55                '\u{61c}'
56                | '\u{200e}'..='\u{200f}'
57                | '\u{202a}'..='\u{202e}'
58                | '\u{2066}'..='\u{2069}' => {
59                    ("invisible direction control", "remove this character")
60                }
61                '\u{2060}' => ("invisible word joiner", "remove this character"),
62                '\u{2061}'..='\u{2064}' => ("invisible operator", "use an explicit R operator"),
63                '\u{a0}' | '\u{2007}' | '\u{202f}' => ("Unicode space", "use an ASCII space"),
64                '\u{85}' => ("Unicode next-line character", "use a newline"),
65                '\u{2028}' => ("Unicode line separator", "use a newline"),
66                '\u{2029}' => ("Unicode paragraph separator", "use a newline"),
67                '\u{2212}' => ("Unicode minus sign", "use ASCII '-'"),
68                _ => return None,
69            };
70            Some(ParseDiagnostic {
71                message: format!(
72                    "unexpected {description} U+{:04X} in R code; {advice}",
73                    ch as u32
74                ),
75                start: token.start,
76                end: token.end,
77            })
78        })
79        .collect();
80    if specific.is_empty() {
81        return;
82    }
83    // Lexer order makes binary search possible even when recovery emitted
84    // structural diagnostics out of source order. Replace generic messages at
85    // these spans instead of reporting the same offending character twice.
86    diagnostics.retain(|d| {
87        specific
88            .binary_search_by_key(&(d.start, d.end), |s| (s.start, s.end))
89            .is_err()
90    });
91    diagnostics.extend(specific);
92    diagnostics.sort_by_key(|d| (d.start, d.end));
93}