From bfb7c5a27b8155ead6a74d3413998973c2a8652f Mon Sep 17 00:00:00 2001 From: elkowar <5300871+elkowar@users.noreply.github.com> Date: Sun, 18 Jul 2021 19:48:16 +0200 Subject: [PATCH] add basic config structure parsing and add some validation, mostly to demonstrate --- examples/errors.rs | 17 ++++--- examples/validation.rs | 40 +++++++++++++++++ src/config/mod.rs | 3 ++ src/config/validate.rs | 42 ++++++++++++++++++ src/config/widget_definition.rs | 44 +++++++++++++++++++ src/config/widget_use.rs | 43 ++++++++++++++++++ src/error.rs | 34 +++++++++----- src/format_diagnostic.rs | 40 ++++++++++++----- src/parser/ast.rs | 24 +++++++--- src/parser/element.rs | 20 ++++++--- src/parser/lexer.rs | 8 ++-- src/parser/parser.lalrpop | 6 +-- ...nfig__parser__element__test__test.snap.new | 17 +++++++ .../eww_config__parser__test.snap.new | 8 ++++ 14 files changed, 300 insertions(+), 46 deletions(-) create mode 100644 examples/validation.rs create mode 100644 src/config/validate.rs create mode 100644 src/config/widget_definition.rs create mode 100644 src/config/widget_use.rs create mode 100644 src/parser/snapshots/eww_config__parser__element__test__test.snap.new create mode 100644 src/parser/snapshots/eww_config__parser__test.snap.new diff --git a/examples/errors.rs b/examples/errors.rs index 63b2655..9b329da 100644 --- a/examples/errors.rs +++ b/examples/errors.rs @@ -1,23 +1,28 @@ -use eww_config::{ast::*, config::*, format_diagnostic::ToDiagnostic}; +use eww_config::{ + config::*, + format_diagnostic::ToDiagnostic, + parser::{ast::*, element::FromAst}, +}; fn main() { let mut files = codespan_reporting::files::SimpleFiles::new(); let input = r#" - (heyho :foo { "foo \" } bar " } - :baz {(foo == bar ? 12.2 : 12)} + (heyho ; :foo { "foo \" } bar " } + ; :baz {(foo == bar ? 12.2 : 12)} (foo) + (defwidget foo [something bla] "foo") (baz))"#; let file_id = files.add("foo.eww", input); - let ast = eww_config::parse_string(file_id, input); - match ast.and_then(Element::::from_ast) { + let ast = eww_config::parser::parse_string(file_id, input); + match ast.and_then(eww_config::parser::element::Element::::from_ast) { Ok(ast) => { println!("{:?}", ast); } Err(err) => { dbg!(&err); - let diag = err.to_diagnostic(&files); + let diag = err.to_diagnostic(); use codespan_reporting::term; let config = term::Config::default(); let mut writer = term::termcolor::StandardStream::stderr(term::termcolor::ColorChoice::Always); diff --git a/examples/validation.rs b/examples/validation.rs new file mode 100644 index 0000000..f54311e --- /dev/null +++ b/examples/validation.rs @@ -0,0 +1,40 @@ +use eww_config::{ + config::{widget_definition::WidgetDefinition, widget_use::WidgetUse, *}, + error::AstError, + format_diagnostic::ToDiagnostic, + parser::{ast::*, element::FromAst}, +}; + +fn main() { + let mut files = codespan_reporting::files::SimpleFiles::new(); + + let input_use = r#" + (foo :something 12 + :bla "bruh" + "some text") + "#; + let input_def = r#" + (defwidget foo [something bla] "foo") + "#; + + let file_id_use = files.add("use.eww", input_use); + let file_id_def = files.add("def.eww", input_def); + let parsed_use = WidgetUse::from_ast(eww_config::parser::parse_string(file_id_use, input_use).unwrap()).unwrap(); + let parsed_def = WidgetDefinition::from_ast(eww_config::parser::parse_string(file_id_def, input_def).unwrap()).unwrap(); + let defs = maplit::hashmap! { + "foo".to_string() => parsed_def, + }; + match validate::validate(&defs, &parsed_use) { + Ok(ast) => { + println!("{:?}", ast); + } + Err(err) => { + let err = AstError::ValidationError(err); + let diag = err.to_diagnostic(); + use codespan_reporting::term; + let config = term::Config::default(); + let mut writer = term::termcolor::StandardStream::stderr(term::termcolor::ColorChoice::Always); + term::emit(&mut writer, &config, &files, &diag).unwrap(); + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index e69de29..1cd54e6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -0,0 +1,3 @@ +pub mod validate; +pub mod widget_definition; +pub mod widget_use; diff --git a/src/config/validate.rs b/src/config/validate.rs new file mode 100644 index 0000000..d35c0cc --- /dev/null +++ b/src/config/validate.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +use simplexpr::SimplExpr; + +use crate::{ + error::AstResult, + parser::{ + ast::{Ast, AstIterator, Span}, + element::{Element, FromAst}, + }, + spanned, + value::{AttrName, VarName}, +}; + +use super::{widget_definition::WidgetDefinition, widget_use::WidgetUse}; + +#[derive(Debug, thiserror::Error)] +pub enum ValidationError { + #[error("Unknown widget referenced: {1}")] + UnknownWidget(Span, String), + + #[error("Missing attribute `{arg_name}` in use of widget `{widget_name}`")] + MissingAttr { widget_name: String, arg_name: AttrName, arg_list_span: Span, use_span: Span }, +} + +pub fn validate(defs: &HashMap, content: &WidgetUse) -> Result<(), ValidationError> { + if let Some(def) = defs.get(&content.name) { + for expected in def.expected_args.iter() { + if !content.attrs.contains_key(expected) { + return Err(ValidationError::MissingAttr { + widget_name: def.name.to_string(), + arg_name: expected.clone(), + arg_list_span: def.args_span, + use_span: content.span, + }); + } + } + } else { + return Err(ValidationError::UnknownWidget(content.span, content.name.to_string())); + } + Ok(()) +} diff --git a/src/config/widget_definition.rs b/src/config/widget_definition.rs new file mode 100644 index 0000000..a2ffd9a --- /dev/null +++ b/src/config/widget_definition.rs @@ -0,0 +1,44 @@ +use std::collections::HashMap; + +use simplexpr::SimplExpr; + +use crate::{ + error::AstResult, + parser::{ + ast::{Ast, AstIterator, Span}, + element::{Element, FromAst}, + }, + spanned, + value::{AttrName, VarName}, +}; + +use super::widget_use::WidgetUse; +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct WidgetDefinition { + pub name: String, + pub expected_args: Vec, + pub widget: WidgetUse, + pub span: Span, + pub args_span: Span, +} + +impl FromAst for WidgetDefinition { + fn from_ast(e: Ast) -> AstResult { + let span = e.span(); + spanned!(e.span(), { + let list = e.as_list()?; + let mut iter = AstIterator::new(list.into_iter()); + + let (_, def_type) = iter.expect_symbol()?; + assert!(def_type == "defwidget"); + + let (_, name) = iter.expect_symbol()?; + let (args_span, expected_args) = iter.expect_array()?; + let expected_args = expected_args.into_iter().map(|x| x.as_symbol().map(AttrName)).collect::>()?; + let widget = iter.expect_any().and_then(WidgetUse::from_ast)?; + // TODO verify that this was the last element in the list + // iter.expect_done()?; + Self { name, expected_args, widget, span, args_span } + }) + } +} diff --git a/src/config/widget_use.rs b/src/config/widget_use.rs new file mode 100644 index 0000000..79949e3 --- /dev/null +++ b/src/config/widget_use.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use simplexpr::SimplExpr; + +use crate::{ + error::AstResult, + parser::{ + ast::{Ast, AstIterator, Span}, + element::{Element, FromAst}, + }, + spanned, + value::AttrName, +}; +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct WidgetUse { + pub name: String, + pub attrs: HashMap, + pub children: Vec, + pub span: Span, +} + +impl FromAst for WidgetUse { + fn from_ast(e: Ast) -> AstResult { + let span = e.span(); + spanned!(e.span(), { + if let Ok(text) = e.as_value_ref() { + Self { + name: "text".to_string(), + attrs: maplit::hashmap! { AttrName("text".to_string()) => SimplExpr::Literal(span.into(), text.clone()) }, + children: Vec::new(), + span, + } + } else { + let list = e.as_list()?; + let mut iter = AstIterator::new(list.into_iter()); + let (_, name) = iter.expect_symbol()?; + let attrs = iter.expect_key_values()?.into_iter().map(|(k, v)| (AttrName(k), v)).collect(); + let children = iter.map(WidgetUse::from_ast).collect::>>()?; + Self { name, attrs, children, span } + } + }) + } +} diff --git a/src/error.rs b/src/error.rs index 333dc58..a603734 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,9 @@ -use crate::parser::{ - ast::{Ast, AstType, Span}, - lexer, parse_error, +use crate::{ + config::validate::ValidationError, + parser::{ + ast::{Ast, AstType, Span}, + lexer, parse_error, + }, }; use codespan_reporting::{diagnostic, files}; use thiserror::Error; @@ -11,10 +14,15 @@ pub type AstResult = Result; pub enum AstError { #[error("Definition invalid")] InvalidDefinition(Option), - #[error("Expected a {1}, but got nothing")] - MissingNode(Option, AstType), + #[error("Expected another element, but got nothing")] + MissingNode(Option), #[error("Wrong type of expression: Expected {1} but got {2}")] WrongExprType(Option, AstType, AstType), + #[error("Expected to get a value, but got {1}")] + NotAValue(Option, AstType), + + #[error(transparent)] + ValidationError(#[from] ValidationError), #[error("Parse error: {source}")] ParseError { file_id: Option, source: lalrpop_util::ParseError }, @@ -24,8 +32,10 @@ impl AstError { pub fn get_span(&self) -> Option { match self { AstError::InvalidDefinition(span) => *span, - AstError::MissingNode(span, _) => *span, + AstError::MissingNode(span) => *span, AstError::WrongExprType(span, ..) => *span, + AstError::NotAValue(span, ..) => *span, + AstError::ValidationError(error) => None, // TODO none here is stupid AstError::ParseError { file_id, source } => file_id.and_then(|id| get_parse_error_span(id, source)), } } @@ -58,18 +68,18 @@ pub fn spanned(span: Span, err: impl Into) -> AstError { use AstError::*; match err.into() { AstError::InvalidDefinition(None) => AstError::InvalidDefinition(Some(span)), - AstError::MissingNode(None, x) => AstError::MissingNode(Some(span), x), + AstError::MissingNode(None) => AstError::MissingNode(Some(span)), AstError::WrongExprType(None, x, y) => AstError::WrongExprType(Some(span), x, y), x => x, } } pub trait OptionAstErrorExt { - fn or_missing(self, t: AstType) -> Result; + fn or_missing(self) -> Result; } impl OptionAstErrorExt for Option { - fn or_missing(self, t: AstType) -> Result { - self.ok_or(AstError::MissingNode(None, t)) + fn or_missing(self) -> Result { + self.ok_or(AstError::MissingNode(None)) } } @@ -87,7 +97,7 @@ impl> AstResultExt for Result { macro_rules! spanned { ($span:expr, $block:expr) => {{ let span = $span; - let result: Result<_, AstError> = try { $block }; - result.at(span) + let result: Result<_, crate::error::AstError> = try { $block }; + crate::error::AstResultExt::at(result, span) }}; } diff --git a/src/format_diagnostic.rs b/src/format_diagnostic.rs index d72c16a..263bdf2 100644 --- a/src/format_diagnostic.rs +++ b/src/format_diagnostic.rs @@ -16,8 +16,8 @@ macro_rules! gen_diagnostic { Diagnostic::error() $(.with_message($msg))? $(.with_labels(vec![ - Label::primary($span.2, $span.0..$span.1) - $(.with_message($label))? + Label::primary($span.2, $span.0..$span.1) + $(.with_message($label))? ]))? $(.with_notes(vec![$note]))? }; @@ -34,15 +34,29 @@ pub trait ToDiagnostic { impl ToDiagnostic for AstError { fn to_diagnostic(&self) -> Diagnostic { - let diag = Diagnostic::error(); - if let Some(span) = self.get_span() { - use lalrpop_util::ParseError::*; + if let AstError::ValidationError(error) = self { + match error { + crate::config::validate::ValidationError::UnknownWidget(span, name) => gen_diagnostic! { + msg = format!("No widget named {} exists", name), + label = span => "Used here", + }, + crate::config::validate::ValidationError::MissingAttr { widget_name, arg_name, arg_list_span, use_span } => { + let diag = gen_diagnostic! { + msg = format!("{}", error), + }; + diag.with_labels(vec![ + Label::secondary(use_span.2, use_span.0..use_span.1).with_message("Argument missing here"), + Label::secondary(arg_list_span.2, arg_list_span.0..arg_list_span.1).with_message("but is required here"), + ]) + } + } + } else if let Some(span) = self.get_span() { match self { AstError::InvalidDefinition(_) => todo!(), - AstError::MissingNode(_, expected) => gen_diagnostic! { - msg = format!("Missing {}", expected), - label = span => format!("Expected `{}` here", expected), + AstError::MissingNode(_) => gen_diagnostic! { + msg = "Expected another element", + label = span => "Expected another element here", }, AstError::WrongExprType(_, expected, actual) => gen_diagnostic! { @@ -50,14 +64,20 @@ impl ToDiagnostic for AstError { label = span => format!("Expected a `{}` here", expected), note = format!("Expected: {}\nGot: {}", expected, actual), }, + AstError::NotAValue(_, actual) => gen_diagnostic! { + msg = format!("Expected value, but got {}", actual), + label = span => "Expected some value here", + note = format!("Got: {}", actual), + }, AstError::ParseError { file_id, source } => lalrpop_error_to_diagnostic(source, span, |error| match error { parse_error::ParseError::SimplExpr(_, error) => simplexpr_error_to_diagnostic(error, span), parse_error::ParseError::LexicalError(_) => lexical_error_to_diagnostic(span), }), + _ => panic!(), } } else { - diag.with_message(format!("{}", self)) + Diagnostic::error().with_message(format!("{}", self)) } } } @@ -86,7 +106,7 @@ fn lalrpop_error_to_diagnostic( fn simplexpr_error_to_diagnostic(error: &simplexpr::error::Error, span: Span) -> Diagnostic { use simplexpr::error::Error::*; match error { - ParseError { source } => lalrpop_error_to_diagnostic(source, span, move |error| lexical_error_to_diagnostic(span)), + ParseError { source, .. } => lalrpop_error_to_diagnostic(source, span, move |error| lexical_error_to_diagnostic(span)), ConversionError(error) => conversion_error_to_diagnostic(error, span), Eval(error) => gen_diagnostic!(format!("{}", error), span), Other(error) => gen_diagnostic!(format!("{}", error), span), diff --git a/src/parser/ast.rs b/src/parser/ast.rs index 66ed23e..c68d279 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -1,15 +1,21 @@ use itertools::Itertools; -use simplexpr::ast::SimplExpr; +use simplexpr::{ast::SimplExpr, dynval::DynVal}; use std::collections::HashMap; use std::fmt::Display; use super::element::FromAst; -use crate::error::{AstError, AstResult}; +use crate::error::{AstError, AstResult, OptionAstErrorExt}; #[derive(Eq, PartialEq, Clone, Copy)] pub struct Span(pub usize, pub usize, pub usize); +impl Into for Span { + fn into(self) -> simplexpr::Span { + simplexpr::Span(self.0, self.1, self.2) + } +} + impl std::fmt::Display for Span { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}..{}", self.0, self.1) @@ -45,7 +51,7 @@ pub enum Ast { Array(Span, Vec), Keyword(Span, String), Symbol(Span, String), - Value(Span, String), + Value(Span, DynVal), SimplExpr(Span, SimplExpr), Comment(Span), } @@ -69,7 +75,7 @@ macro_rules! as_func { } impl Ast { - as_func!(AstType::Value, as_value as_value_ref = Ast::Value(_, x) => x); + as_func!(AstType::Value, as_value as_value_ref = Ast::Value(_, x) => x); as_func!(AstType::Symbol, as_symbol as_symbol_ref = Ast::Symbol(_, x) => x); @@ -155,7 +161,7 @@ macro_rules! return_or_put_back { self.iter.put_back(other); Err(AstError::WrongExprType(Some(span), expr_type, actual_type)) } - None => Err(AstError::MissingNode(None, expr_type)), + None => Err(AstError::MissingNode(None)), } } }; @@ -164,14 +170,20 @@ macro_rules! return_or_put_back { impl> AstIterator { return_or_put_back!(expect_symbol, AstType::Symbol, (Span, String) = Ast::Symbol(span, x) => (span, x)); - return_or_put_back!(expect_string, AstType::Value, (Span, String) = Ast::Value(span, x) => (span, x)); + return_or_put_back!(expect_value, AstType::Value, (Span, DynVal) = Ast::Value(span, x) => (span, x)); return_or_put_back!(expect_list, AstType::List, (Span, Vec) = Ast::List(span, x) => (span, x)); + return_or_put_back!(expect_array, AstType::Array, (Span, Vec) = Ast::Array(span, x) => (span, x)); + pub fn new(iter: I) -> Self { AstIterator { iter: itertools::put_back(iter) } } + pub fn expect_any(&mut self) -> AstResult { + self.iter.next().or_missing().and_then(T::from_ast) + } + pub fn expect_key_values(&mut self) -> AstResult> { parse_key_values(&mut self.iter) } diff --git a/src/parser/element.rs b/src/parser/element.rs index 728ac0b..7455607 100644 --- a/src/parser/element.rs +++ b/src/parser/element.rs @@ -1,16 +1,13 @@ use super::ast::{Ast, AstIterator, AstType, Span}; -use crate::{error::*, parser, spanned}; +use crate::{error::*, parser, spanned, value::AttrName}; use itertools::Itertools; +use simplexpr::ast::SimplExpr; use std::{ collections::{HashMap, LinkedList}, iter::FromIterator, str::FromStr, }; -type VarName = String; -type AttrValue = String; -type AttrName = String; - pub trait FromAst: Sized { fn from_ast(e: Ast) -> AstResult; } @@ -21,6 +18,17 @@ impl FromAst for Ast { } } +impl FromAst for SimplExpr { + fn from_ast(e: Ast) -> AstResult { + match e { + Ast::Symbol(span, x) => Ok(SimplExpr::VarRef(span.into(), x)), + Ast::Value(span, x) => Ok(SimplExpr::Literal(span.into(), x)), + Ast::SimplExpr(span, x) => Ok(x), + _ => Err(AstError::NotAValue(Some(e.span()), e.expr_type())), + } + } +} + #[derive(Debug, Eq, PartialEq)] pub struct Element { name: String, @@ -36,7 +44,7 @@ impl FromAst for Element { let list = e.as_list()?; let mut iter = AstIterator::new(list.into_iter()); let (_, name) = iter.expect_symbol()?; - let attrs = iter.expect_key_values()?; + let attrs = iter.expect_key_values()?.into_iter().map(|(k, v)| (AttrName(k), v)).collect(); let children = iter.map(C::from_ast).collect::>>()?; Element { span, name, attrs, children } }) diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs index de184e1..d13cfa3 100644 --- a/src/parser/lexer.rs +++ b/src/parser/lexer.rs @@ -61,12 +61,12 @@ regex_rules! { r"\(" => |_| Token::LPren, r"\)" => |_| Token::RPren, r"\[" => |_| Token::LBrack, - r"\]" => |_| Token::LBrack, + r"\]" => |_| Token::RBrack, r"true" => |_| Token::True, r"false" => |_| Token::False, r#""(?:[^"\\]|\\.)*""# => |x| Token::StrLit(x), r#"[+-]?(?:[0-9]+[.])?[0-9]+"# => |x| Token::NumLit(x), - r#"[a-zA-Z_!\?<>/.*-+][^\s{}\(\)]*"# => |x| Token::Symbol(x), + r#"[a-zA-Z_!\?<>/.*-+][^\s{}\(\)\[\](){}]*"# => |x| Token::Symbol(x), r#":\S+"# => |x| Token::Keyword(x), r#";.*"# => |_| Token::Comment, r"[ \t\n\f]+" => |_| Token::Skip @@ -141,7 +141,9 @@ impl Iterator for Lexer { self.pos += len; match LEXER_FNS[i](tok_str.to_string()) { Token::Skip => {} - token => return Some(Ok((old_pos, token, self.pos))), + token => { + return Some(Ok((old_pos, token, self.pos))); + } } } } diff --git a/src/parser/parser.lalrpop b/src/parser/parser.lalrpop index 817a57b..7fc5163 100644 --- a/src/parser/parser.lalrpop +++ b/src/parser/parser.lalrpop @@ -33,7 +33,7 @@ pub Ast: Ast = { => Ast::SimplExpr(Span(l, r, file_id), expr), => x, => x, - => Ast::Value(Span(l, r, file_id), x), + => Ast::Value(Span(l, r, file_id), x.into()), "comment" => Ast::Comment(Span(l, r, file_id)), }; @@ -55,8 +55,8 @@ StrLit: String = { SimplExpr: SimplExpr = { =>? { let expr = x[1..x.len() - 1].to_string(); - simplexpr::parse_string(&expr).map_err(|e| { - let span = e.get_span().map(|simplexpr::Span(simpl_l, simpl_r)| Span(1 + l + simpl_l, 1 + l + simpl_r, file_id)); + simplexpr::parse_string(file_id, &expr).map_err(|e| { + let span = e.get_span().map(|simplexpr::Span(simpl_l, simpl_r, file_id)| Span(1 + l + simpl_l, 1 + l + simpl_r, file_id)); ParseError::User { error: parse_error::ParseError::SimplExpr(span, e) }}) } } diff --git a/src/parser/snapshots/eww_config__parser__element__test__test.snap.new b/src/parser/snapshots/eww_config__parser__element__test__test.snap.new new file mode 100644 index 0000000..56ba341 --- /dev/null +++ b/src/parser/snapshots/eww_config__parser__element__test__test.snap.new @@ -0,0 +1,17 @@ +--- +source: src/parser/element.rs +expression: "Element::::from_ast(parser.parse(0, lexer).unwrap()).unwrap()" + +--- +Element { + name: "box", + attrs: { + ":bar": "12", + ":baz": "hi", + }, + children: [ + foo, + (bar), + ], + span: 0..33, +} diff --git a/src/parser/snapshots/eww_config__parser__test.snap.new b/src/parser/snapshots/eww_config__parser__test.snap.new new file mode 100644 index 0000000..befaabc --- /dev/null +++ b/src/parser/snapshots/eww_config__parser__test.snap.new @@ -0,0 +1,8 @@ +--- +source: src/parser/mod.rs +expression: "p.parse(0, Lexer::new(0, \"1\".to_string()))" + +--- +Ok( + "1", +)