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:
parent
e78511f692
commit
567aa31c07
8 changed files with 105 additions and 25 deletions
25
notes.org
25
notes.org
|
|
@ -235,7 +235,7 @@ i will use rustyline, since it seems like the most feature-complete
|
||||||
insert <id:int> <username:string> <email:string>
|
insert <id:int> <username:string> <email:string>
|
||||||
** DONE parse row insert
|
** DONE parse row insert
|
||||||
* DONE separate statements with semicolons
|
* DONE separate statements with semicolons
|
||||||
* TODO this error message could be better
|
* DONE this error message could be better
|
||||||
#+begin example
|
#+begin example
|
||||||
Error: unexpected token
|
Error: unexpected token
|
||||||
╭─[ <stdin>:1:24 ]
|
╭─[ <stdin>:1:24 ]
|
||||||
|
|
@ -247,8 +247,29 @@ Error: unexpected token
|
||||||
│ Note: expected token type to be one of ["semicolon"]
|
│ Note: expected token type to be one of ["semicolon"]
|
||||||
───╯
|
───╯
|
||||||
#+end example
|
#+end example
|
||||||
* TODO correct all instances of <unknown> in locations
|
** plan
|
||||||
|
1. Create an example mapping system
|
||||||
|
- Define a mapping of token types to example values
|
||||||
|
- Example: "integer" → "42", "string" → "example", "semicolon" → ";"
|
||||||
|
2. Enhance CommandParseError
|
||||||
|
- Add a method to generate user-friendly error messages
|
||||||
|
- Include both the expected token type and concrete examples
|
||||||
|
3. Implementation approach
|
||||||
|
- Create a static lookup table or function that returns examples
|
||||||
|
- Extend existing error handling to include examples in messages
|
||||||
|
- Make sure the examples follow SQL syntax conventions
|
||||||
|
4. Error display refinement
|
||||||
|
- Update error_display.rs to include these examples
|
||||||
|
- Format error messages to show both what was expected and example syntax
|
||||||
|
5. Testing
|
||||||
|
- Add tests that verify the error messages include helpful examples
|
||||||
|
- Ensure examples are contextually appropriate
|
||||||
|
|
||||||
|
This will make errors like "expected semicolon" more helpful by showing "expected semicolon (example: ;)".
|
||||||
|
TODO * TODO correct all instances of <unknown> in locations
|
||||||
* TODO meta-commands must be followed by end-of-file
|
* TODO meta-commands must be followed by end-of-file
|
||||||
|
* TODO project code documentation
|
||||||
|
* TODO project usage documentation
|
||||||
* DONE in case of parse error, skip until next semicolon to better recover
|
* DONE in case of parse error, skip until next semicolon to better recover
|
||||||
* TODO serialize/deserialize row to/from raw bytes
|
* TODO serialize/deserialize row to/from raw bytes
|
||||||
** TODO look for best practices for creating binary formats
|
** TODO look for best practices for creating binary formats
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
#[derive(Debug)]
|
||||||
pub enum CommandParseError {
|
pub enum CommandParseError {
|
||||||
Scan(ScanError),
|
Scan(ScanError),
|
||||||
UnexpectedToken(Token, &'static [&'static str]),
|
UnexpectedToken(Token, &'static [ExpectedToken]),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MetaCommand> for Command {
|
impl From<MetaCommand> for Command {
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,41 @@ impl OSDBError for CommandParseError {
|
||||||
CommandParseError::Scan(x) => {
|
CommandParseError::Scan(x) => {
|
||||||
x.display(file, input);
|
x.display(file, input);
|
||||||
}
|
}
|
||||||
CommandParseError::UnexpectedToken(token, items) => {
|
CommandParseError::UnexpectedToken(token, expected_tokens) => {
|
||||||
let location = (file, Into::<std::ops::Range<usize>>::into(&token.location));
|
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_message("unexpected token")
|
||||||
.with_label(
|
.with_label(
|
||||||
Label::new(location.clone())
|
Label::new(location.clone())
|
||||||
.with_color(Color::Red)
|
.with_color(Color::Red)
|
||||||
.with_message(format!("found {token}")),
|
.with_message(format!("found {token}")),
|
||||||
)
|
);
|
||||||
.with_note(format!("expected token type to be one of {items:?}"))
|
|
||||||
.finish()
|
// If we have expected tokens, show an example for the first one
|
||||||
.eprint((file, Source::from(input)))
|
if let Some(first_expected) = expected_tokens.get(0) {
|
||||||
.unwrap()
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
command::{Command, CommandParseError},
|
command::{Command, CommandParseError, ExpectedToken},
|
||||||
statements::Statement,
|
statements::Statement,
|
||||||
tokens::{Location, Token, TokenData, tokenize},
|
tokens::{Location, Token, TokenData, tokenize},
|
||||||
};
|
};
|
||||||
|
|
@ -37,7 +37,7 @@ fn expect_semicolon(tokens: &mut VecDeque<crate::tokens::Token>) -> Result<(), C
|
||||||
}
|
}
|
||||||
_ => Err(CommandParseError::UnexpectedToken(
|
_ => Err(CommandParseError::UnexpectedToken(
|
||||||
next_token.clone(),
|
next_token.clone(),
|
||||||
&["semicolon"],
|
&[ExpectedToken::Semicolon],
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -51,7 +51,7 @@ fn expect_semicolon(tokens: &mut VecDeque<crate::tokens::Token>) -> Result<(), C
|
||||||
data: TokenData::EndOfFile,
|
data: TokenData::EndOfFile,
|
||||||
lexeme: String::new(),
|
lexeme: String::new(),
|
||||||
},
|
},
|
||||||
&["semicolon"],
|
&[ExpectedToken::Semicolon],
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,14 +101,14 @@ pub fn parse(file: String, input: String) -> Result<Vec<Command>, Vec<CommandPar
|
||||||
TokenData::Int(_) => {
|
TokenData::Int(_) => {
|
||||||
errs.push(CommandParseError::UnexpectedToken(
|
errs.push(CommandParseError::UnexpectedToken(
|
||||||
token,
|
token,
|
||||||
&["statement", "meta command", "eof"],
|
&[ExpectedToken::Statement, ExpectedToken::MetaCommand, ExpectedToken::EndOfFile],
|
||||||
));
|
));
|
||||||
skip_to_next_statement(&mut tokens);
|
skip_to_next_statement(&mut tokens);
|
||||||
}
|
}
|
||||||
TokenData::String(_) => {
|
TokenData::String(_) => {
|
||||||
errs.push(CommandParseError::UnexpectedToken(
|
errs.push(CommandParseError::UnexpectedToken(
|
||||||
token,
|
token,
|
||||||
&["statement", "meta command", "eof"],
|
&[ExpectedToken::Statement, ExpectedToken::MetaCommand, ExpectedToken::EndOfFile],
|
||||||
));
|
));
|
||||||
skip_to_next_statement(&mut tokens);
|
skip_to_next_statement(&mut tokens);
|
||||||
}
|
}
|
||||||
|
|
@ -135,13 +135,13 @@ fn parse_insert_command(
|
||||||
data: TokenData::EndOfFile,
|
data: TokenData::EndOfFile,
|
||||||
lexeme: String::new(),
|
lexeme: String::new(),
|
||||||
},
|
},
|
||||||
&["integer"],
|
&[ExpectedToken::Integer],
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let id = match id_token.data {
|
let id = match id_token.data {
|
||||||
TokenData::Int(id) => id,
|
TokenData::Int(id) => id,
|
||||||
_ => return Err(CommandParseError::UnexpectedToken(id_token, &["integer"])),
|
_ => return Err(CommandParseError::UnexpectedToken(id_token, &[ExpectedToken::Integer])),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the username (string)
|
// Parse the username (string)
|
||||||
|
|
@ -155,7 +155,7 @@ fn parse_insert_command(
|
||||||
data: TokenData::EndOfFile,
|
data: TokenData::EndOfFile,
|
||||||
lexeme: String::new(),
|
lexeme: String::new(),
|
||||||
},
|
},
|
||||||
&["string"],
|
&[ExpectedToken::String],
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -164,7 +164,7 @@ fn parse_insert_command(
|
||||||
_ => {
|
_ => {
|
||||||
return Err(CommandParseError::UnexpectedToken(
|
return Err(CommandParseError::UnexpectedToken(
|
||||||
username_token,
|
username_token,
|
||||||
&["string"],
|
&[ExpectedToken::String],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -180,13 +180,13 @@ fn parse_insert_command(
|
||||||
data: TokenData::EndOfFile,
|
data: TokenData::EndOfFile,
|
||||||
lexeme: String::new(),
|
lexeme: String::new(),
|
||||||
},
|
},
|
||||||
&["string"],
|
&[ExpectedToken::String],
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let email = match email_token.data {
|
let email = match email_token.data {
|
||||||
TokenData::String(email) => email,
|
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
|
// Check for semicolon after the insert command
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ Err(
|
||||||
lexeme: "\"not_an_id\"",
|
lexeme: "\"not_an_id\"",
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
"integer",
|
Integer,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Err(
|
||||||
lexeme: ";",
|
lexeme: ";",
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
"string",
|
String,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Err(
|
||||||
lexeme: "",
|
lexeme: "",
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
"semicolon",
|
Semicolon,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Err(
|
||||||
lexeme: "",
|
lexeme: "",
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
"semicolon",
|
Semicolon,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue