feat(errors): improve error messages with example values

Add token type examples to make error messages more helpful. Created an
ExpectedToken enum to replace string literals for better type safety,
added example values for each token type, and enhanced error display to
show concrete examples of valid syntax.
This commit is contained in:
Khaïs COLIN 2025-06-03 19:12:50 +02:00
parent e78511f692
commit 567aa31c07
Signed by: logistic-bot
SSH key fingerprint: SHA256:RlpiqKeXpcPFZZ4y9Ou4xi2M8OhRJovIwDlbCaMsuAo
8 changed files with 105 additions and 25 deletions

View file

@ -47,10 +47,47 @@ impl Command {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExpectedToken {
Integer,
String,
Semicolon,
Statement,
MetaCommand,
EndOfFile,
}
impl ExpectedToken {
/// Returns an example value for this token type
pub fn example(&self) -> &'static str {
match self {
ExpectedToken::Integer => "42",
ExpectedToken::String => "\"example\"",
ExpectedToken::Semicolon => ";",
ExpectedToken::Statement => "select",
ExpectedToken::MetaCommand => ".exit",
ExpectedToken::EndOfFile => "<end of input>",
}
}
}
impl std::fmt::Display for ExpectedToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExpectedToken::Integer => write!(f, "integer"),
ExpectedToken::String => write!(f, "string"),
ExpectedToken::Semicolon => write!(f, "semicolon"),
ExpectedToken::Statement => write!(f, "statement"),
ExpectedToken::MetaCommand => write!(f, "meta command"),
ExpectedToken::EndOfFile => write!(f, "end of file"),
}
}
}
#[derive(Debug)]
pub enum CommandParseError {
Scan(ScanError),
UnexpectedToken(Token, &'static [&'static str]),
UnexpectedToken(Token, &'static [ExpectedToken]),
}
impl From<MetaCommand> for Command {

View file

@ -11,19 +11,41 @@ impl OSDBError for CommandParseError {
CommandParseError::Scan(x) => {
x.display(file, input);
}
CommandParseError::UnexpectedToken(token, items) => {
CommandParseError::UnexpectedToken(token, expected_tokens) => {
let location = (file, Into::<std::ops::Range<usize>>::into(&token.location));
Report::build(ReportKind::Error, location.clone())
let mut report = Report::build(ReportKind::Error, location.clone())
.with_message("unexpected token")
.with_label(
Label::new(location.clone())
.with_color(Color::Red)
.with_message(format!("found {token}")),
)
.with_note(format!("expected token type to be one of {items:?}"))
.finish()
.eprint((file, Source::from(input)))
.unwrap()
);
// If we have expected tokens, show an example for the first one
if let Some(first_expected) = expected_tokens.get(0) {
let example = first_expected.example();
// Add a note with all expected types
let expected_types: Vec<_> = expected_tokens
.iter()
.map(|t| format!("{}", t))
.collect();
// Use singular form when there's only one expected token type
match expected_types.as_slice() {
[single_type] => {
report = report.with_note(format!("expected: {}", single_type));
},
_ => {
report = report.with_note(format!("expected one of: {}", expected_types.join(", ")));
}
}
report =
report.with_help(format!("try a token of the expected type: {example}"))
}
report.finish().eprint((file, Source::from(input))).unwrap()
}
}
}

View file

@ -1,7 +1,7 @@
use std::collections::VecDeque;
use crate::{
command::{Command, CommandParseError},
command::{Command, CommandParseError, ExpectedToken},
statements::Statement,
tokens::{Location, Token, TokenData, tokenize},
};
@ -37,7 +37,7 @@ fn expect_semicolon(tokens: &mut VecDeque<crate::tokens::Token>) -> Result<(), C
}
_ => Err(CommandParseError::UnexpectedToken(
next_token.clone(),
&["semicolon"],
&[ExpectedToken::Semicolon],
)),
}
} else {
@ -51,7 +51,7 @@ fn expect_semicolon(tokens: &mut VecDeque<crate::tokens::Token>) -> Result<(), C
data: TokenData::EndOfFile,
lexeme: String::new(),
},
&["semicolon"],
&[ExpectedToken::Semicolon],
))
}
}
@ -101,14 +101,14 @@ pub fn parse(file: String, input: String) -> Result<Vec<Command>, Vec<CommandPar
TokenData::Int(_) => {
errs.push(CommandParseError::UnexpectedToken(
token,
&["statement", "meta command", "eof"],
&[ExpectedToken::Statement, ExpectedToken::MetaCommand, ExpectedToken::EndOfFile],
));
skip_to_next_statement(&mut tokens);
}
TokenData::String(_) => {
errs.push(CommandParseError::UnexpectedToken(
token,
&["statement", "meta command", "eof"],
&[ExpectedToken::Statement, ExpectedToken::MetaCommand, ExpectedToken::EndOfFile],
));
skip_to_next_statement(&mut tokens);
}
@ -135,13 +135,13 @@ fn parse_insert_command(
data: TokenData::EndOfFile,
lexeme: String::new(),
},
&["integer"],
&[ExpectedToken::Integer],
)
})?;
let id = match id_token.data {
TokenData::Int(id) => id,
_ => return Err(CommandParseError::UnexpectedToken(id_token, &["integer"])),
_ => return Err(CommandParseError::UnexpectedToken(id_token, &[ExpectedToken::Integer])),
};
// Parse the username (string)
@ -155,7 +155,7 @@ fn parse_insert_command(
data: TokenData::EndOfFile,
lexeme: String::new(),
},
&["string"],
&[ExpectedToken::String],
)
})?;
@ -164,7 +164,7 @@ fn parse_insert_command(
_ => {
return Err(CommandParseError::UnexpectedToken(
username_token,
&["string"],
&[ExpectedToken::String],
));
}
};
@ -180,13 +180,13 @@ fn parse_insert_command(
data: TokenData::EndOfFile,
lexeme: String::new(),
},
&["string"],
&[ExpectedToken::String],
)
})?;
let email = match email_token.data {
TokenData::String(email) => email,
_ => return Err(CommandParseError::UnexpectedToken(email_token, &["string"])),
_ => return Err(CommandParseError::UnexpectedToken(email_token, &[ExpectedToken::String])),
};
// Check for semicolon after the insert command

View file

@ -17,7 +17,7 @@ Err(
lexeme: "\"not_an_id\"",
},
[
"integer",
Integer,
],
),
],

View file

@ -15,7 +15,7 @@ Err(
lexeme: ";",
},
[
"string",
String,
],
),
],

View file

@ -15,7 +15,7 @@ Err(
lexeme: "",
},
[
"semicolon",
Semicolon,
],
),
],

View file

@ -15,7 +15,7 @@ Err(
lexeme: "",
},
[
"semicolon",
Semicolon,
],
),
],