Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow multiple errors to be reported #45

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/bin/ambit/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,18 @@ fn ensure_paths_exist(force: bool) -> AmbitResult<()> {
// Fetch entries from config file and return as vector
fn get_config_entries(config_path: &AmbitPath) -> AmbitResult<Vec<Entry>> {
let content = config_path.as_string()?;
config::get_entries(content.chars().peekable())
.collect::<Result<Vec<_>, _>>()
.map_err(AmbitError::Parse)
let config = config::get_entries(content.chars().peekable());
// Partition the config into entries and errors.
let (entries, errors): (Vec<_>, Vec<_>) = config.partition(Result::is_ok);
if errors.is_empty() {
Ok(entries.into_iter().map(Result::unwrap).collect())
} else {
// There is at least one error in the configuration.
// There is no need to return any entries; simply return all the errors received.
Err(AmbitError::Parse(
errors.into_iter().map(Result::unwrap_err).collect(),
))
}
}

// Return if link_name is symlinked to target (link_name -> target).
Expand Down
1 change: 1 addition & 0 deletions src/config/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ impl Token {
}
}

#[derive(Debug)]
pub struct Lexer<I: Iterator<Item = char>> {
iter: Peekable<I>,
line: usize,
Expand Down
2 changes: 1 addition & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl From<ParseErrorType> for ParseError {
}
}

pub type ParseResult<T> = std::result::Result<T, ParseError>;
pub type ParseResult<T> = Result<T, ParseError>;

pub fn get_entries<I: Iterator<Item = char>>(char_iter: Peekable<I>) -> Parser<Lexer<I>> {
let lex = Lexer::new(char_iter);
Expand Down
86 changes: 78 additions & 8 deletions src/config/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@ impl<I: Iterator<Item = Token>> Iterator for Parser<I> {
let new = Entry::parse(&mut self.iter);
match new {
Err(mut e) => {
e.tok = self.iter.peek().cloned();
e.tok = self.iter.next();
while Entry::parse(&mut self.iter).is_err() {
// If an error has been encountered, continue iterating until a non-error entry is found.
// Contiguous errors are a by-product of the initial error and shouldn't be reported.
if self.iter.next().is_none() {
break;
}
}
Err(e)
}
Ok(p) => Ok(p),
Expand Down Expand Up @@ -305,12 +312,11 @@ mod tests {
Ok(parsed) => assert_eq!(parsed, ast),
}
}
fn fail(toks: &[Token], err: ParseError) {
fn fail(toks: &[Token], errors: Vec<ParseError>) {
// Take a vector of errors to check for multiple errors if needed.
let iter = toks.iter().cloned().peekable();
let res = Parser::new(iter)
.collect::<ParseResult<Vec<_>>>()
.unwrap_err();
assert_eq!(err, res);
let res: Vec<_> = Parser::new(iter).filter_map(|e| e.err()).collect();
assert_eq!(errors, res);
}

#[test]
Expand Down Expand Up @@ -533,11 +539,75 @@ mod tests {
fn semicolon_error() {
fail(
&toklist!["a"],
ParseError {
vec![ParseError {
tok: None, // `None` means it failed at EOF
ty: ParseErrorType::Expected(&[TokType::Semicolon]),
},
}],
);
}

#[test]
fn multiple_errors() {
// Here we are testing that multiple errors can be reported.
// Only one error should be reported per invalid entry.
// This should be done while still being able to parse valid entries.
let toks = &toklist![
// This first entry should be invalid.
".config/bspwm/",
TokType::LBrace,
"os",
TokType::LParen,
"linux",
TokType::RParen,
// Missing colon...
TokType::LBrace,
"host",
TokType::LParen,
"claby2",
TokType::Colon,
"a",
TokType::Comma,
"default",
TokType::Colon,
"b",
TokType::Comma,
TokType::RBrace,
TokType::RBrace,
TokType::MapsTo,
"c",
TokType::Semicolon,
// The following entry should be valid.
"yes",
TokType::Semicolon,
// The following entry should be invalid.
"file" // Missing semicolon...
];
let iter = toks.iter().cloned().peekable();
let (entries, errors): (Vec<_>, Vec<_>) = Parser::new(iter).partition(Result::is_ok);
let entries: Vec<_> = entries.into_iter().map(Result::unwrap).collect();
let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect();
// Check if the 'yes' entry passed to ensure that it isn't consumed accidentally.
assert_eq!(
entries,
vec![Entry {
left: Spec::from("yes"),
right: None,
},]
);
assert_eq!(
errors,
vec![
ParseError {
tok: Some(Token::new(TokType::LBrace, 0)),
ty: ParseErrorType::Expected(&[TokType::Colon]),
},
ParseError {
tok: None,
ty: ParseErrorType::Expected(&[TokType::Semicolon]),
}
]
);
}

// TODO: add more tests
}
9 changes: 7 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub enum AmbitError {
// TODO: As of now, a single ParseError is returned from config::get_entries
// Future changes may result in a Vec<ParseError> being returned.
// This should be taken care of.
Parse(config::ParseError),
Parse(Vec<config::ParseError>),
WalkDir(walkdir::Error),
// File error is encountered on failed file open operation
// Provides additional path information
Expand Down Expand Up @@ -46,7 +46,12 @@ impl Display for AmbitError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
AmbitError::Io(ref e) => e.fmt(f),
AmbitError::Parse(ref e) => e.fmt(f),
AmbitError::Parse(errors) => {
for e in errors.iter() {
e.fmt(f)?;
}
Ok(())
}
AmbitError::WalkDir(ref e) => e.fmt(f),
AmbitError::File { path, .. } => {
f.write_fmt(format_args!("File error with `{}`", path.display()))
Expand Down