1#![forbid(unsafe_code)]
4#![deny(missing_docs)]
5
6use std::ops::Deref;
7
8use crate::{
9 call::{Call, Function},
10 context::Context,
11 identifier::Identifier,
12 literal::Literal,
13 op::{BinOp, UnOp},
14};
15
16use self::parser::{ExprParser, Rule};
17use anyhow::Result;
18use itertools::Itertools;
19use pest::{Parser, iterators::Pair};
20
21pub mod call;
22pub mod context;
23pub mod identifier;
24pub mod literal;
25pub mod op;
26
27mod parser {
31 use pest_derive::Parser;
32
33 #[derive(Parser)]
35 #[grammar = "expr.pest"]
36 pub struct ExprParser;
37}
38
39#[derive(Copy, Clone, Debug, PartialEq)]
42pub struct Origin<'src> {
43 pub span: subfeature::Span,
45 pub raw: &'src str,
52}
53
54impl<'a> Origin<'a> {
55 pub fn new(span: impl Into<subfeature::Span>, raw: &'a str) -> Self {
57 Self {
58 span: span.into(),
59 raw: raw.trim(),
60 }
61 }
62}
63
64#[derive(Debug, PartialEq)]
72pub struct SpannedExpr<'src> {
73 pub origin: Origin<'src>,
75 pub inner: Expr<'src>,
77}
78
79impl<'a> SpannedExpr<'a> {
80 pub(crate) fn new(origin: Origin<'a>, inner: Expr<'a>) -> Self {
82 Self { origin, inner }
83 }
84
85 pub fn contexts(&self) -> Vec<(&Context<'a>, &Origin<'a>)> {
94 let mut contexts = vec![];
95
96 match self.deref() {
97 Expr::Index(expr) => contexts.extend(expr.contexts()),
98 Expr::Call(Call { func: _, args }) => {
99 for arg in args {
100 contexts.extend(arg.contexts());
101 }
102 }
103 Expr::Context(ctx) => {
104 contexts.push((ctx, &self.origin));
106
107 ctx.parts
110 .iter()
111 .for_each(|part| contexts.extend(part.contexts()));
112 }
113 Expr::BinOp { lhs, op: _, rhs } => {
114 contexts.extend(lhs.contexts());
115 contexts.extend(rhs.contexts());
116 }
117 Expr::UnOp { op: _, expr } => contexts.extend(expr.contexts()),
118 _ => (),
119 }
120
121 contexts
122 }
123
124 pub fn dataflow_contexts(&self) -> Vec<(&Context<'a>, &Origin<'a>)> {
133 let mut contexts = vec![];
134
135 match self.deref() {
136 Expr::Call(Call { func, args }) => {
137 if func == "toJSON" || func == "format" || func == "join" {
141 for arg in args {
142 contexts.extend(arg.dataflow_contexts());
143 }
144 }
145 }
146 Expr::Context(ctx) => contexts.push((ctx, &self.origin)),
153 Expr::BinOp { lhs, op, rhs } => match op {
154 BinOp::And => {
157 contexts.extend(rhs.dataflow_contexts());
158 }
159 BinOp::Or => {
161 contexts.extend(lhs.dataflow_contexts());
162 contexts.extend(rhs.dataflow_contexts());
163 }
164 _ => (),
165 },
166 _ => (),
167 }
168
169 contexts
170 }
171
172 pub fn computed_indices(&self) -> Vec<&SpannedExpr<'a>> {
177 let mut index_exprs = vec![];
178
179 match self.deref() {
180 Expr::Call(Call { func: _, args }) => {
181 for arg in args {
182 index_exprs.extend(arg.computed_indices());
183 }
184 }
185 Expr::Index(spanned_expr) => {
186 if !spanned_expr.is_literal() && !matches!(spanned_expr.inner, Expr::Star) {
188 index_exprs.push(self);
189 }
190 }
191 Expr::Context(context) => {
192 for part in &context.parts {
193 index_exprs.extend(part.computed_indices());
194 }
195 }
196 Expr::BinOp { lhs, op: _, rhs } => {
197 index_exprs.extend(lhs.computed_indices());
198 index_exprs.extend(rhs.computed_indices());
199 }
200 Expr::UnOp { op: _, expr } => {
201 index_exprs.extend(expr.computed_indices());
202 }
203 _ => {}
204 }
205
206 index_exprs
207 }
208
209 pub fn constant_reducible_subexprs(&self) -> Vec<&SpannedExpr<'a>> {
217 if !self.is_literal() && self.constant_reducible() {
218 return vec![self];
219 }
220
221 let mut subexprs = vec![];
222
223 match self.deref() {
224 Expr::Call(Call { func: _, args }) => {
225 for arg in args {
226 subexprs.extend(arg.constant_reducible_subexprs());
227 }
228 }
229 Expr::Context(ctx) => {
230 for part in &ctx.parts {
233 subexprs.extend(part.constant_reducible_subexprs());
234 }
235 }
236 Expr::BinOp { lhs, op: _, rhs } => {
237 subexprs.extend(lhs.constant_reducible_subexprs());
238 subexprs.extend(rhs.constant_reducible_subexprs());
239 }
240 Expr::UnOp { op: _, expr } => subexprs.extend(expr.constant_reducible_subexprs()),
241
242 Expr::Index(expr) => subexprs.extend(expr.constant_reducible_subexprs()),
243 _ => {}
244 }
245
246 subexprs
247 }
248}
249
250impl<'a> Deref for SpannedExpr<'a> {
251 type Target = Expr<'a>;
252
253 fn deref(&self) -> &Self::Target {
254 &self.inner
255 }
256}
257
258impl<'doc> From<&SpannedExpr<'doc>> for subfeature::Fragment<'doc> {
259 fn from(expr: &SpannedExpr<'doc>) -> Self {
260 Self::new(expr.origin.raw)
261 }
262}
263
264#[derive(Debug, PartialEq)]
266pub enum Expr<'src> {
267 Literal(Literal<'src>),
269 Star,
271 Call(Call<'src>),
273 Identifier(Identifier<'src>),
275 Index(Box<SpannedExpr<'src>>),
277 Context(Context<'src>),
279 BinOp {
281 lhs: Box<SpannedExpr<'src>>,
283 op: BinOp,
285 rhs: Box<SpannedExpr<'src>>,
287 },
288 UnOp {
290 op: UnOp,
292 expr: Box<SpannedExpr<'src>>,
294 },
295}
296
297impl<'src> Expr<'src> {
298 fn ident(i: &'src str) -> Self {
300 Self::Identifier(Identifier(i))
301 }
302
303 fn context(components: impl Into<Vec<SpannedExpr<'src>>>) -> Self {
305 Self::Context(Context::new(components))
306 }
307
308 pub fn is_literal(&self) -> bool {
310 matches!(self, Expr::Literal(_))
311 }
312
313 pub fn constant_reducible(&self) -> bool {
332 match self {
333 Expr::Literal(_) => true,
335 Expr::BinOp { lhs, op: _, rhs } => lhs.constant_reducible() && rhs.constant_reducible(),
337 Expr::UnOp { op: _, expr } => expr.constant_reducible(),
339 Expr::Call(Call { func, args }) => {
340 if func == "format"
342 || func == "contains"
343 || func == "startsWith"
344 || func == "endsWith"
345 || func == "toJSON"
346 || func == "join"
351 {
352 args.iter().all(|e| e.constant_reducible())
353 } else {
354 false
355 }
356 }
357 _ => false,
359 }
360 }
361
362 #[allow(clippy::unwrap_used)]
364 pub fn parse(expr: &'src str) -> Result<SpannedExpr<'src>> {
365 let or_expr = ExprParser::parse(Rule::expression, expr)?
367 .next()
368 .unwrap()
369 .into_inner()
370 .next()
371 .unwrap();
372
373 fn parse_pair(pair: Pair<'_, Rule>) -> Result<Box<SpannedExpr<'_>>> {
374 match pair.as_rule() {
386 Rule::or_expr => {
387 let (span, raw) = (pair.as_span(), pair.as_str());
388 let mut pairs = pair.into_inner();
389 let lhs = parse_pair(pairs.next().unwrap())?;
390 pairs.try_fold(lhs, |expr, next| {
391 Ok(SpannedExpr::new(
392 Origin::new(span.start()..span.end(), raw),
393 Expr::BinOp {
394 lhs: expr,
395 op: BinOp::Or,
396 rhs: parse_pair(next)?,
397 },
398 )
399 .into())
400 })
401 }
402 Rule::and_expr => {
403 let (span, raw) = (pair.as_span(), pair.as_str());
404 let mut pairs = pair.into_inner();
405 let lhs = parse_pair(pairs.next().unwrap())?;
406 pairs.try_fold(lhs, |expr, next| {
407 Ok(SpannedExpr::new(
408 Origin::new(span.start()..span.end(), raw),
409 Expr::BinOp {
410 lhs: expr,
411 op: BinOp::And,
412 rhs: parse_pair(next)?,
413 },
414 )
415 .into())
416 })
417 }
418 Rule::eq_expr => {
419 let (span, raw) = (pair.as_span(), pair.as_str());
423 let mut pairs = pair.into_inner();
424 let lhs = parse_pair(pairs.next().unwrap())?;
425
426 let pair_chunks = pairs.chunks(2);
427 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
428 let eq_op = next.next().unwrap();
429 let comp_expr = next.next().unwrap();
430
431 let eq_op = match eq_op.as_str() {
432 "==" => BinOp::Eq,
433 "!=" => BinOp::Neq,
434 _ => unreachable!(),
435 };
436
437 Ok(SpannedExpr::new(
438 Origin::new(span.start()..span.end(), raw),
439 Expr::BinOp {
440 lhs: expr,
441 op: eq_op,
442 rhs: parse_pair(comp_expr)?,
443 },
444 )
445 .into())
446 })
447 }
448 Rule::comp_expr => {
449 let (span, raw) = (pair.as_span(), pair.as_str());
451 let mut pairs = pair.into_inner();
452 let lhs = parse_pair(pairs.next().unwrap())?;
453
454 let pair_chunks = pairs.chunks(2);
455 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
456 let comp_op = next.next().unwrap();
457 let unary_expr = next.next().unwrap();
458
459 let eq_op = match comp_op.as_str() {
460 ">" => BinOp::Gt,
461 ">=" => BinOp::Ge,
462 "<" => BinOp::Lt,
463 "<=" => BinOp::Le,
464 _ => unreachable!(),
465 };
466
467 Ok(SpannedExpr::new(
468 Origin::new(span.start()..span.end(), raw),
469 Expr::BinOp {
470 lhs: expr,
471 op: eq_op,
472 rhs: parse_pair(unary_expr)?,
473 },
474 )
475 .into())
476 })
477 }
478 Rule::unary_expr => {
479 let (span, raw) = (pair.as_span(), pair.as_str());
480 let mut pairs = pair.into_inner();
481 let inner_pair = pairs.next().unwrap();
482
483 match inner_pair.as_rule() {
484 Rule::unary_op => Ok(SpannedExpr::new(
485 Origin::new(span.start()..span.end(), raw),
486 Expr::UnOp {
487 op: UnOp::Not,
488 expr: parse_pair(pairs.next().unwrap())?,
489 },
490 )
491 .into()),
492 Rule::primary_expr => parse_pair(inner_pair),
493 _ => unreachable!(),
494 }
495 }
496 Rule::primary_expr => {
497 parse_pair(pair.into_inner().next().unwrap())
499 }
500 Rule::number => Ok(SpannedExpr::new(
501 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
502 pair.as_str().parse::<f64>().unwrap().into(),
503 )
504 .into()),
505 Rule::string => {
506 let (span, raw) = (pair.as_span(), pair.as_str());
507 let string_inner = pair.into_inner().next().unwrap().as_str();
509
510 if !string_inner.contains('\'') {
513 Ok(SpannedExpr::new(
514 Origin::new(span.start()..span.end(), raw),
515 string_inner.into(),
516 )
517 .into())
518 } else {
519 Ok(SpannedExpr::new(
520 Origin::new(span.start()..span.end(), raw),
521 string_inner.replace("''", "'").into(),
522 )
523 .into())
524 }
525 }
526 Rule::boolean => Ok(SpannedExpr::new(
527 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
528 pair.as_str().parse::<bool>().unwrap().into(),
529 )
530 .into()),
531 Rule::null => Ok(SpannedExpr::new(
532 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
533 Expr::Literal(Literal::Null),
534 )
535 .into()),
536 Rule::star => Ok(SpannedExpr::new(
537 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
538 Expr::Star,
539 )
540 .into()),
541 Rule::function_call => {
542 let (span, raw) = (pair.as_span(), pair.as_str());
543 let mut pairs = pair.into_inner();
544
545 let identifier = pairs.next().unwrap();
546 let args = pairs
547 .map(|pair| parse_pair(pair).map(|e| *e))
548 .collect::<Result<_, _>>()?;
549
550 Ok(SpannedExpr::new(
551 Origin::new(span.start()..span.end(), raw),
552 Expr::Call(Call {
553 func: Function(identifier.as_str()),
554 args,
555 }),
556 )
557 .into())
558 }
559 Rule::identifier => Ok(SpannedExpr::new(
560 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
561 Expr::ident(pair.as_str()),
562 )
563 .into()),
564 Rule::index => Ok(SpannedExpr::new(
565 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
566 Expr::Index(parse_pair(pair.into_inner().next().unwrap())?),
567 )
568 .into()),
569 Rule::context => {
570 let (span, raw) = (pair.as_span(), pair.as_str());
571 let pairs = pair.into_inner();
572
573 let mut inner: Vec<SpannedExpr> = pairs
574 .map(|pair| parse_pair(pair).map(|e| *e))
575 .collect::<Result<_, _>>()?;
576
577 if inner.len() == 1 && matches!(inner[0].inner, Expr::Call { .. }) {
581 Ok(inner.remove(0).into())
582 } else {
583 Ok(SpannedExpr::new(
584 Origin::new(span.start()..span.end(), raw),
585 Expr::context(inner),
586 )
587 .into())
588 }
589 }
590 r => panic!("unrecognized rule: {r:?}"),
591 }
592 }
593
594 parse_pair(or_expr).map(|e| *e)
595 }
596}
597
598impl<'src> From<&'src str> for Expr<'src> {
599 fn from(s: &'src str) -> Self {
600 Expr::Literal(Literal::String(s.into()))
601 }
602}
603
604impl From<String> for Expr<'_> {
605 fn from(s: String) -> Self {
606 Expr::Literal(Literal::String(s.into()))
607 }
608}
609
610impl From<f64> for Expr<'_> {
611 fn from(n: f64) -> Self {
612 Expr::Literal(Literal::Number(n))
613 }
614}
615
616impl From<bool> for Expr<'_> {
617 fn from(b: bool) -> Self {
618 Expr::Literal(Literal::Boolean(b))
619 }
620}
621
622#[derive(Debug, Clone, PartialEq)]
627pub enum Evaluation {
628 String(String),
630 Number(f64),
632 Boolean(bool),
634 Null,
636 Array(Vec<Evaluation>),
638 Object(std::collections::HashMap<String, Evaluation>),
640}
641
642impl TryFrom<serde_json::Value> for Evaluation {
643 type Error = ();
644
645 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
646 match value {
647 serde_json::Value::Null => Ok(Evaluation::Null),
648 serde_json::Value::Bool(b) => Ok(Evaluation::Boolean(b)),
649 serde_json::Value::Number(n) => {
650 if let Some(f) = n.as_f64() {
651 Ok(Evaluation::Number(f))
652 } else {
653 Err(())
654 }
655 }
656 serde_json::Value::String(s) => Ok(Evaluation::String(s)),
657 serde_json::Value::Array(arr) => {
658 let elements = arr
659 .into_iter()
660 .map(|elem| elem.try_into())
661 .collect::<Result<_, _>>()?;
662 Ok(Evaluation::Array(elements))
663 }
664 serde_json::Value::Object(obj) => {
665 let mut map = std::collections::HashMap::new();
666 for (key, value) in obj {
667 map.insert(key, value.try_into()?);
668 }
669 Ok(Evaluation::Object(map))
670 }
671 }
672 }
673}
674
675impl TryInto<serde_json::Value> for Evaluation {
676 type Error = ();
677
678 fn try_into(self) -> Result<serde_json::Value, Self::Error> {
679 match self {
680 Evaluation::Null => Ok(serde_json::Value::Null),
681 Evaluation::Boolean(b) => Ok(serde_json::Value::Bool(b)),
682 Evaluation::Number(n) => {
683 if n.fract() == 0.0 {
687 Ok(serde_json::Value::Number(serde_json::Number::from(
688 n as i64,
689 )))
690 } else if let Some(num) = serde_json::Number::from_f64(n) {
691 Ok(serde_json::Value::Number(num))
692 } else {
693 Err(())
694 }
695 }
696 Evaluation::String(s) => Ok(serde_json::Value::String(s)),
697 Evaluation::Array(arr) => {
698 let elements = arr
699 .into_iter()
700 .map(|elem| elem.try_into())
701 .collect::<Result<_, _>>()?;
702 Ok(serde_json::Value::Array(elements))
703 }
704 Evaluation::Object(obj) => {
705 let mut map = serde_json::Map::new();
706 for (key, value) in obj {
707 map.insert(key, value.try_into()?);
708 }
709 Ok(serde_json::Value::Object(map))
710 }
711 }
712 }
713}
714
715impl Evaluation {
716 pub fn as_boolean(&self) -> bool {
724 match self {
725 Evaluation::Boolean(b) => *b,
726 Evaluation::Null => false,
727 Evaluation::Number(n) => *n != 0.0,
728 Evaluation::String(s) => !s.is_empty(),
729 Evaluation::Array(_) | Evaluation::Object(_) => true,
731 }
732 }
733
734 pub fn as_number(&self) -> f64 {
738 match self {
739 Evaluation::String(s) => {
740 if s.is_empty() {
741 0.0
742 } else {
743 s.parse::<f64>().unwrap_or(f64::NAN)
744 }
745 }
746 Evaluation::Number(n) => *n,
747 Evaluation::Boolean(b) => {
748 if *b {
749 1.0
750 } else {
751 0.0
752 }
753 }
754 Evaluation::Null => 0.0,
755 Evaluation::Array(_) | Evaluation::Object(_) => f64::NAN,
756 }
757 }
758
759 pub fn sema(&self) -> EvaluationSema<'_> {
762 EvaluationSema(self)
763 }
764}
765
766pub struct EvaluationSema<'a>(&'a Evaluation);
769
770impl PartialEq for EvaluationSema<'_> {
771 fn eq(&self, other: &Self) -> bool {
772 match (self.0, other.0) {
773 (Evaluation::Null, Evaluation::Null) => true,
774 (Evaluation::Boolean(a), Evaluation::Boolean(b)) => a == b,
775 (Evaluation::Number(a), Evaluation::Number(b)) => a == b,
776 (Evaluation::String(a), Evaluation::String(b)) => a == b,
777
778 (a, b) => a.as_number() == b.as_number(),
780 }
781 }
782}
783
784impl PartialOrd for EvaluationSema<'_> {
785 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
786 match (self.0, other.0) {
787 (Evaluation::Null, Evaluation::Null) => Some(std::cmp::Ordering::Equal),
788 (Evaluation::Boolean(a), Evaluation::Boolean(b)) => a.partial_cmp(b),
789 (Evaluation::Number(a), Evaluation::Number(b)) => a.partial_cmp(b),
790 (Evaluation::String(a), Evaluation::String(b)) => a.partial_cmp(b),
791 (a, b) => a.as_number().partial_cmp(&b.as_number()),
793 }
794 }
795}
796
797impl std::fmt::Display for EvaluationSema<'_> {
798 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
799 match self.0 {
800 Evaluation::String(s) => write!(f, "{}", s),
801 Evaluation::Number(n) => {
802 if n.fract() == 0.0 {
804 write!(f, "{}", *n as i64)
805 } else {
806 write!(f, "{}", n)
807 }
808 }
809 Evaluation::Boolean(b) => write!(f, "{}", b),
810 Evaluation::Null => write!(f, ""),
811 Evaluation::Array(_) => write!(f, "Array"),
812 Evaluation::Object(_) => write!(f, "Object"),
813 }
814 }
815}
816
817impl<'src> Expr<'src> {
818 pub fn consteval(&self) -> Option<Evaluation> {
841 match self {
842 Expr::Literal(literal) => Some(literal.consteval()),
843
844 Expr::BinOp { lhs, op, rhs } => {
845 let lhs_val = lhs.consteval()?;
846 let rhs_val = rhs.consteval()?;
847
848 match op {
849 BinOp::And => {
850 if lhs_val.as_boolean() {
852 Some(rhs_val)
853 } else {
854 Some(lhs_val)
855 }
856 }
857 BinOp::Or => {
858 if lhs_val.as_boolean() {
860 Some(lhs_val)
861 } else {
862 Some(rhs_val)
863 }
864 }
865 BinOp::Eq => Some(Evaluation::Boolean(lhs_val.sema() == rhs_val.sema())),
866 BinOp::Neq => Some(Evaluation::Boolean(lhs_val.sema() != rhs_val.sema())),
867 BinOp::Lt => Some(Evaluation::Boolean(lhs_val.sema() < rhs_val.sema())),
868 BinOp::Le => Some(Evaluation::Boolean(lhs_val.sema() <= rhs_val.sema())),
869 BinOp::Gt => Some(Evaluation::Boolean(lhs_val.sema() > rhs_val.sema())),
870 BinOp::Ge => Some(Evaluation::Boolean(lhs_val.sema() >= rhs_val.sema())),
871 }
872 }
873
874 Expr::UnOp { op, expr } => {
875 let val = expr.consteval()?;
876 match op {
877 UnOp::Not => Some(Evaluation::Boolean(!val.as_boolean())),
878 }
879 }
880
881 Expr::Call(call) => call.consteval(),
882
883 _ => None,
885 }
886 }
887}
888
889#[cfg(test)]
890mod tests {
891 use std::borrow::Cow;
892
893 use anyhow::Result;
894 use pest::Parser as _;
895 use pretty_assertions::assert_eq;
896
897 use crate::{Call, Literal, Origin, SpannedExpr};
898
899 use super::{BinOp, Expr, ExprParser, Function, Rule, UnOp};
900
901 #[test]
902 fn test_literal_string_borrows() {
903 let cases = &[
904 ("'foo'", true),
905 ("'foo bar'", true),
906 ("'foo '' bar'", false),
907 ("'foo''bar'", false),
908 ("'foo''''bar'", false),
909 ];
910
911 for (expr, borrows) in cases {
912 let Expr::Literal(Literal::String(s)) = &*Expr::parse(expr).unwrap() else {
913 panic!("expected a literal string expression for {expr}");
914 };
915
916 assert!(matches!(
917 (s, borrows),
918 (Cow::Borrowed(_), true) | (Cow::Owned(_), false)
919 ));
920 }
921 }
922
923 #[test]
924 fn test_literal_as_str() {
925 let cases = &[
926 ("'foo'", "foo"),
927 ("'foo '' bar'", "foo ' bar"),
928 ("123", "123"),
929 ("123.000", "123"),
930 ("0.0", "0"),
931 ("0.1", "0.1"),
932 ("0.12345", "0.12345"),
933 ("true", "true"),
934 ("false", "false"),
935 ("null", "null"),
936 ];
937
938 for (expr, expected) in cases {
939 let Expr::Literal(expr) = &*Expr::parse(expr).unwrap() else {
940 panic!("expected a literal expression for {expr}");
941 };
942
943 assert_eq!(expr.as_str(), *expected);
944 }
945 }
946
947 #[test]
948 fn test_function_eq() {
949 let func = Function("foo");
950 assert_eq!(&func, "foo");
951 assert_eq!(&func, "FOO");
952 assert_eq!(&func, "Foo");
953
954 assert_eq!(func, Function("FOO"));
955 }
956
957 #[test]
958 fn test_parse_string_rule() {
959 let cases = &[
960 ("''", ""),
961 ("' '", " "),
962 ("''''", "''"),
963 ("'test'", "test"),
964 ("'spaces are ok'", "spaces are ok"),
965 ("'escaping '' works'", "escaping '' works"),
966 ];
967
968 for (case, expected) in cases {
969 let s = ExprParser::parse(Rule::string, case)
970 .unwrap()
971 .next()
972 .unwrap();
973
974 assert_eq!(s.into_inner().next().unwrap().as_str(), *expected);
975 }
976 }
977
978 #[test]
979 fn test_parse_context_rule() {
980 let cases = &[
981 "foo.bar",
982 "github.action_path",
983 "inputs.foo-bar",
984 "inputs.also--valid",
985 "inputs.this__too",
986 "inputs.this__too",
987 "secrets.GH_TOKEN",
988 "foo.*.bar",
989 "github.event.issue.labels.*.name",
990 ];
991
992 for case in cases {
993 assert_eq!(
994 ExprParser::parse(Rule::context, case)
995 .unwrap()
996 .next()
997 .unwrap()
998 .as_str(),
999 *case
1000 );
1001 }
1002 }
1003
1004 #[test]
1005 fn test_parse_call_rule() {
1006 let cases = &[
1007 "foo()",
1008 "foo(bar)",
1009 "foo(bar())",
1010 "foo(1.23)",
1011 "foo(1,2)",
1012 "foo(1, 2)",
1013 "foo(1, 2, secret.GH_TOKEN)",
1014 "foo( )",
1015 "fromJSON(inputs.free-threading)",
1016 ];
1017
1018 for case in cases {
1019 assert_eq!(
1020 ExprParser::parse(Rule::function_call, case)
1021 .unwrap()
1022 .next()
1023 .unwrap()
1024 .as_str(),
1025 *case
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn test_parse_expr_rule() -> Result<()> {
1032 let multiline = "github.repository_owner == 'Homebrew' &&
1034 ((github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
1035 (github.event_name == 'pull_request_target' &&
1036 (github.event.action == 'ready_for_review' || github.event.label.name == 'automerge-skip')))";
1037
1038 let multiline2 = "foo.bar.baz[
1039 0
1040 ]";
1041
1042 let cases = &[
1043 "true",
1044 "fromJSON(inputs.free-threading) && '--disable-gil' || ''",
1045 "foo || bar || baz",
1046 "foo || bar && baz || foo && 1 && 2 && 3 || 4",
1047 "(github.actor != 'github-actions[bot]' && github.actor) || 'BrewTestBot'",
1048 "(true || false) == true",
1049 "!(!true || false)",
1050 "!(!true || false) == true",
1051 "(true == false) == true",
1052 "(true == (false || true && (true || false))) == true",
1053 "(github.actor != 'github-actions[bot]' && github.actor) == 'BrewTestBot'",
1054 "foo()[0]",
1055 "fromJson(steps.runs.outputs.data).workflow_runs[0].id",
1056 multiline,
1057 "'a' == 'b' && 'c' || 'd'",
1058 "github.event['a']",
1059 "github.event['a' == 'b']",
1060 "github.event['a' == 'b' && 'c' || 'd']",
1061 "github['event']['inputs']['dry-run']",
1062 "github[format('{0}', 'event')]",
1063 "github['event']['inputs'][github.event.inputs.magic]",
1064 "github['event']['inputs'].*",
1065 "1 == 1",
1066 "1 > 1",
1067 "1 >= 1",
1068 "matrix.node_version >= 20",
1069 "true||false",
1070 multiline2,
1071 "fromJSON( github.event.inputs.hmm ) [ 0 ]",
1072 ];
1073
1074 for case in cases {
1075 assert_eq!(
1076 ExprParser::parse(Rule::expression, case)?
1077 .next()
1078 .unwrap()
1079 .as_str(),
1080 *case
1081 );
1082 }
1083
1084 Ok(())
1085 }
1086
1087 #[test]
1088 fn test_parse() {
1089 let cases = &[
1090 (
1091 "!true || false || true",
1092 SpannedExpr::new(
1093 Origin::new(0..22, "!true || false || true"),
1094 Expr::BinOp {
1095 lhs: SpannedExpr::new(
1096 Origin::new(0..22, "!true || false || true"),
1097 Expr::BinOp {
1098 lhs: SpannedExpr::new(
1099 Origin::new(0..5, "!true"),
1100 Expr::UnOp {
1101 op: UnOp::Not,
1102 expr: SpannedExpr::new(
1103 Origin::new(1..5, "true"),
1104 true.into(),
1105 )
1106 .into(),
1107 },
1108 )
1109 .into(),
1110 op: BinOp::Or,
1111 rhs: SpannedExpr::new(Origin::new(9..14, "false"), false.into())
1112 .into(),
1113 },
1114 )
1115 .into(),
1116 op: BinOp::Or,
1117 rhs: SpannedExpr::new(Origin::new(18..22, "true"), true.into()).into(),
1118 },
1119 ),
1120 ),
1121 (
1122 "'foo '' bar'",
1123 SpannedExpr::new(
1124 Origin::new(0..12, "'foo '' bar'"),
1125 Expr::Literal(Literal::String("foo ' bar".into())),
1126 ),
1127 ),
1128 (
1129 "('foo '' bar')",
1130 SpannedExpr::new(
1131 Origin::new(1..13, "'foo '' bar'"),
1132 Expr::Literal(Literal::String("foo ' bar".into())),
1133 ),
1134 ),
1135 (
1136 "((('foo '' bar')))",
1137 SpannedExpr::new(
1138 Origin::new(3..15, "'foo '' bar'"),
1139 Expr::Literal(Literal::String("foo ' bar".into())),
1140 ),
1141 ),
1142 (
1143 "foo(1, 2, 3)",
1144 SpannedExpr::new(
1145 Origin::new(0..12, "foo(1, 2, 3)"),
1146 Expr::Call(Call {
1147 func: Function("foo"),
1148 args: vec![
1149 SpannedExpr::new(Origin::new(4..5, "1"), 1.0.into()),
1150 SpannedExpr::new(Origin::new(7..8, "2"), 2.0.into()),
1151 SpannedExpr::new(Origin::new(10..11, "3"), 3.0.into()),
1152 ],
1153 }),
1154 ),
1155 ),
1156 (
1157 "foo.bar.baz",
1158 SpannedExpr::new(
1159 Origin::new(0..11, "foo.bar.baz"),
1160 Expr::context(vec![
1161 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1162 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1163 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1164 ]),
1165 ),
1166 ),
1167 (
1168 "foo.bar.baz[1][2]",
1169 SpannedExpr::new(
1170 Origin::new(0..17, "foo.bar.baz[1][2]"),
1171 Expr::context(vec![
1172 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1173 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1174 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1175 SpannedExpr::new(
1176 Origin::new(11..14, "[1]"),
1177 Expr::Index(Box::new(SpannedExpr::new(
1178 Origin::new(12..13, "1"),
1179 1.0.into(),
1180 ))),
1181 ),
1182 SpannedExpr::new(
1183 Origin::new(14..17, "[2]"),
1184 Expr::Index(Box::new(SpannedExpr::new(
1185 Origin::new(15..16, "2"),
1186 2.0.into(),
1187 ))),
1188 ),
1189 ]),
1190 ),
1191 ),
1192 (
1193 "foo.bar.baz[*]",
1194 SpannedExpr::new(
1195 Origin::new(0..14, "foo.bar.baz[*]"),
1196 Expr::context([
1197 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1198 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1199 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1200 SpannedExpr::new(
1201 Origin::new(11..14, "[*]"),
1202 Expr::Index(Box::new(SpannedExpr::new(
1203 Origin::new(12..13, "*"),
1204 Expr::Star,
1205 ))),
1206 ),
1207 ]),
1208 ),
1209 ),
1210 (
1211 "vegetables.*.ediblePortions",
1212 SpannedExpr::new(
1213 Origin::new(0..27, "vegetables.*.ediblePortions"),
1214 Expr::context(vec![
1215 SpannedExpr::new(
1216 Origin::new(0..10, "vegetables"),
1217 Expr::ident("vegetables"),
1218 ),
1219 SpannedExpr::new(Origin::new(11..12, "*"), Expr::Star),
1220 SpannedExpr::new(
1221 Origin::new(13..27, "ediblePortions"),
1222 Expr::ident("ediblePortions"),
1223 ),
1224 ]),
1225 ),
1226 ),
1227 (
1228 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1231 SpannedExpr::new(
1232 Origin::new(
1233 0..88,
1234 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1235 ),
1236 Expr::BinOp {
1237 lhs: Box::new(SpannedExpr::new(
1238 Origin::new(
1239 0..59,
1240 "github.ref == 'refs/heads/main' && 'value_for_main_branch'",
1241 ),
1242 Expr::BinOp {
1243 lhs: Box::new(SpannedExpr::new(
1244 Origin::new(0..32, "github.ref == 'refs/heads/main'"),
1245 Expr::BinOp {
1246 lhs: Box::new(SpannedExpr::new(
1247 Origin::new(0..10, "github.ref"),
1248 Expr::context(vec![
1249 SpannedExpr::new(
1250 Origin::new(0..6, "github"),
1251 Expr::ident("github"),
1252 ),
1253 SpannedExpr::new(
1254 Origin::new(7..10, "ref"),
1255 Expr::ident("ref"),
1256 ),
1257 ]),
1258 )),
1259 op: BinOp::Eq,
1260 rhs: Box::new(SpannedExpr::new(
1261 Origin::new(14..31, "'refs/heads/main'"),
1262 Expr::Literal(Literal::String(
1263 "refs/heads/main".into(),
1264 )),
1265 )),
1266 },
1267 )),
1268 op: BinOp::And,
1269 rhs: Box::new(SpannedExpr::new(
1270 Origin::new(35..58, "'value_for_main_branch'"),
1271 Expr::Literal(Literal::String("value_for_main_branch".into())),
1272 )),
1273 },
1274 )),
1275 op: BinOp::Or,
1276 rhs: Box::new(SpannedExpr::new(
1277 Origin::new(62..88, "'value_for_other_branches'"),
1278 Expr::Literal(Literal::String("value_for_other_branches".into())),
1279 )),
1280 },
1281 ),
1282 ),
1283 (
1284 "(true || false) == true",
1285 SpannedExpr::new(
1286 Origin::new(0..23, "(true || false) == true"),
1287 Expr::BinOp {
1288 lhs: Box::new(SpannedExpr::new(
1289 Origin::new(1..14, "true || false"),
1290 Expr::BinOp {
1291 lhs: Box::new(SpannedExpr::new(
1292 Origin::new(1..5, "true"),
1293 true.into(),
1294 )),
1295 op: BinOp::Or,
1296 rhs: Box::new(SpannedExpr::new(
1297 Origin::new(9..14, "false"),
1298 false.into(),
1299 )),
1300 },
1301 )),
1302 op: BinOp::Eq,
1303 rhs: Box::new(SpannedExpr::new(Origin::new(19..23, "true"), true.into())),
1304 },
1305 ),
1306 ),
1307 (
1308 "!(!true || false)",
1309 SpannedExpr::new(
1310 Origin::new(0..17, "!(!true || false)"),
1311 Expr::UnOp {
1312 op: UnOp::Not,
1313 expr: Box::new(SpannedExpr::new(
1314 Origin::new(2..16, "!true || false"),
1315 Expr::BinOp {
1316 lhs: Box::new(SpannedExpr::new(
1317 Origin::new(2..7, "!true"),
1318 Expr::UnOp {
1319 op: UnOp::Not,
1320 expr: Box::new(SpannedExpr::new(
1321 Origin::new(3..7, "true"),
1322 true.into(),
1323 )),
1324 },
1325 )),
1326 op: BinOp::Or,
1327 rhs: Box::new(SpannedExpr::new(
1328 Origin::new(11..16, "false"),
1329 false.into(),
1330 )),
1331 },
1332 )),
1333 },
1334 ),
1335 ),
1336 (
1337 "foobar[format('{0}', 'event')]",
1338 SpannedExpr::new(
1339 Origin::new(0..30, "foobar[format('{0}', 'event')]"),
1340 Expr::context([
1341 SpannedExpr::new(Origin::new(0..6, "foobar"), Expr::ident("foobar")),
1342 SpannedExpr::new(
1343 Origin::new(6..30, "[format('{0}', 'event')]"),
1344 Expr::Index(Box::new(SpannedExpr::new(
1345 Origin::new(7..29, "format('{0}', 'event')"),
1346 Expr::Call(Call {
1347 func: Function("format"),
1348 args: vec![
1349 SpannedExpr::new(
1350 Origin::new(14..19, "'{0}'"),
1351 Expr::from("{0}"),
1352 ),
1353 SpannedExpr::new(
1354 Origin::new(21..28, "'event'"),
1355 Expr::from("event"),
1356 ),
1357 ],
1358 }),
1359 ))),
1360 ),
1361 ]),
1362 ),
1363 ),
1364 (
1365 "github.actor_id == '49699333'",
1366 SpannedExpr::new(
1367 Origin::new(0..29, "github.actor_id == '49699333'"),
1368 Expr::BinOp {
1369 lhs: SpannedExpr::new(
1370 Origin::new(0..15, "github.actor_id"),
1371 Expr::context(vec![
1372 SpannedExpr::new(
1373 Origin::new(0..6, "github"),
1374 Expr::ident("github"),
1375 ),
1376 SpannedExpr::new(
1377 Origin::new(7..15, "actor_id"),
1378 Expr::ident("actor_id"),
1379 ),
1380 ]),
1381 )
1382 .into(),
1383 op: BinOp::Eq,
1384 rhs: Box::new(SpannedExpr::new(
1385 Origin::new(19..29, "'49699333'"),
1386 Expr::from("49699333"),
1387 )),
1388 },
1389 ),
1390 ),
1391 ];
1392
1393 for (case, expr) in cases {
1394 assert_eq!(*expr, Expr::parse(case).unwrap());
1395 }
1396 }
1397
1398 #[test]
1399 fn test_expr_constant_reducible() -> Result<()> {
1400 for (expr, reducible) in &[
1401 ("'foo'", true),
1402 ("1", true),
1403 ("true", true),
1404 ("null", true),
1405 ("!true", true),
1408 ("!null", true),
1409 ("true && false", true),
1410 ("true || false", true),
1411 ("null && !null && true", true),
1412 ("format('{0} {1}', 'foo', 'bar')", true),
1415 ("format('{0} {1}', 1, 2)", true),
1416 ("format('{0} {1}', 1, '2')", true),
1417 ("contains('foo', 'bar')", true),
1418 ("startsWith('foo', 'bar')", true),
1419 ("endsWith('foo', 'bar')", true),
1420 ("startsWith(some.context, 'bar')", false),
1421 ("endsWith(some.context, 'bar')", false),
1422 ("format('{0} {1}', '1', format('{0}', null))", true),
1424 ("format('{0} {1}', '1', startsWith('foo', 'foo'))", true),
1425 ("format('{0} {1}', '1', startsWith(foo.bar, 'foo'))", false),
1426 ("foo", false),
1427 ("foo.bar", false),
1428 ("foo.bar[1]", false),
1429 ("foo.bar == 'bar'", false),
1430 ("foo.bar || bar || baz", false),
1431 ("foo.bar && bar && baz", false),
1432 ] {
1433 let expr = Expr::parse(expr)?;
1434 assert_eq!(expr.constant_reducible(), *reducible);
1435 }
1436
1437 Ok(())
1438 }
1439
1440 #[test]
1441 fn test_evaluate_constant_complex_expressions() -> Result<()> {
1442 use crate::Evaluation;
1443
1444 let test_cases = &[
1445 ("!false", Evaluation::Boolean(true)),
1447 ("!true", Evaluation::Boolean(false)),
1448 ("!(true && false)", Evaluation::Boolean(true)),
1449 ("true && (false || true)", Evaluation::Boolean(true)),
1451 ("false || (true && false)", Evaluation::Boolean(false)),
1452 (
1454 "contains(format('{0} {1}', 'hello', 'world'), 'world')",
1455 Evaluation::Boolean(true),
1456 ),
1457 (
1458 "startsWith(format('prefix_{0}', 'test'), 'prefix')",
1459 Evaluation::Boolean(true),
1460 ),
1461 ];
1462
1463 for (expr_str, expected) in test_cases {
1464 let expr = Expr::parse(expr_str)?;
1465 let result = expr.consteval().unwrap();
1466 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1467 }
1468
1469 Ok(())
1470 }
1471
1472 #[test]
1473 fn test_evaluation_sema_display() {
1474 use crate::Evaluation;
1475
1476 let test_cases = &[
1477 (Evaluation::String("hello".to_string()), "hello"),
1478 (Evaluation::Number(42.0), "42"),
1479 (Evaluation::Number(3.14), "3.14"),
1480 (Evaluation::Boolean(true), "true"),
1481 (Evaluation::Boolean(false), "false"),
1482 (Evaluation::Null, ""),
1483 ];
1484
1485 for (result, expected) in test_cases {
1486 assert_eq!(result.sema().to_string(), *expected);
1487 }
1488 }
1489
1490 #[test]
1491 fn test_evaluation_result_to_boolean() {
1492 use crate::Evaluation;
1493
1494 let test_cases = &[
1495 (Evaluation::Boolean(true), true),
1496 (Evaluation::Boolean(false), false),
1497 (Evaluation::Null, false),
1498 (Evaluation::Number(0.0), false),
1499 (Evaluation::Number(1.0), true),
1500 (Evaluation::Number(-1.0), true),
1501 (Evaluation::String("".to_string()), false),
1502 (Evaluation::String("hello".to_string()), true),
1503 (Evaluation::Array(vec![]), true), (Evaluation::Object(std::collections::HashMap::new()), true), ];
1506
1507 for (result, expected) in test_cases {
1508 assert_eq!(result.as_boolean(), *expected);
1509 }
1510 }
1511
1512 #[test]
1513 fn test_github_actions_logical_semantics() -> Result<()> {
1514 use crate::Evaluation;
1515
1516 let test_cases = &[
1518 ("false && 'hello'", Evaluation::Boolean(false)),
1520 ("null && 'hello'", Evaluation::Null),
1521 ("'' && 'hello'", Evaluation::String("".to_string())),
1522 (
1523 "'hello' && 'world'",
1524 Evaluation::String("world".to_string()),
1525 ),
1526 ("true && 42", Evaluation::Number(42.0)),
1527 ("true || 'hello'", Evaluation::Boolean(true)),
1529 (
1530 "'hello' || 'world'",
1531 Evaluation::String("hello".to_string()),
1532 ),
1533 ("false || 'hello'", Evaluation::String("hello".to_string())),
1534 ("null || false", Evaluation::Boolean(false)),
1535 ("'' || null", Evaluation::Null),
1536 ];
1537
1538 for (expr_str, expected) in test_cases {
1539 let expr = Expr::parse(expr_str)?;
1540 let result = expr.consteval().unwrap();
1541 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1542 }
1543
1544 Ok(())
1545 }
1546
1547 #[test]
1548 fn test_expr_has_constant_reducible_subexpr() -> Result<()> {
1549 for (expr, reducible) in &[
1550 ("'foo'", false),
1552 ("1", false),
1553 ("true", false),
1554 ("null", false),
1555 (
1557 "format('{0}, {1}', github.event.number, format('{0}', 'abc'))",
1558 true,
1559 ),
1560 ("foobar[format('{0}', 'event')]", true),
1561 ] {
1562 let expr = Expr::parse(expr)?;
1563 assert_eq!(!expr.constant_reducible_subexprs().is_empty(), *reducible);
1564 }
1565 Ok(())
1566 }
1567
1568 #[test]
1569 fn test_expr_contexts() -> Result<()> {
1570 let expr = Expr::parse("foo.bar.baz[1].qux")?;
1572 assert_eq!(
1573 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1574 ["foo.bar.baz[1].qux",]
1575 );
1576
1577 let expr = Expr::parse("foo.bar[1].baz || abc.def")?;
1579 assert_eq!(
1580 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1581 ["foo.bar[1].baz", "abc.def",]
1582 );
1583
1584 let expr = Expr::parse("foo.bar[abc.def]")?;
1586 assert_eq!(
1587 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1588 ["foo.bar[abc.def]", "abc.def",]
1589 );
1590
1591 Ok(())
1592 }
1593
1594 #[test]
1595 fn test_expr_dataflow_contexts() -> Result<()> {
1596 let expr = Expr::parse("foo.bar")?;
1598 assert_eq!(
1599 expr.dataflow_contexts()
1600 .iter()
1601 .map(|t| t.1.raw)
1602 .collect::<Vec<_>>(),
1603 ["foo.bar"]
1604 );
1605
1606 let expr = Expr::parse("foo.bar[1]")?;
1607 assert_eq!(
1608 expr.dataflow_contexts()
1609 .iter()
1610 .map(|t| t.1.raw)
1611 .collect::<Vec<_>>(),
1612 ["foo.bar[1]"]
1613 );
1614
1615 let expr = Expr::parse("foo.bar == 'bar'")?;
1617 assert!(expr.dataflow_contexts().is_empty());
1618
1619 let expr = Expr::parse("foo.bar || abc || d.e.f")?;
1621 assert_eq!(
1622 expr.dataflow_contexts()
1623 .iter()
1624 .map(|t| t.1.raw)
1625 .collect::<Vec<_>>(),
1626 ["foo.bar", "abc", "d.e.f"]
1627 );
1628
1629 let expr = Expr::parse("foo.bar && abc && d.e.f")?;
1631 assert_eq!(
1632 expr.dataflow_contexts()
1633 .iter()
1634 .map(|t| t.1.raw)
1635 .collect::<Vec<_>>(),
1636 ["d.e.f"]
1637 );
1638
1639 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || 'false'")?;
1640 assert_eq!(
1641 expr.dataflow_contexts()
1642 .iter()
1643 .map(|t| t.1.raw)
1644 .collect::<Vec<_>>(),
1645 ["foo.bar"]
1646 );
1647
1648 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || foo.baz")?;
1649 assert_eq!(
1650 expr.dataflow_contexts()
1651 .iter()
1652 .map(|t| t.1.raw)
1653 .collect::<Vec<_>>(),
1654 ["foo.bar", "foo.baz"]
1655 );
1656
1657 let expr = Expr::parse("fromJson(steps.runs.outputs.data).workflow_runs[0].id")?;
1658 assert_eq!(
1659 expr.dataflow_contexts()
1660 .iter()
1661 .map(|t| t.1.raw)
1662 .collect::<Vec<_>>(),
1663 ["fromJson(steps.runs.outputs.data).workflow_runs[0].id"]
1664 );
1665
1666 let expr = Expr::parse("format('{0} {1} {2}', foo.bar, tojson(github), toJSON(github))")?;
1667 assert_eq!(
1668 expr.dataflow_contexts()
1669 .iter()
1670 .map(|t| t.1.raw)
1671 .collect::<Vec<_>>(),
1672 ["foo.bar", "github", "github"]
1673 );
1674
1675 Ok(())
1676 }
1677
1678 #[test]
1679 fn test_spannedexpr_computed_indices() -> Result<()> {
1680 for (expr, computed_indices) in &[
1681 ("foo.bar", vec![]),
1682 ("foo.bar[1]", vec![]),
1683 ("foo.bar[*]", vec![]),
1684 ("foo.bar[abc]", vec!["[abc]"]),
1685 (
1686 "foo.bar[format('{0}', 'foo')]",
1687 vec!["[format('{0}', 'foo')]"],
1688 ),
1689 ("foo.bar[abc].def[efg]", vec!["[abc]", "[efg]"]),
1690 ] {
1691 let expr = Expr::parse(expr)?;
1692
1693 assert_eq!(
1694 expr.computed_indices()
1695 .iter()
1696 .map(|e| e.origin.raw)
1697 .collect::<Vec<_>>(),
1698 *computed_indices
1699 );
1700 }
1701
1702 Ok(())
1703 }
1704
1705 #[test]
1706 fn test_fragment_from_expr() {
1707 for (expr, expected) in &[
1708 ("foo==bar", "foo==bar"),
1709 ("foo == bar", "foo == bar"),
1710 ("foo == bar", r"foo == bar"),
1711 ("foo(bar)", "foo(bar)"),
1712 ("foo(bar, baz)", "foo(bar, baz)"),
1713 ("foo (bar, baz)", "foo (bar, baz)"),
1714 ("a . b . c . d", "a . b . c . d"),
1715 ("true \n && \n false", r"true\s+\&\&\s+false"),
1716 ] {
1717 let expr = Expr::parse(expr).unwrap();
1718 match subfeature::Fragment::from(&expr) {
1719 subfeature::Fragment::Raw(actual) => assert_eq!(actual, *expected),
1720 subfeature::Fragment::Regex(actual) => assert_eq!(actual.as_str(), *expected),
1721 };
1722 }
1723 }
1724}