1 unstable release
0.1.0 | Sep 8, 2024 |
---|
#829 in Rust patterns
Used in moon_script
80KB
427 lines
You are reading the documentation for simple_detailed_error version 1.0.0
This crate helps you creating errors by giving you the [SimpleErrorDetail] trait where you give
text indicating why an error happens and how to solve it, while still using a pattern that
easily allows you to tell the user information about said error, such as what happened, why,
how, where, how to solve it and its causes.
Guided and deep example with parsing errors of a scripting language
Say we are creating a scripting language inside rust where we receive a script like
if missing_variable > 0 { return missing_function(missing_variable); }
, this script has
two errors: The variable missing_variable doesn't exists, and the function
missing_function doesn't exist either, in this situation, we would like to show the user
an error message like this:
- Error: Couldn't compile code.
- Has: 2 explained causes.
- Causes:
- Cause nº 1 -
- At: if missing_variable > 0
- Error: Variable missing_variable doesn't exists.
- Solution: Declare it before using it, like this:
let missing_variable = your value
- Cause nº 2 -
- At: return missing_function(missing_variable);
- Error: Function missing_function doesn't exists.
- Solution: Implement an missing_function function, like this:
fn missing_function(...) { ...your code here... }
Defining structs that explain errors
For now, let's declare an enum that would represent all the errors we want (They can be different structs, but to make it comfortable, we are going to use three variants), for example:
#[derive(Debug)]
enum CompilationError<'code_input>{
MissingVariable { variable_name : &'code_input str }, // This represents when a non-existing
// variable is referenced
MissingFunction { function_name : &'code_input str }, // This represents when a non-declared
// function tries to get called
RootCompilationError // This isn't really an error, is just one representation for saying
// 'hey, there are errors in your compilation'
}
Now let's just implement [SimpleErrorDetail] on this enum, this makes it so we have to implement a function where we return a [SimpleErrorExplanation] where we can give an explanation and a solution for the error using the functions SimpleErrorExplanation::solution and SimpleErrorExplanation::explanation (Note: You are not forced to give neither the solution nor the explanation, but is is highly advised, as they should help your users):
use simple_detailed_error::{SimpleErrorExplanation, SimpleErrorDetail};
impl <'code_input> SimpleErrorDetail for CompilationError<'code_input>{
fn explain_error(&self) -> SimpleErrorExplanation {
match self{
CompilationError::MissingVariable{ variable_name } => {
SimpleErrorExplanation::new()
.explanation(format!("Variable {variable_name} doesn't exists."))
.solution(format!("Declare it before using it, like this:\nlet {variable_name} = *your value*"))
}
CompilationError::MissingFunction{ function_name } => {
SimpleErrorExplanation::new()
.explanation(format!("Function {function_name} doesn't exists."))
.solution(format!("Implement an is_odd function, like this:\nfn {function_name}(...) {{ ...your code here... }}"))
}
CompilationError::RootCompilationError => {
SimpleErrorExplanation::new().explanation("Couldn't compile code.")
}
}
}
}
#[derive(Debug)]
enum CompilationError<'code_input>{
MissingVariable {variable_name : &'code_input str},
MissingFunction {function_name : &'code_input str},
RootCompilationError
}
Creating error values and displaying them
Perfect! With this, our enum representing our errors now can use functions like SimpleErrorDetail::to_parsing_error, which turns our variant into a struct of [SimpleError] containing said variant and using is as a representation of an error whose explanation and solutions are those said when we implemented SimpleErrorDetail::explain_error.
The [SimpleError] struct is one that holds information about an error, such as why it happened, how to solve it, or where it happened, it can also hold other [SimpleError]s inside, this represents an error being caused by another error, or even by multiple errors, for example, we can add an error using SimpleError::add_cause or SimpleError::with_cause.
For now, we are going to create a variant of CompilationError::RootCompilationError
which will
hold our errors, and then we are going to stack it with the missing variable and the missing
function errors:
- Creating the missing variable error: This is simply constructing a
CompilationError::MissingVariable
variant, where the variable name is just a reference to where it says 'missing_variable' in the original input, and since this is a parsing error, we can also use the SimpleError::at to indicate a bigger string where the error is happening, we will use it to reference 'if missing_variable > 0'.
...Wait, we are using theat
function, but that's implemented for [SimpleError], why is it working then? Well, the trait [SimpleErrorDetail] implements plenty of functions of [SimpleError], this is so you can use your struct (In this caseCompilationError
) as you were using a value of type [SimpleError]. - Creating the missing function error: This isn't very different from our previous case,
we just constructing a
CompilationError::MissingFunction
, as for the functionat
, this time we will reference 'return missing_function(missing_variable);'. - Creating the Error root: This is something you'll perhaps never do in a real project,
but we are going to use a base error, in this case
CompilationError::RootCompilationError
where we will stack the errors using the SimpleError::with_cause function, stacking the missing variable and missing function errors, although this is just for showing it's functionality, you should just stack real causes.
Once done, we can just can print our [SimpleError] and it will result in the error stack shown earlier.
use simple_detailed_error::{SimpleErrorExplanation, SimpleErrorDetail};
let code_to_compile = "if missing_variable > 0 { return missing_function(missing_variable); }";
let missing_variable_error = CompilationError::MissingVariable {variable_name: &code_to_compile[3..19] }
.at(&code_to_compile[0..23]);
let missing_function_error = CompilationError::MissingFunction {function_name: &code_to_compile[33..49] }
.at(&code_to_compile[26..68]);
let errors_stacker = CompilationError::RootCompilationError
.with_cause(missing_variable_error).with_cause(missing_function_error);
assert_eq!(format!("{errors_stacker}"), "Error: Couldn't compile code.\nHas: 2 explained causes.\nCauses: \n - Cause nº 1 -\n - At: if missing_variable > 0\n - Error: Variable missing_variable doesn't exists.\n - Solution: Declare it before using it, like this:\n let missing_variable = *your value*\n \n - Cause nº 2 -\n - At: return missing_function(missing_variable);\n - Error: Function missing_function doesn't exists.\n - Solution: Implement an is_odd function, like this:\n fn missing_function(...) { ...your code here... }");
impl <'code_input> SimpleErrorDetail for CompilationError<'code_input>{
fn explain_error(&self) -> SimpleErrorExplanation {
match self{
CompilationError::MissingVariable{ variable_name } => {
SimpleErrorExplanation::new()
.explanation(format!("Variable {variable_name} doesn't exists."))
.solution(format!("Declare it before using it, like this:\nlet {variable_name} = *your value*"))
}
CompilationError::MissingFunction{ function_name } => {
SimpleErrorExplanation::new()
.explanation(format!("Function {function_name} doesn't exists."))
.solution(format!("Implement an is_odd function, like this:\nfn {function_name}(...) {{ ...your code here... }}"))
}
CompilationError::RootCompilationError => {
SimpleErrorExplanation::new().explanation("Couldn't compile code.")
}
}
}
}
#[derive(Debug)]
enum CompilationError<'code_input>{
MissingVariable {variable_name : &'code_input str},
MissingFunction {function_name : &'code_input str},
RootCompilationError
}
Feature 'colorization': Adding color and emphasis on errors
That was quite alright! But... the output could benefit from some colorization, what if the result looked more like this?
- Error: Couldn't compile code.
- Has: 2 explained causes.
- Causes:
- Cause nº 1 -
- At: if missing_variable > 0
- Error: Variable missing_variable doesn't exists.
- Solution: Declare it before using it, like this:
let missing_variable = your value
- Cause nº 2 -
- At: return missing_function(missing_variable);
- Error: Function missing_function doesn't exists.
- Solution: Declare it before using it, like this:
fn missing_function (...) { ...your code here... }
By dimming irrelevant parts and making the parts where the errors happen to be easier to see might direct the attention of the user to the error, this makes them to read less in plenty of times while they still all the relevant information.
- Colorizing explanations and solutions: With the [colored] crate you can colorize the
explanations and solutions, for example, in
Variable {variable_name} doesn't exist
it would be great if we could turn the variable name into a bold and red name, taking the user's attention directly to it, like 'Variable missing variable doesn't exist'.
For this, we can just replace.explanation(format!("Variable {variable_name} doesn't exists."))
for.explanation(format!("Variable {} doesn't exists.", variable_name.red().bold()))
. - Colorizing the input: The string_colorization::colorize takes an &str and then applies some
colors and stylizations to other &str of it that we tell, this means that if we had a &str that
is part of the &str taken as input, then we could colorize it!
For example, in out missing variable error, we used SimpleError::at, where the substring referenced is 'if missing_variable > 0', thing is, our missing variable variant has another substring whose value is 'missing_variable', this means that if we used SimpleErrorExplanation::colorization_marker like thisSimpleErrorExplanation::new() .colorization_marker(variable_name, foreground::Red + style::Bold)
, then 'if missing_variable > 0' would look like 'if missing_variable > 0'.
The down-side of this is that your struct or enum must have references to the original source, although this is often the case for many situations, like parsing values as in this situation.
We can also use SimpleErrorExplanation::whole_input_colorization to colorize everything written insideat
, for example,SimpleErrorExplanation::new() .complete_input_colorization(foreground::Blue + style::Italic + style::Dimmed)
will make it soif missing_variable > 0
looks blue, italic and dimmed, telling the user it is not important.
But... now 'if missing_variable > 0' looks like if missing_variable > 0, not like if missing_variable > 0, why is 'missing_variable' also dimmed? This is because the complete_input_colorization set it to dim, while the colorization_marker didn't override this, to avoid this, we can use string_colorization::style::Clear to remove all the stylization from complete_input_colorization, this makes it soSimpleErrorExplanation::new() .colorization_marker(variable_name, style::Clear + foreground::Red + style::Bold) .complete_input_colorization(foreground::Blue + style::Italic + style::Dimmed)
while turn 'if missing_variable > 0' into the desired if missing_variable > 0.
use colored::Colorize;
use string_colorization::{foreground, style};
use simple_detailed_error::{SimpleErrorDetail, SimpleErrorExplanation};
impl <'code_input> SimpleErrorDetail for CompilationError<'code_input>{
fn explain_error(&self) -> SimpleErrorExplanation {
match self{
CompilationError::MissingVariable{ variable_name } => {
SimpleErrorExplanation::new()
.colorization_marker(variable_name, style::Clear + foreground::Red + style::Bold)
.whole_input_colorization(foreground::Blue + style::Italic + style::Dimmed)
.explanation(format!("Variable {} doesn't exists.", variable_name.red().bold()))
.solution(format!("Declare it before using it, like this:\nlet {} = {}", variable_name.green(), "*your value*".italic()))
}
CompilationError::MissingFunction{ function_name } => {
SimpleErrorExplanation::new()
.colorization_marker(function_name, style::Clear + foreground::Red + style::Bold)
.whole_input_colorization(foreground::Blue + style::Italic + style::Dimmed)
.explanation(format!("Function {} doesn't exists.", function_name.red().bold()))
.solution(format!("Implement an {function_name} function, like this:\nfn {}(...) {{ ...{}... }}", function_name.green(), "*your code here*".italic() ))
}
CompilationError::RootCompilationError => {
SimpleErrorExplanation::new().explanation("Couldn't compile code.")
}
}
}
}
#[derive(Debug)]
enum CompilationError<'code_input>{
MissingVariable {variable_name : &'code_input str},
MissingFunction {function_name : &'code_input str},
RootCompilationError,
}
Great! Now everything is finished! This is the resulting code:
use colored::Colorize;
use string_colorization::{foreground, style};
use simple_detailed_error::{SimpleErrorExplanation, SimpleErrorDetail};
colored::control::set_override(true); // This forces the colorization to be applied, this should
// not appear in your code, is written here to force it
// for testing purposes to show you this code is correct.
let code_to_compile = "if missing_variable > 0 { return missing_function(missing_variable); }";
let missing_variable_error = CompilationError::MissingVariable {variable_name: &code_to_compile[3..19] }
.at(&code_to_compile[0..23]);
let missing_function_error = CompilationError::MissingFunction {function_name: &code_to_compile[33..49] }
.at(&code_to_compile[26..68]);
let errors_stacker = CompilationError::RootCompilationError
.with_cause(missing_variable_error).with_cause(missing_function_error);
assert_eq!(format!("{errors_stacker}"), "Error: Couldn't compile code.\nHas: 2 explained causes.\nCauses: \n - Cause nº 1 -\n - At: \u{1b}[34m\u{1b}[3m\u{1b}[2mif \u{1b}[0m\u{1b}[34m\u{1b}[3m\u{1b}[0m\u{1b}[34m\u{1b}[0m\u{1b}[31m\u{1b}[3m\u{1b}[2m\u{1b}[1mmissing_variable\u{1b}[0m\u{1b}[31m\u{1b}[3m\u{1b}[2m\u{1b}[0m\u{1b}[31m\u{1b}[3m\u{1b}[0m\u{1b}[31m\u{1b}[0m\u{1b}[34m\u{1b}[3m\u{1b}[2m > 0\u{1b}[0m\u{1b}[34m\u{1b}[3m\u{1b}[0m\u{1b}[34m\u{1b}[0m\n - Error: Variable \u{1b}[1;31mmissing_variable\u{1b}[0m doesn't exists.\n - Solution: Declare it before using it, like this:\n let \u{1b}[32mmissing_variable\u{1b}[0m = \u{1b}[3m*your value*\u{1b}[0m\n \n - Cause nº 2 -\n - At: \u{1b}[34m\u{1b}[3m\u{1b}[2mreturn \u{1b}[0m\u{1b}[34m\u{1b}[3m\u{1b}[0m\u{1b}[34m\u{1b}[0m\u{1b}[31m\u{1b}[3m\u{1b}[2m\u{1b}[1mmissing_function\u{1b}[0m\u{1b}[31m\u{1b}[3m\u{1b}[2m\u{1b}[0m\u{1b}[31m\u{1b}[3m\u{1b}[0m\u{1b}[31m\u{1b}[0m\u{1b}[34m\u{1b}[3m\u{1b}[2m(missing_variable);\u{1b}[0m\u{1b}[34m\u{1b}[3m\u{1b}[0m\u{1b}[34m\u{1b}[0m\n - Error: Function \u{1b}[1;31mmissing_function\u{1b}[0m doesn't exists.\n - Solution: Implement an missing_function function, like this:\n fn \u{1b}[32mmissing_function\u{1b}[0m(...) { ...\u{1b}[3m*your code here*\u{1b}[0m... }");
impl <'code_input> SimpleErrorDetail for CompilationError<'code_input>{
fn explain_error(&self) -> SimpleErrorExplanation {
match self{
CompilationError::MissingVariable{ variable_name } => {
SimpleErrorExplanation::new()
.colorization_marker(variable_name, style::Clear + foreground::Red + style::Bold)
.whole_input_colorization(foreground::Blue + style::Italic + style::Dimmed)
.explanation(format!("Variable {} doesn't exists.", variable_name.red().bold()))
.solution(format!("Declare it before using it, like this:\nlet {} = {}", variable_name.green(), "*your value*".italic()))
}
CompilationError::MissingFunction{ function_name } => {
SimpleErrorExplanation::new()
.colorization_marker(function_name, style::Clear + foreground::Red + style::Bold)
.whole_input_colorization(foreground::Blue + style::Italic + style::Dimmed)
.explanation(format!("Function {} doesn't exists.", function_name.red().bold()))
.solution(format!("Implement an {function_name} function, like this:\nfn {}(...) {{ ...{}... }}", function_name.green(), "*your code here*".italic() ))
}
CompilationError::RootCompilationError => {
SimpleErrorExplanation::new().explanation("Couldn't compile code.")
}
}
}
}
#[derive(Debug)]
enum CompilationError<'code_input>{
MissingVariable {variable_name : &'code_input str},
MissingFunction {function_name : &'code_input str},
RootCompilationError
}
Features
std
: Implements the Error trait for SimpleError, it might also be used for future implementations that might require targeting std.colorization
: Allows the colorization markers functions to be used on SimpleErrorExplanation, helping you to create beautiful colored error message to direct your user's attention.serde
: Implements Serialize and Deserialize on SimpleErrorDisplayInfo, this is useful for storing logs of errors, especially for auditing.
Currently, the std
and colorization
are enabled by default.
Dependencies
~0–9MB
~43K SLoC