shape_ast/error/parse_error/
formatting.rs1use super::{
4 ExpectedToken, IdentifierContext, MissingComponentKind, NumberError, ParseErrorKind,
5 StringDelimiter, TokenCategory, TokenInfo, TokenKind,
6};
7
8pub 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
164pub 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(), "WHITESPACE" | "COMMENT" => String::new(),
202 _ => String::new(), }
204}
205
206pub fn matching_close(open: char) -> char {
208 match open {
209 '(' => ')',
210 '[' => ']',
211 '{' => '}',
212 '<' => '>',
213 _ => open,
214 }
215}