Print parse errors with codespan-reporting
This commit is contained in:
parent
2f6911eeca
commit
badc0d7a9f
5 changed files with 150 additions and 36 deletions
42
src/cli.rs
42
src/cli.rs
|
|
@ -7,9 +7,9 @@ use codespan_reporting::files::SimpleFile;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
use crate::eval::{DateRange, Entry, EntryMode};
|
use crate::eval::{self, DateRange, Entry, EntryMode};
|
||||||
use crate::files::arguments::CliRange;
|
use crate::files::arguments::CliRange;
|
||||||
use crate::files::{self, FileSource, Files};
|
use crate::files::{self, FileSource, Files, ParseError};
|
||||||
|
|
||||||
use self::error::Error;
|
use self::error::Error;
|
||||||
use self::layout::line::LineLayout;
|
use self::layout::line::LineLayout;
|
||||||
|
|
@ -95,6 +95,24 @@ fn find_layout(
|
||||||
layout::layout(files, entries, range, now)
|
layout::layout(files, entries, range, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_eval_arg<T, R>(
|
||||||
|
name: &str,
|
||||||
|
text: &str,
|
||||||
|
eval: impl FnOnce(T) -> Result<R, eval::Error<()>>,
|
||||||
|
) -> Option<R>
|
||||||
|
where
|
||||||
|
T: FromStr<Err = ParseError<()>>,
|
||||||
|
{
|
||||||
|
match T::from_str(text) {
|
||||||
|
Ok(value) => match eval(value) {
|
||||||
|
Ok(result) => return Some(result),
|
||||||
|
Err(e) => crate::error::eprint_error(&SimpleFile::new(name, text), &e),
|
||||||
|
},
|
||||||
|
Err(e) => crate::error::eprint_error(&SimpleFile::new(name, text), &e),
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn run_command(
|
fn run_command(
|
||||||
opt: &Opt,
|
opt: &Opt,
|
||||||
files: &mut Files,
|
files: &mut Files,
|
||||||
|
|
@ -144,21 +162,11 @@ pub fn run() {
|
||||||
|
|
||||||
let now = find_now(&opt, &files);
|
let now = find_now(&opt, &files);
|
||||||
|
|
||||||
// Kinda ugly, but it can stay for now (until it grows at least).
|
let range = match parse_eval_arg("--range", &opt.range, |range: CliRange| {
|
||||||
let range = match CliRange::from_str(&opt.range) {
|
range.eval((), now.date())
|
||||||
Ok(range) => match range.eval((), now.date()) {
|
}) {
|
||||||
Ok(range) => range,
|
Some(range) => range,
|
||||||
Err(e) => {
|
None => process::exit(1),
|
||||||
eprintln!("Failed to evaluate --range:");
|
|
||||||
let file = SimpleFile::new("--range", &opt.range);
|
|
||||||
crate::error::eprint_error(&file, &e);
|
|
||||||
process::exit(1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to parse --range:\n{}", e.with_path("--range"));
|
|
||||||
process::exit(1)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = run_command(&opt, &mut files, range, now) {
|
if let Err(e) = run_command(&opt, &mut files, range, now) {
|
||||||
|
|
|
||||||
26
src/files.rs
26
src/files.rs
|
|
@ -8,7 +8,7 @@ use codespan_reporting::files::SimpleFiles;
|
||||||
use tzfile::Tz;
|
use tzfile::Tz;
|
||||||
|
|
||||||
use self::commands::{Command, Done, File, Log};
|
use self::commands::{Command, Done, File, Log};
|
||||||
pub use self::error::{Error, Result};
|
pub use self::error::{Error, ParseError, Result};
|
||||||
use self::primitives::Spanned;
|
use self::primitives::Spanned;
|
||||||
|
|
||||||
pub mod arguments;
|
pub mod arguments;
|
||||||
|
|
@ -162,9 +162,26 @@ impl Files {
|
||||||
file: path.clone(),
|
file: path.clone(),
|
||||||
error: e,
|
error: e,
|
||||||
})?;
|
})?;
|
||||||
|
let cs_id = self
|
||||||
|
.cs_files
|
||||||
|
.add(name.to_string_lossy().to_string(), content.clone());
|
||||||
|
|
||||||
// Using `name` instead of `path` for the unwrap below.
|
// Using `name` instead of `path` for the unwrap below.
|
||||||
let file = parse::parse(name, &content)?;
|
let file = match parse::parse(name, &content) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(error) => {
|
||||||
|
// Using a dummy file. This should be fine since we return an
|
||||||
|
// error immediately after and the user must never call `load`
|
||||||
|
// twice. Otherwise, we run the danger of overwriting a file
|
||||||
|
// with empty content.
|
||||||
|
self.files
|
||||||
|
.push(LoadedFile::new(name.to_owned(), cs_id, File::dummy()));
|
||||||
|
return Err(Error::Parse {
|
||||||
|
file: FileSource(self.files.len() - 1),
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let includes = file
|
let includes = file
|
||||||
.commands
|
.commands
|
||||||
|
|
@ -175,10 +192,7 @@ impl Files {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
loaded.insert(path.clone());
|
loaded.insert(path);
|
||||||
let cs_id = self
|
|
||||||
.cs_files
|
|
||||||
.add(path.to_string_lossy().to_string(), content);
|
|
||||||
self.files
|
self.files
|
||||||
.push(LoadedFile::new(name.to_owned(), cs_id, file));
|
.push(LoadedFile::new(name.to_owned(), cs_id, file));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::result;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
|
@ -5,7 +6,8 @@ use pest::iterators::Pair;
|
||||||
use pest::Parser;
|
use pest::Parser;
|
||||||
|
|
||||||
use super::commands::Delta;
|
use super::commands::Delta;
|
||||||
use super::parse::{self, Error, Result, Rule, TodayfileParser};
|
use super::parse::{self, Result, Rule, TodayfileParser};
|
||||||
|
use super::ParseError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CliDatum {
|
pub enum CliDatum {
|
||||||
|
|
@ -61,14 +63,15 @@ fn parse_cli_ident(p: Pair<'_, Rule>) -> Result<CliIdent> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for CliIdent {
|
impl FromStr for CliIdent {
|
||||||
type Err = Error;
|
type Err = ParseError<()>;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> result::Result<Self, ParseError<()>> {
|
||||||
let mut pairs = TodayfileParser::parse(Rule::cli_ident, s)?;
|
let mut pairs =
|
||||||
|
TodayfileParser::parse(Rule::cli_ident, s).map_err(|e| ParseError::new((), e))?;
|
||||||
let p = pairs.next().unwrap();
|
let p = pairs.next().unwrap();
|
||||||
assert_eq!(pairs.next(), None);
|
assert_eq!(pairs.next(), None);
|
||||||
|
|
||||||
parse_cli_ident(p)
|
parse_cli_ident(p).map_err(|e| ParseError::new((), e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,13 +135,14 @@ fn parse_cli_range(p: Pair<'_, Rule>) -> Result<CliRange> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for CliRange {
|
impl FromStr for CliRange {
|
||||||
type Err = Error;
|
type Err = ParseError<()>;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> result::Result<Self, ParseError<()>> {
|
||||||
let mut pairs = TodayfileParser::parse(Rule::cli_range, s)?;
|
let mut pairs =
|
||||||
|
TodayfileParser::parse(Rule::cli_range, s).map_err(|e| ParseError::new((), e))?;
|
||||||
let p = pairs.next().unwrap();
|
let p = pairs.next().unwrap();
|
||||||
assert_eq!(pairs.next(), None);
|
assert_eq!(pairs.next(), None);
|
||||||
|
|
||||||
parse_cli_range(p)
|
parse_cli_range(p).map_err(|e| ParseError::new((), e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -357,3 +357,14 @@ pub struct File {
|
||||||
pub contents: String,
|
pub contents: String,
|
||||||
pub commands: Vec<Command>,
|
pub commands: Vec<Command>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
/// Create an empty dummy file. This file should only be used as a
|
||||||
|
/// placeholder value.
|
||||||
|
pub fn dummy() -> Self {
|
||||||
|
Self {
|
||||||
|
contents: String::new(),
|
||||||
|
commands: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,85 @@ use std::{io, result};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
||||||
use codespan_reporting::term::Config;
|
use codespan_reporting::term::Config;
|
||||||
|
use pest::error::{ErrorVariant, InputLocation};
|
||||||
|
|
||||||
use crate::error::Eprint;
|
use crate::error::Eprint;
|
||||||
|
|
||||||
use super::primitives::Span;
|
use super::primitives::Span;
|
||||||
use super::{parse, FileSource, Files};
|
use super::{parse, FileSource, Files};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("{error}")]
|
||||||
|
pub struct ParseError<S> {
|
||||||
|
file: S,
|
||||||
|
error: parse::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ParseError<S> {
|
||||||
|
pub fn new(file: S, error: parse::Error) -> Self {
|
||||||
|
Self { file, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rule_name(rule: parse::Rule) -> String {
|
||||||
|
// TODO Rename rules to be more readable?
|
||||||
|
format!("{:?}", rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enumerate(rules: &[parse::Rule]) -> String {
|
||||||
|
match rules.len() {
|
||||||
|
0 => "something".to_string(),
|
||||||
|
1 => Self::rule_name(rules[0]),
|
||||||
|
n => {
|
||||||
|
let except_last = rules
|
||||||
|
.iter()
|
||||||
|
.take(n - 1)
|
||||||
|
.map(|rule| Self::rule_name(*rule))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
let last = Self::rule_name(rules[n - 1]);
|
||||||
|
format!("{} or {}", except_last, last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notes(&self) -> Vec<String> {
|
||||||
|
match &self.error.variant {
|
||||||
|
ErrorVariant::ParsingError {
|
||||||
|
positives,
|
||||||
|
negatives,
|
||||||
|
} => {
|
||||||
|
let mut notes = vec![];
|
||||||
|
if !positives.is_empty() {
|
||||||
|
notes.push(format!("expected {}", Self::enumerate(positives)))
|
||||||
|
}
|
||||||
|
if !negatives.is_empty() {
|
||||||
|
notes.push(format!("unexpected {}", Self::enumerate(negatives)))
|
||||||
|
}
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
ErrorVariant::CustomError { message } => vec![message.clone()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, F> Eprint<'a, F> for ParseError<F::FileId>
|
||||||
|
where
|
||||||
|
F: codespan_reporting::files::Files<'a>,
|
||||||
|
{
|
||||||
|
fn eprint<'f: 'a>(&self, files: &'f F, config: &Config) {
|
||||||
|
let range = match self.error.location {
|
||||||
|
InputLocation::Pos(at) => at..at,
|
||||||
|
InputLocation::Span((from, to)) => from..to,
|
||||||
|
};
|
||||||
|
let name = files.name(self.file).expect("file exists");
|
||||||
|
let diagnostic = Diagnostic::error()
|
||||||
|
.with_message(format!("Could not parse {}", name))
|
||||||
|
.with_labels(vec![Label::primary(self.file, range)])
|
||||||
|
.with_notes(self.notes());
|
||||||
|
Self::eprint_diagnostic(files, config, &diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Could not resolve {path}: {error}")]
|
#[error("Could not resolve {path}: {error}")]
|
||||||
|
|
@ -27,8 +100,11 @@ pub enum Error {
|
||||||
},
|
},
|
||||||
#[error("Could not determine local timezone: {error}")]
|
#[error("Could not determine local timezone: {error}")]
|
||||||
LocalTz { error: io::Error },
|
LocalTz { error: io::Error },
|
||||||
#[error("{0}")]
|
#[error("{error}")]
|
||||||
Parse(#[from] parse::Error),
|
Parse {
|
||||||
|
file: FileSource,
|
||||||
|
error: parse::Error,
|
||||||
|
},
|
||||||
#[error("Conflicting time zones {tz1} and {tz2}")]
|
#[error("Conflicting time zones {tz1} and {tz2}")]
|
||||||
TzConflict {
|
TzConflict {
|
||||||
file1: FileSource,
|
file1: FileSource,
|
||||||
|
|
@ -81,8 +157,9 @@ impl<'a> Eprint<'a, Files> for Error {
|
||||||
eprintln!("Could not determine local timezone:");
|
eprintln!("Could not determine local timezone:");
|
||||||
eprintln!(" {}", error);
|
eprintln!(" {}", error);
|
||||||
}
|
}
|
||||||
// TODO Format using codespan-reporting as well
|
Error::Parse { file, error } => {
|
||||||
Error::Parse(error) => eprintln!("{}", error),
|
ParseError::new(*file, error.clone()).eprint(files, config)
|
||||||
|
}
|
||||||
Error::TzConflict {
|
Error::TzConflict {
|
||||||
file1,
|
file1,
|
||||||
span1,
|
span1,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue