//! The parser of the Bases expression language. use notescraft_bases::Error; use notescraft_bases::expr::{BinOp, Expr, UnOp, parse_expr}; /// A name. fn ident(name: &str) -> Expr { Expr::Ident(name.to_string()) } /// A member access. fn member(base: Expr, name: &str) -> Expr { Expr::Member(Box::new(base), name.to_string()) } /// An index access. fn index(base: Expr, at: Expr) -> Expr { Expr::Index(Box::new(base), Box::new(at)) } /// A call. fn call(callee: Expr, args: Vec) -> Expr { Expr::Call { callee: Box::new(callee), args } } /// A binary operation. fn binary(op: BinOp, left: Expr, right: Expr) -> Expr { Expr::Binary(op, Box::new(left), Box::new(right)) } /// A prefix operation. fn unary(op: UnOp, operand: Expr) -> Expr { Expr::Unary(op, Box::new(operand)) } /// A string literal. fn text(value: &str) -> Expr { Expr::Str(value.to_string()) } /// Parses, or fails the test with the reason. fn parse(source: &str) -> Expr { parse_expr(source).unwrap_or_else(|err| panic!("`{source}` must parse: {err}")) } #[test] fn precedence_follows_the_spec() { let cases: Vec<(&str, Expr)> = vec![ ("a || b && c", binary(BinOp::Or, ident("a"), binary(BinOp::And, ident("b"), ident("c")))), ("a && b || c", binary(BinOp::Or, binary(BinOp::And, ident("a"), ident("b")), ident("c"))), ("a == b && c != d", binary(BinOp::And, binary(BinOp::Eq, ident("a"), ident("b")), binary(BinOp::Ne, ident("c"), ident("d")))), ("a < b == c", binary(BinOp::Eq, binary(BinOp::Lt, ident("a"), ident("b")), ident("c"))), ("a + b * c", binary(BinOp::Add, ident("a"), binary(BinOp::Mul, ident("b"), ident("c")))), ("a * b + c", binary(BinOp::Add, binary(BinOp::Mul, ident("a"), ident("b")), ident("c"))), ("-a * b", binary(BinOp::Mul, unary(UnOp::Neg, ident("a")), ident("b"))), ("!a && b", binary(BinOp::And, unary(UnOp::Not, ident("a")), ident("b"))), ("a - b - c", binary(BinOp::Sub, binary(BinOp::Sub, ident("a"), ident("b")), ident("c"))), ("a / b % c", binary(BinOp::Mod, binary(BinOp::Div, ident("a"), ident("b")), ident("c"))), ("a + b == c + d", binary(BinOp::Eq, binary(BinOp::Add, ident("a"), ident("b")), binary(BinOp::Add, ident("c"), ident("d")))), ]; for (source, expected) in cases { assert_eq!(parse(source), expected, "{source}"); } } #[test] fn member_index_and_call_chain() { let expected = call(member(index(member(ident("a"), "b"), Expr::Num(0.0)), "c"), vec![text("x")]); assert_eq!(parse("a.b[0].c(\"x\")"), expected); } #[test] fn lists_and_objects_are_literals() { assert_eq!(parse("[latitude, longitude]"), Expr::Array(vec![ident("latitude"), ident("longitude")])); assert_eq!(parse("{\"k\": 1, \"j\": \"v\"}"), Expr::Object(vec![("k".to_string(), Expr::Num(1.0)), ("j".to_string(), text("v"))])); assert_eq!(parse("[]"), Expr::Array(Vec::new())); } #[test] fn comparison_and_boolean_operators() { assert_eq!( parse("price > 2.1 && status != \"done\""), binary(BinOp::And, binary(BinOp::Gt, ident("price"), Expr::Num(2.1)), binary(BinOp::Ne, ident("status"), text("done")),) ); assert_eq!( parse("file.hasTag(\"a\") || !file.inFolder(\"x\")"), binary(BinOp::Or, call(member(ident("file"), "hasTag"), vec![text("a")]), unary(UnOp::Not, call(member(ident("file"), "inFolder"), vec![text("x")])),) ); } #[test] fn a_regex_is_a_literal_where_an_operand_is_expected() { let regex = Expr::Regex { pattern: "ab".to_string(), flags: String::new() }; assert_eq!(parse("(/ab/)"), regex); assert_eq!(parse("x == /ab/"), binary(BinOp::Eq, ident("x"), regex)); assert_eq!(parse("x == /ab/i"), binary(BinOp::Eq, ident("x"), Expr::Regex { pattern: "ab".to_string(), flags: "i".to_string() })); } #[test] fn a_slash_after_an_operand_is_a_division() { assert_eq!(parse("a / b"), binary(BinOp::Div, ident("a"), ident("b"))); assert_eq!(parse("a / b / c"), binary(BinOp::Div, binary(BinOp::Div, ident("a"), ident("b")), ident("c"))); } #[test] fn strings_know_their_escapes() { assert_eq!(parse("\"a\\nb\""), text("a\nb")); assert_eq!(parse("'a\\'b'"), text("a'b")); assert_eq!(parse("\"a\\\\b\""), text("a\\b")); } #[test] fn an_unterminated_string_reports_its_position() { let err = parse_expr("\"abc").expect_err("the string is not closed"); match err { Error::Syntax { line, column, message } => { assert_eq!(line, 1); assert_eq!(column, 1); assert!(message.contains("not closed"), "{message}"); } other @ (Error::Limit(_) | Error::Invalid(_) | Error::Schema { .. }) => panic!("unexpected error: {other:?}"), } } #[test] fn a_deep_expression_is_refused() { let source = format!("{}x{}", "(".repeat(65), ")".repeat(65)); let err = parse_expr(&source).expect_err("65 levels are too deep"); assert!(matches!(err, Error::Limit(_)), "unexpected error: {err:?}"); let source = format!("{}x{}", "(".repeat(64), ")".repeat(64)); assert!(parse_expr(&source).is_ok(), "64 levels are allowed"); } #[test] fn a_long_expression_is_refused() { let source = "a".repeat(4097); let err = parse_expr(&source).expect_err("4097 bytes are too many"); assert!(matches!(err, Error::Limit(_)), "unexpected error: {err:?}"); } #[test] fn deps_collect_the_namespaces() { let expr = parse("note.title + file.size + formula.total + this.file.ctime + price"); let deps = expr.deps(); assert_eq!(deps.note, vec!["price".to_string(), "title".to_string()]); assert_eq!(deps.file, vec!["size".to_string()]); assert_eq!(deps.formula, vec!["total".to_string()]); assert!(deps.uses_this); } #[test] fn deps_keep_the_member_path() { let deps = parse("note.meta.author").deps(); assert_eq!(deps.note, vec!["meta.author".to_string()]); assert_eq!(deps.file, Vec::::new()); assert_eq!(deps.formula, Vec::::new()); assert!(!deps.uses_this); } /// The next value of a small linear congruential generator. fn next_u64(state: &mut u64) -> u64 { *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); *state >> 33 } #[test] fn random_sources_never_panic() { let mut state = 0x2545_f491_4f6c_dd1d_u64; for _ in 0..5000 { let len = usize::try_from(next_u64(&mut state) % 24).unwrap_or(0); let mut bytes = Vec::new(); for _ in 0..len { bytes.push(u8::try_from(next_u64(&mut state) % 256).unwrap_or(0)); } let source = String::from_utf8_lossy(&bytes); drop(parse_expr(&source)); } }