Skip to main content

shape_ast/error/parse_error/
formatting.rs

1//! Error message formatting functions
2
3use super::{
4    ExpectedToken, IdentifierContext, MissingComponentKind, NumberError, ParseErrorKind,
5    StringDelimiter, TokenCategory, TokenInfo, TokenKind,
6};
7
8/// Format the main error message based on error kind (plain text, no colors)
9pub fn format_error_message(kind: &ParseErrorKind) -> String {
10    match kind {
11        ParseErrorKind::UnexpectedToken { found, expected } => {
12            let found_str = format_found_token(found);
13            let expected_str = format_expected_tokens(expected);
14            format!("expected {}, found {}", expected_str, found_str)
15        }
16
17        ParseErrorKind::UnexpectedEof { expected } => {
18            let expected_str = format_expected_tokens(expected);
19            format!("unexpected end of input, expected {}", expected_str)
20        }
21
22        ParseErrorKind::UnterminatedString { delimiter, .. } => {
23            let delim_name = match delimiter {
24                StringDelimiter::DoubleQuote => "double-quoted string",
25                StringDelimiter::SingleQuote => "single-quoted string",
26                StringDelimiter::Backtick => "template literal",
27            };
28            format!("unterminated {}", delim_name)
29        }
30
31        ParseErrorKind::UnterminatedComment { .. } => "unterminated block comment".to_string(),
32
33        ParseErrorKind::UnbalancedDelimiter { opener, found, .. } => match found {
34            Some(c) => format!(
35                "mismatched closing delimiter: expected `{}`, found `{}`",
36                matching_close(*opener),
37                c
38            ),
39            None => format!("unclosed delimiter `{}`", opener),
40        },
41
42        ParseErrorKind::InvalidNumber { text, reason } => {
43            let reason_str = match reason {
44                NumberError::MultipleDecimalPoints => "multiple decimal points",
45                NumberError::InvalidExponent => "invalid exponent",
46                NumberError::TrailingDecimalPoint => "trailing decimal point",
47                NumberError::LeadingZeros => "leading zeros not allowed",
48                NumberError::InvalidDigit(c) => {
49                    return format!("invalid digit `{}` in number `{}`", c, text);
50                }
51                NumberError::TooLarge => "number too large",
52                NumberError::Empty => "empty number",
53            };
54            format!("invalid number literal `{}`: {}", text, reason_str)
55        }
56
57        ParseErrorKind::ReservedKeyword { keyword, context } => {
58            let context_str = match context {
59                IdentifierContext::VariableName => "variable name",
60                IdentifierContext::FunctionName => "function name",
61                IdentifierContext::ParameterName => "parameter name",
62                IdentifierContext::PatternName => "pattern name",
63                IdentifierContext::TypeName => "type name",
64                IdentifierContext::PropertyName => "property name",
65            };
66            format!(
67                "`{}` is a reserved keyword and cannot be used as a {}",
68                keyword, context_str
69            )
70        }
71
72        ParseErrorKind::InvalidEscape { sequence, .. } => {
73            format!("unknown escape sequence `{}`", sequence)
74        }
75
76        ParseErrorKind::InvalidCharacter { char, codepoint } => {
77            if char.is_control() {
78                format!("invalid character U+{:04X}", codepoint)
79            } else {
80                format!("invalid character `{}`", char)
81            }
82        }
83
84        ParseErrorKind::MissingComponent { component, after } => {
85            let comp_str = match component {
86                MissingComponentKind::Semicolon => "`;`",
87                MissingComponentKind::ClosingBrace => "`}`",
88                MissingComponentKind::ClosingBracket => "`]`",
89                MissingComponentKind::ClosingParen => "`)`",
90                MissingComponentKind::FunctionBody => "function body",
91                MissingComponentKind::Expression => "expression",
92                MissingComponentKind::TypeAnnotation => "type annotation",
93                MissingComponentKind::Identifier => "identifier",
94                MissingComponentKind::Arrow => "`->`",
95                MissingComponentKind::Colon => "`:`",
96            };
97            match after {
98                Some(ctx) => format!("expected {} after {}", comp_str, ctx),
99                None => format!("expected {}", comp_str),
100            }
101        }
102
103        ParseErrorKind::Custom { message } => message.clone(),
104    }
105}
106
107fn format_found_token(found: &TokenInfo) -> String {
108    match &found.kind {
109        Some(TokenKind::Keyword(kw)) => format!("keyword `{}`", kw),
110        Some(TokenKind::EndOfInput) => "end of input".to_string(),
111        Some(TokenKind::Identifier) => format!("identifier `{}`", found.text),
112        Some(TokenKind::Number) => format!("number `{}`", found.text),
113        Some(TokenKind::String) => format!("string `{}`", found.text),
114        _ if found.text.is_empty() => "nothing".to_string(),
115        _ => format!("`{}`", found.text),
116    }
117}
118
119fn format_expected_tokens(expected: &[ExpectedToken]) -> String {
120    if expected.is_empty() {
121        return "something else".to_string();
122    }
123
124    let items: Vec<String> = expected
125        .iter()
126        .filter_map(|e| match e {
127            ExpectedToken::Literal(s) => Some(format!("`{}`", s)),
128            ExpectedToken::Rule(r) => {
129                let name = rule_to_friendly_name(r);
130                if name.is_empty() { None } else { Some(name) }
131            }
132            ExpectedToken::Category(c) => Some(category_to_string(*c)),
133        })
134        .collect();
135
136    if items.is_empty() {
137        return "valid syntax".to_string();
138    }
139
140    match items.len() {
141        1 => items[0].clone(),
142        2 => format!("{} or {}", items[0], items[1]),
143        _ => {
144            let last = items.last().unwrap();
145            let rest = &items[..items.len() - 1];
146            format!("{}, or {}", rest.join(", "), last)
147        }
148    }
149}
150
151fn category_to_string(c: TokenCategory) -> String {
152    match c {
153        TokenCategory::Expression => "an expression".to_string(),
154        TokenCategory::Statement => "a statement".to_string(),
155        TokenCategory::Type => "a type".to_string(),
156        TokenCategory::Pattern => "a pattern".to_string(),
157        TokenCategory::Identifier => "an identifier".to_string(),
158        TokenCategory::Literal => "a literal".to_string(),
159        TokenCategory::Operator => "an operator".to_string(),
160        TokenCategory::Delimiter => "a delimiter".to_string(),
161    }
162}
163
164/// Convert pest Rule names to user-friendly descriptions
165pub fn rule_to_friendly_name(rule: &str) -> String {
166    match rule {
167        "expression" | "primary_expr" | "postfix_expr" => "an expression".to_string(),
168        "statement" => "a statement".to_string(),
169        "ident" | "identifier" => "an identifier".to_string(),
170        "number" | "integer" => "a number".to_string(),
171        "string" => "a string".to_string(),
172        "function_def" => "a function definition".to_string(),
173        "variable_decl" => "a variable declaration".to_string(),
174        "type_annotation" => "a type annotation".to_string(),
175        "function_params" => "function parameters".to_string(),
176        "function_body" => "a function body `{ ... }`".to_string(),
177        "if_stmt" | "if_expr" => "an if statement".to_string(),
178        "for_loop" | "for_expr" => "a for loop".to_string(),
179        "while_loop" | "while_expr" => "a while loop".to_string(),
180        "return_stmt" => "a return statement".to_string(),
181        "query" => "a query".to_string(),
182        "find_query" => "a find query".to_string(),
183        "scan_query" => "a scan query".to_string(),
184        "array_literal" => "an array".to_string(),
185        "object_literal" => "an object".to_string(),
186        "import_stmt" => "an import statement".to_string(),
187        "pub_item" => "a pub item".to_string(),
188        "match_expr" => "a match expression".to_string(),
189        "match_arm" => "a match arm".to_string(),
190        "block_expr" => "a block `{ ... }`".to_string(),
191        "join_kind" => "`all`, `race`, `any`, or `settle`".to_string(),
192        "comptime_annotation_handler_phase" => "`pre` or `post`".to_string(),
193        "annotation_handler_kind" => "a handler kind (`on_define`, `before`, `after`, `metadata`, `comptime pre`, `comptime post`)".to_string(),
194        "return_type" => "a return type `-> Type`".to_string(),
195        "stream_def" => "a stream definition".to_string(),
196        "enum_def" => "an enum definition".to_string(),
197        "struct_type_def" => "a struct definition".to_string(),
198        "trait_def" => "a trait definition".to_string(),
199        "impl_block" => "an impl block".to_string(),
200        "EOI" => String::new(), // Don't show "expected end of input"
201        "WHITESPACE" | "COMMENT" => String::new(),
202        _ => String::new(), // Hide internal rules
203    }
204}
205
206/// Get the matching closing delimiter
207pub fn matching_close(open: char) -> char {
208    match open {
209        '(' => ')',
210        '[' => ']',
211        '{' => '}',
212        '<' => '>',
213        _ => open,
214    }
215}