From 67fbefbb4f8b219821d111dc98257833a2921c99 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Sat, 20 Jan 2024 17:56:33 +0200 Subject: [PATCH] improve error handling --- .gitignore | 4 +- Makefile | 5 ++ README.md | 2 +- example.bean | 1 + grammar.pest | 2 + src/directives.rs | 21 +++++- src/main.rs | 23 ++++--- src/parser.rs | 166 +++++++++++++++++++++++++++++++++++++--------- src/utils.rs | 33 +++++++++ 9 files changed, 212 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 6985cf1..42e179b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -# Generated by Cargo +# Coverage files +*.profraw + # will have compiled files and executables debug/ target/ diff --git a/Makefile b/Makefile index d5ae1ff..7582daa 100644 --- a/Makefile +++ b/Makefile @@ -23,3 +23,8 @@ test: .PHONY: lint lint: cargo clippy + +.PHONY: coverage +coverage: + RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='test.profraw' cargo test + grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov diff --git a/README.md b/README.md index 8a0a3f4..819c5a1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Using [pest](https://pest.rs/) for parsing. Two useful links: - [pest bootstrap parsing](https://github.com/pest-parser/pest/tree/master/meta/src) - [playground](https://pest.rs/#editor) -Planned featuers: +Planned features: - [x] Parse beancount files - [x] Stricter transaction keywords - [x] Propagate line numbers for debugging diff --git a/example.bean b/example.bean index 67f1517..b664b3d 100644 --- a/example.bean +++ b/example.bean @@ -1,5 +1,6 @@ * Example beancount file + fo ** Random metadata stuff option "operating_currency" "GBP" 2000-01-01 custom "fava-option" "language" "en" diff --git a/grammar.pest b/grammar.pest index c8d0222..ef47b17 100644 --- a/grammar.pest +++ b/grammar.pest @@ -14,6 +14,7 @@ entry = _{ | transaction | COMMENT | space+ + | badline } heading = _{ "*" ~ anyline } @@ -64,4 +65,5 @@ tag = @{ "#" ~ ASCII_ALPHA_LOWER+ } link = @{ "^" ~ ASCII_ALPHA_LOWER+ } COMMENT = _{ NEWLINE? ~ space* ~ ";" ~ anyline } +badline = { (!NEWLINE ~ ANY)+ } anyline = _{ (!NEWLINE ~ ANY)* } diff --git a/src/directives.rs b/src/directives.rs index 2f55e27..16ae991 100644 --- a/src/directives.rs +++ b/src/directives.rs @@ -583,6 +583,23 @@ impl fmt::Display for Transaction { } } +#[derive(Debug)] +pub struct Badline { + line: usize, +} + +impl Badline { + pub fn new(line: usize) -> Self { + Self { line } + } +} + +impl fmt::Display for Badline { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Badline: L{line}", line=self.line) + } +} + pub enum Directive { Eoi(Eoi), ConfigCustom(ConfigCustom), @@ -655,8 +672,8 @@ mod tests { #[test] fn test_open() { let text = r#"2023-01-01 open Assets:Bank GBP"#; - let entries = parser::parse(&text); - let dirs = parser::consume(entries); + let entries = parser::parse(&text).unwrap(); + let (dirs, _) = parser::consume(entries); let date = NaiveDate::parse_from_str("2023-01-01", DATE_FMT).unwrap(); let a = &Open { date, diff --git a/src/main.rs b/src/main.rs index 5ce9480..3bea10a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,18 +29,25 @@ fn main() { } } -fn load(text: String) -> Vec { - let entries = parser::parse(&text); - let mut directives = parser::consume(entries); - parser::sort(&mut directives); - book::balance_transactions(&mut directives); - utils::print_directives(&directives); - directives +fn load(text: String) -> Result, parser::ParseError> { + let entries = parser::parse(&text)?; + let (dirs, bad) = parser::consume(entries); + if bad.len() > 0 { + utils::print_badlines(bad) + } + let mut dirs = dirs; + parser::sort(&mut dirs); + book::balance_transactions(&mut dirs); + utils::print_directives(&dirs); + Ok(dirs) } fn balance(path: &String) { let text = std::fs::read_to_string(path).expect("cannot read file"); - let directives = load(text); + let directives = load(text).unwrap_or_else(|e| { + println!("Error: something went wrong: {e}"); + std::process::exit(1); + }); let bals = balance::get_balances(directives); utils::print_bals(bals); // println!("{bals:?}"); diff --git a/src/parser.rs b/src/parser.rs index de25c98..667f025 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,46 +1,120 @@ use std::cmp::Ordering; +use std::fmt; +use pest::error::LineColLocation; use pest::iterators::Pairs; use pest::Parser; -use crate::directives; use crate::directives::Directive; +use crate::directives::{self, Badline}; use crate::grammar::{BeanParser, Rule}; +use crate::utils; -pub fn parse(data: &str) -> Pairs<'_, Rule> { - BeanParser::parse(Rule::root, data) - .expect("parse failed") - .next() - .unwrap() - .into_inner() // go inside the root element +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum ParseErrorType { + Parse, + Into, } -pub fn consume(entries: Pairs<'_, Rule>) -> Vec { - entries - .map(|entry| { - eprintln!("debug:\t{:?}\t{:?}", entry.as_rule(), entry.as_span(),); - match entry.as_rule() { - Rule::option => { - Directive::ConfigOption(directives::ConfigOption::from_entry(entry)) - } - Rule::custom => { - Directive::ConfigCustom(directives::ConfigCustom::from_entry(entry)) - } - Rule::commodity => Directive::Commodity(directives::Commodity::from_entry(entry)), - Rule::open => Directive::Open(directives::Open::from_entry(entry)), - Rule::close => Directive::Close(directives::Close::from_entry(entry)), - Rule::balance => Directive::Balance(directives::Balance::from_entry(entry)), - Rule::pad => Directive::Pad(directives::Pad::from_entry(entry)), - Rule::price => Directive::Price(directives::Price::from_entry(entry)), - Rule::document => Directive::Document(directives::Document::from_entry(entry)), - Rule::transaction => { - Directive::Transaction(directives::Transaction::from_entry(entry)) - } - Rule::EOI => Directive::Eoi(directives::Eoi::from_entry(entry)), - _ => unreachable!("no rule for this entry!"), - } - }) - .collect() +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct ParseError { + ty: ParseErrorType, + line: usize, + col: usize, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ParseError: ({ty:?}) L{line} C{col}", + ty = self.ty, + line = self.line, + col = self.col, + ) + } +} + +pub fn parse(data: &str) -> Result, ParseError> { + let mut entries = match BeanParser::parse(Rule::root, data) { + Ok(pairs) => Ok(pairs), + Err(error) => { + let (line, col) = match error.line_col { + LineColLocation::Pos(pos) => pos, + LineColLocation::Span(pos, _) => pos, + }; + let ty = ParseErrorType::Parse; + Err(ParseError { ty, line, col }) + } + }?; + match entries.next() { + Some(entry) => { + utils::print_pair(&entry, 0); + Ok(entry.into_inner()) + } + None => Err(ParseError { + ty: ParseErrorType::Into, + line: 0, + col: 0, + }), + } +} + +pub fn consume(entries: Pairs<'_, Rule>) -> (Vec, Vec) { + let mut bad: Vec = Vec::with_capacity(entries.len()); + let mut dirs: Vec = Vec::new(); + for entry in entries { + eprintln!("debug:\t{:?}\t{:?}", entry.as_rule(), entry.as_span(),); + match entry.as_rule() { + Rule::option => { + dirs.push(Directive::ConfigOption( + directives::ConfigOption::from_entry(entry), + )); + } + Rule::custom => { + dirs.push(Directive::ConfigCustom( + directives::ConfigCustom::from_entry(entry), + )); + } + Rule::commodity => { + dirs.push(Directive::Commodity(directives::Commodity::from_entry( + entry, + ))); + } + Rule::open => { + dirs.push(Directive::Open(directives::Open::from_entry(entry))); + } + Rule::close => { + dirs.push(Directive::Close(directives::Close::from_entry(entry))); + } + Rule::balance => { + dirs.push(Directive::Balance(directives::Balance::from_entry(entry))); + } + Rule::pad => { + dirs.push(Directive::Pad(directives::Pad::from_entry(entry))); + } + Rule::price => { + dirs.push(Directive::Price(directives::Price::from_entry(entry))); + } + Rule::document => { + dirs.push(Directive::Document(directives::Document::from_entry(entry))); + } + Rule::transaction => { + dirs.push(Directive::Transaction(directives::Transaction::from_entry( + entry, + ))); + } + Rule::EOI => { + dirs.push(Directive::Eoi(directives::Eoi::from_entry(entry))); + } + Rule::badline => { + let (line, _) = entry.line_col(); + bad.push(directives::Badline::new(line)); + } + _ => unreachable!("no rule for this entry!"), + }; + } + (dirs, bad) } pub fn sort(directives: &mut [Directive]) { @@ -49,3 +123,29 @@ pub fn sort(directives: &mut [Directive]) { other => other, }); } + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_parse() { + let text = r#"2023-01-01 open Assets:Bank GBP"#; + let entries = parse(&text).unwrap(); + let (dirs, _) = consume(entries); + let got = &dirs[0]; + match got { + Directive::Open(_) => (), + _ => panic!("Found wrong directive type"), + } + } + + #[test] + fn test_bad() { + let text = r#" + 2023-01-01 foo + "#; + let entries = parse(&text).unwrap(); + let (_, bad) = consume(entries); + assert!(bad.len() == 1); + } +} diff --git a/src/utils.rs b/src/utils.rs index b801452..f2005b3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,6 @@ +use crate::{grammar::Rule, directives::Badline}; +use pest::iterators::Pair; + use crate::directives::{AccBal, Directive}; pub fn print_directives(directives: &Vec) { @@ -15,6 +18,36 @@ pub fn print_bals(bals: AccBal) { } } +pub fn print_badlines(bad: Vec) { + for b in bad { + println!("{b}"); + } +} + +pub fn print_pair(pair: &Pair, depth: usize) { + if depth == 0 { + println!(" -- Debug full parse output"); + } + + let indent = " ".repeat(depth); + let inner_pairs: Vec> = pair.clone().into_inner().collect(); + + if inner_pairs.is_empty() { + // It's a leaf node + println!("{}{:?}: {}", indent, pair.as_rule(), pair.as_str()); + } else { + // Not a leaf node, just print the rule + println!("{}{:?}:", indent, pair.as_rule()); + // Recursively print inner pairs + for inner_pair in inner_pairs { + print_pair(&inner_pair, depth + 1); + } + } + if depth == 0 { + println!(" -- END Debug full parse output"); + } +} + #[cfg(test)] mod tests { use super::*;