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 itertools::Itertools;
18use pest::{Parser, iterators::Pair};
19
20pub mod call;
21pub mod context;
22pub mod identifier;
23pub mod literal;
24pub mod op;
25
26#[derive(Debug, thiserror::Error)]
28pub enum Error {
29 #[error("Parse error: {0}")]
31 Pest(#[from] pest::error::Error<Rule>),
32 #[error("Invalid function call")]
34 Call(#[from] call::Error),
35}
36
37mod parser {
41 use pest_derive::Parser;
42
43 #[derive(Parser)]
45 #[grammar = "expr.pest"]
46 pub struct ExprParser;
47}
48
49#[derive(Copy, Clone, Debug, PartialEq)]
52pub struct Origin<'src> {
53 pub span: subfeature::Span,
55 pub raw: &'src str,
62}
63
64impl<'a> Origin<'a> {
65 pub fn new(span: impl Into<subfeature::Span>, raw: &'a str) -> Self {
67 Self {
68 span: span.into(),
69 raw: raw.trim(),
70 }
71 }
72}
73
74#[derive(Debug, PartialEq)]
82pub struct SpannedExpr<'src> {
83 pub origin: Origin<'src>,
85 pub inner: Expr<'src>,
87}
88
89impl<'a> SpannedExpr<'a> {
90 pub(crate) fn new(origin: Origin<'a>, inner: Expr<'a>) -> Self {
92 Self { origin, inner }
93 }
94
95 pub fn contexts(&self) -> Vec<(&Context<'a>, &Origin<'a>)> {
104 let mut contexts = vec![];
105
106 match self.deref() {
107 Expr::Index(expr) => contexts.extend(expr.contexts()),
108 Expr::Call(Call { func: _, args }) => {
109 for arg in args {
110 contexts.extend(arg.contexts());
111 }
112 }
113 Expr::Context(ctx) => {
114 contexts.push((ctx, &self.origin));
116
117 ctx.parts
120 .iter()
121 .for_each(|part| contexts.extend(part.contexts()));
122 }
123 Expr::BinOp { lhs, op: _, rhs } => {
124 contexts.extend(lhs.contexts());
125 contexts.extend(rhs.contexts());
126 }
127 Expr::UnOp { op: _, expr } => contexts.extend(expr.contexts()),
128 _ => (),
129 }
130
131 contexts
132 }
133
134 pub fn dataflow_contexts(&self) -> Vec<(&Context<'a>, &Origin<'a>)> {
143 let mut contexts = vec![];
144
145 match self.deref() {
146 Expr::Call(Call { func, args }) => {
147 if matches!(func, Function::ToJSON | Function::Format | Function::Join) {
151 for arg in args {
152 contexts.extend(arg.dataflow_contexts());
153 }
154 }
155 }
156 Expr::Context(ctx) => contexts.push((ctx, &self.origin)),
163 Expr::BinOp { lhs, op, rhs } => match op {
164 BinOp::And => {
167 contexts.extend(rhs.dataflow_contexts());
168 }
169 BinOp::Or => {
171 contexts.extend(lhs.dataflow_contexts());
172 contexts.extend(rhs.dataflow_contexts());
173 }
174 _ => (),
175 },
176 _ => (),
177 }
178
179 contexts
180 }
181
182 pub fn leaf_expressions(&self) -> Vec<&SpannedExpr<'a>> {
196 let mut leaves = vec![];
197
198 match self.deref() {
199 Expr::BinOp { lhs, op, rhs } => match op {
200 BinOp::And => {
201 leaves.extend(rhs.leaf_expressions());
202 }
203 BinOp::Or => {
204 leaves.extend(lhs.leaf_expressions());
205 leaves.extend(rhs.leaf_expressions());
206 }
207 _ => leaves.push(self),
209 },
210 _ => leaves.push(self),
211 }
212
213 leaves
214 }
215
216 pub fn computed_indices(&self) -> Vec<&SpannedExpr<'a>> {
221 let mut index_exprs = vec![];
222
223 match self.deref() {
224 Expr::Call(Call { func: _, args }) => {
225 for arg in args {
226 index_exprs.extend(arg.computed_indices());
227 }
228 }
229 Expr::Index(spanned_expr)
230 if !spanned_expr.is_literal() && !matches!(spanned_expr.inner, Expr::Star) => {
232 index_exprs.push(self);
233 }
234 Expr::Context(context) => {
235 for part in &context.parts {
236 index_exprs.extend(part.computed_indices());
237 }
238 }
239 Expr::BinOp { lhs, op: _, rhs } => {
240 index_exprs.extend(lhs.computed_indices());
241 index_exprs.extend(rhs.computed_indices());
242 }
243 Expr::UnOp { op: _, expr } => {
244 index_exprs.extend(expr.computed_indices());
245 }
246 _ => {}
247 }
248
249 index_exprs
250 }
251
252 pub fn constant_reducible_subexprs(&self) -> Vec<&SpannedExpr<'a>> {
260 if !self.is_literal() && self.constant_reducible() {
261 return vec![self];
262 }
263
264 let mut subexprs = vec![];
265
266 match self.deref() {
267 Expr::Call(Call { func: _, args }) => {
268 for arg in args {
269 subexprs.extend(arg.constant_reducible_subexprs());
270 }
271 }
272 Expr::Context(ctx) => {
273 for part in &ctx.parts {
276 subexprs.extend(part.constant_reducible_subexprs());
277 }
278 }
279 Expr::BinOp { lhs, op: _, rhs } => {
280 subexprs.extend(lhs.constant_reducible_subexprs());
281 subexprs.extend(rhs.constant_reducible_subexprs());
282 }
283 Expr::UnOp { op: _, expr } => subexprs.extend(expr.constant_reducible_subexprs()),
284
285 Expr::Index(expr) => subexprs.extend(expr.constant_reducible_subexprs()),
286 _ => {}
287 }
288
289 subexprs
290 }
291}
292
293impl<'a> Deref for SpannedExpr<'a> {
294 type Target = Expr<'a>;
295
296 fn deref(&self) -> &Self::Target {
297 &self.inner
298 }
299}
300
301impl<'doc> From<&SpannedExpr<'doc>> for subfeature::Fragment<'doc> {
302 fn from(expr: &SpannedExpr<'doc>) -> Self {
303 Self::new(expr.origin.raw)
304 }
305}
306
307#[derive(Debug, PartialEq)]
309pub enum Expr<'src> {
310 Literal(Literal<'src>),
312 Star,
314 Call(Call<'src>),
316 Identifier(Identifier<'src>),
318 Index(Box<SpannedExpr<'src>>),
320 Context(Context<'src>),
322 BinOp {
324 lhs: Box<SpannedExpr<'src>>,
326 op: BinOp,
328 rhs: Box<SpannedExpr<'src>>,
330 },
331 UnOp {
333 op: UnOp,
335 expr: Box<SpannedExpr<'src>>,
337 },
338}
339
340impl<'src> Expr<'src> {
341 fn ident(i: &'src str) -> Self {
343 Self::Identifier(Identifier(i))
344 }
345
346 fn context(components: impl Into<Vec<SpannedExpr<'src>>>) -> Self {
348 Self::Context(Context::new(components))
349 }
350
351 pub fn is_literal(&self) -> bool {
353 matches!(self, Expr::Literal(_))
354 }
355
356 pub fn constant_reducible(&self) -> bool {
375 match self {
376 Expr::Literal(_) => true,
378 Expr::BinOp { lhs, op: _, rhs } => lhs.constant_reducible() && rhs.constant_reducible(),
380 Expr::UnOp { op: _, expr } => expr.constant_reducible(),
382 Expr::Call(Call { func, args }) => {
383 if matches!(
388 func,
389 Function::Contains
390 | Function::StartsWith
391 | Function::EndsWith
392 | Function::Format
393 | Function::ToJSON
394 | Function::Join ) {
396 args.iter().all(|e| e.constant_reducible())
397 } else {
398 false
399 }
400 }
401 _ => false,
403 }
404 }
405
406 #[allow(clippy::unwrap_used)]
408 pub fn parse(expr: &'src str) -> Result<SpannedExpr<'src>, Error> {
409 let or_expr = ExprParser::parse(Rule::expression, expr)?
411 .next()
412 .unwrap()
413 .into_inner()
414 .next()
415 .unwrap();
416
417 fn parse_pair(pair: Pair<'_, Rule>) -> Result<Box<SpannedExpr<'_>>, Error> {
418 match pair.as_rule() {
430 Rule::or_expr => {
431 let (span, raw) = (pair.as_span(), pair.as_str());
432 let mut pairs = pair.into_inner();
433 let lhs = parse_pair(pairs.next().unwrap())?;
434 pairs.try_fold(lhs, |expr, next| {
435 Ok(SpannedExpr::new(
436 Origin::new(span.start()..span.end(), raw),
437 Expr::BinOp {
438 lhs: expr,
439 op: BinOp::Or,
440 rhs: parse_pair(next)?,
441 },
442 )
443 .into())
444 })
445 }
446 Rule::and_expr => {
447 let (span, raw) = (pair.as_span(), pair.as_str());
448 let mut pairs = pair.into_inner();
449 let lhs = parse_pair(pairs.next().unwrap())?;
450 pairs.try_fold(lhs, |expr, next| {
451 Ok(SpannedExpr::new(
452 Origin::new(span.start()..span.end(), raw),
453 Expr::BinOp {
454 lhs: expr,
455 op: BinOp::And,
456 rhs: parse_pair(next)?,
457 },
458 )
459 .into())
460 })
461 }
462 Rule::eq_expr => {
463 let (span, raw) = (pair.as_span(), pair.as_str());
467 let mut pairs = pair.into_inner();
468 let lhs = parse_pair(pairs.next().unwrap())?;
469
470 let pair_chunks = pairs.chunks(2);
471 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
472 let eq_op = next.next().unwrap();
473 let comp_expr = next.next().unwrap();
474
475 let eq_op = match eq_op.as_str() {
476 "==" => BinOp::Eq,
477 "!=" => BinOp::Neq,
478 _ => unreachable!(),
479 };
480
481 Ok(SpannedExpr::new(
482 Origin::new(span.start()..span.end(), raw),
483 Expr::BinOp {
484 lhs: expr,
485 op: eq_op,
486 rhs: parse_pair(comp_expr)?,
487 },
488 )
489 .into())
490 })
491 }
492 Rule::comp_expr => {
493 let (span, raw) = (pair.as_span(), pair.as_str());
495 let mut pairs = pair.into_inner();
496 let lhs = parse_pair(pairs.next().unwrap())?;
497
498 let pair_chunks = pairs.chunks(2);
499 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
500 let comp_op = next.next().unwrap();
501 let unary_expr = next.next().unwrap();
502
503 let eq_op = match comp_op.as_str() {
504 ">" => BinOp::Gt,
505 ">=" => BinOp::Ge,
506 "<" => BinOp::Lt,
507 "<=" => BinOp::Le,
508 _ => unreachable!(),
509 };
510
511 Ok(SpannedExpr::new(
512 Origin::new(span.start()..span.end(), raw),
513 Expr::BinOp {
514 lhs: expr,
515 op: eq_op,
516 rhs: parse_pair(unary_expr)?,
517 },
518 )
519 .into())
520 })
521 }
522 Rule::unary_expr => {
523 let (span, raw) = (pair.as_span(), pair.as_str());
524 let mut pairs = pair.into_inner();
525 let inner_pair = pairs.next().unwrap();
526
527 match inner_pair.as_rule() {
528 Rule::unary_op => Ok(SpannedExpr::new(
529 Origin::new(span.start()..span.end(), raw),
530 Expr::UnOp {
531 op: UnOp::Not,
532 expr: parse_pair(pairs.next().unwrap())?,
533 },
534 )
535 .into()),
536 Rule::primary_expr => parse_pair(inner_pair),
537 _ => unreachable!(),
538 }
539 }
540 Rule::primary_expr => {
541 parse_pair(pair.into_inner().next().unwrap())
543 }
544 Rule::number => Ok(SpannedExpr::new(
545 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
546 parse_number(pair.as_str()).into(),
547 )
548 .into()),
549 Rule::string => {
550 let (span, raw) = (pair.as_span(), pair.as_str());
551 let string_inner = pair.into_inner().next().unwrap().as_str();
553
554 if !string_inner.contains('\'') {
557 Ok(SpannedExpr::new(
558 Origin::new(span.start()..span.end(), raw),
559 string_inner.into(),
560 )
561 .into())
562 } else {
563 Ok(SpannedExpr::new(
564 Origin::new(span.start()..span.end(), raw),
565 string_inner.replace("''", "'").into(),
566 )
567 .into())
568 }
569 }
570 Rule::boolean => Ok(SpannedExpr::new(
571 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
572 pair.as_str().parse::<bool>().unwrap().into(),
573 )
574 .into()),
575 Rule::null => Ok(SpannedExpr::new(
576 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
577 Expr::Literal(Literal::Null),
578 )
579 .into()),
580 Rule::star => Ok(SpannedExpr::new(
581 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
582 Expr::Star,
583 )
584 .into()),
585 Rule::function_call => {
586 let (span, raw) = (pair.as_span(), pair.as_str());
587 let mut pairs = pair.into_inner();
588
589 let identifier = pairs.next().unwrap();
590 let args = pairs
591 .map(|pair| parse_pair(pair).map(|e| *e))
592 .collect::<Result<_, _>>()?;
593
594 let call = Call::new(identifier.as_str(), args)?;
595
596 Ok(SpannedExpr::new(
597 Origin::new(span.start()..span.end(), raw),
598 Expr::Call(call),
599 )
600 .into())
601 }
602 Rule::identifier => Ok(SpannedExpr::new(
603 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
604 Expr::ident(pair.as_str()),
605 )
606 .into()),
607 Rule::index => Ok(SpannedExpr::new(
608 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
609 Expr::Index(parse_pair(pair.into_inner().next().unwrap())?),
610 )
611 .into()),
612 Rule::context => {
613 let (span, raw) = (pair.as_span(), pair.as_str());
614 let pairs = pair.into_inner();
615
616 let mut inner: Vec<SpannedExpr> = pairs
617 .map(|pair| parse_pair(pair).map(|e| *e))
618 .collect::<Result<_, _>>()?;
619
620 if inner.len() == 1 && !matches!(inner[0].inner, Expr::Identifier(_)) {
626 Ok(inner.remove(0).into())
627 } else {
628 Ok(SpannedExpr::new(
629 Origin::new(span.start()..span.end(), raw),
630 Expr::context(inner),
631 )
632 .into())
633 }
634 }
635 r => panic!("unrecognized rule: {r:?}"),
636 }
637 }
638
639 parse_pair(or_expr).map(|e| *e)
640 }
641}
642
643impl<'src> From<&'src str> for Expr<'src> {
644 fn from(s: &'src str) -> Self {
645 Expr::Literal(Literal::String(s.into()))
646 }
647}
648
649impl From<String> for Expr<'_> {
650 fn from(s: String) -> Self {
651 Expr::Literal(Literal::String(s.into()))
652 }
653}
654
655impl From<f64> for Expr<'_> {
656 fn from(n: f64) -> Self {
657 Expr::Literal(Literal::Number(n))
658 }
659}
660
661impl From<bool> for Expr<'_> {
662 fn from(b: bool) -> Self {
663 Expr::Literal(Literal::Boolean(b))
664 }
665}
666
667#[derive(Debug, Clone, PartialEq)]
672pub enum Evaluation {
673 String(String),
675 Number(f64),
677 Boolean(bool),
679 Null,
681 Array(Vec<Evaluation>),
683 Object(std::collections::HashMap<String, Evaluation>),
685}
686
687impl TryFrom<serde_json::Value> for Evaluation {
688 type Error = ();
689
690 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
691 match value {
692 serde_json::Value::Null => Ok(Evaluation::Null),
693 serde_json::Value::Bool(b) => Ok(Evaluation::Boolean(b)),
694 serde_json::Value::Number(n) => {
695 if let Some(f) = n.as_f64() {
696 Ok(Evaluation::Number(f))
697 } else {
698 Err(())
699 }
700 }
701 serde_json::Value::String(s) => Ok(Evaluation::String(s)),
702 serde_json::Value::Array(arr) => {
703 let elements = arr
704 .into_iter()
705 .map(|elem| elem.try_into())
706 .collect::<Result<_, _>>()?;
707 Ok(Evaluation::Array(elements))
708 }
709 serde_json::Value::Object(obj) => {
710 let mut map = std::collections::HashMap::new();
711 for (key, value) in obj {
712 map.insert(key, value.try_into()?);
713 }
714 Ok(Evaluation::Object(map))
715 }
716 }
717 }
718}
719
720impl TryInto<serde_json::Value> for Evaluation {
721 type Error = ();
722
723 fn try_into(self) -> Result<serde_json::Value, Self::Error> {
724 match self {
725 Evaluation::Null => Ok(serde_json::Value::Null),
726 Evaluation::Boolean(b) => Ok(serde_json::Value::Bool(b)),
727 Evaluation::Number(n) => {
728 if n.fract() == 0.0 {
732 Ok(serde_json::Value::Number(serde_json::Number::from(
733 n as i64,
734 )))
735 } else if let Some(num) = serde_json::Number::from_f64(n) {
736 Ok(serde_json::Value::Number(num))
737 } else {
738 Err(())
739 }
740 }
741 Evaluation::String(s) => Ok(serde_json::Value::String(s)),
742 Evaluation::Array(arr) => {
743 let elements = arr
744 .into_iter()
745 .map(|elem| elem.try_into())
746 .collect::<Result<_, _>>()?;
747 Ok(serde_json::Value::Array(elements))
748 }
749 Evaluation::Object(obj) => {
750 let mut map = serde_json::Map::new();
751 for (key, value) in obj {
752 map.insert(key, value.try_into()?);
753 }
754 Ok(serde_json::Value::Object(map))
755 }
756 }
757 }
758}
759
760impl Evaluation {
761 pub fn as_boolean(&self) -> bool {
769 match self {
770 Evaluation::Boolean(b) => *b,
771 Evaluation::Null => false,
772 Evaluation::Number(n) => *n != 0.0 && !n.is_nan(),
773 Evaluation::String(s) => !s.is_empty(),
774 Evaluation::Array(_) | Evaluation::Object(_) => true,
776 }
777 }
778
779 pub fn as_number(&self) -> f64 {
783 match self {
784 Evaluation::String(s) => parse_number(s),
785 Evaluation::Number(n) => *n,
786 Evaluation::Boolean(b) => {
787 if *b {
788 1.0
789 } else {
790 0.0
791 }
792 }
793 Evaluation::Null => 0.0,
794 Evaluation::Array(_) | Evaluation::Object(_) => f64::NAN,
795 }
796 }
797
798 pub fn sema(&self) -> EvaluationSema<'_> {
801 EvaluationSema(self)
802 }
803}
804
805fn parse_number(s: &str) -> f64 {
811 let trimmed = s.trim();
812 if trimmed.is_empty() {
813 return 0.0;
814 }
815
816 if let Ok(value) = trimmed.parse::<f64>()
819 && value.is_finite()
820 {
821 return value;
822 }
823
824 if let Some(hex_digits) = trimmed.strip_prefix("0x") {
827 return u32::from_str_radix(hex_digits, 16)
828 .map(|n| (n as i32) as f64)
829 .unwrap_or(f64::NAN);
830 }
831
832 if let Some(oct_digits) = trimmed.strip_prefix("0o") {
834 return u32::from_str_radix(oct_digits, 8)
835 .map(|n| (n as i32) as f64)
836 .unwrap_or(f64::NAN);
837 }
838
839 let after_sign = trimmed
842 .strip_prefix(['+', '-'].as_slice())
843 .unwrap_or(trimmed);
844 if after_sign.eq_ignore_ascii_case("infinity") {
845 return if trimmed.starts_with('-') {
846 f64::NEG_INFINITY
847 } else {
848 f64::INFINITY
849 };
850 }
851
852 f64::NAN
853}
854
855pub struct EvaluationSema<'a>(&'a Evaluation);
858
859impl EvaluationSema<'_> {
860 fn upper_special(value: &str) -> String {
865 let mut result = String::with_capacity(value.len());
868 let mut parts = value.split('ı');
869 if let Some(first) = parts.next() {
870 result.extend(first.chars().flat_map(char::to_uppercase));
871 }
872 for part in parts {
873 result.push('ı');
874 result.extend(part.chars().flat_map(char::to_uppercase));
875 }
876 result
877 }
878}
879
880impl PartialEq for EvaluationSema<'_> {
881 fn eq(&self, other: &Self) -> bool {
882 match (self.0, other.0) {
883 (Evaluation::Null, Evaluation::Null) => true,
884 (Evaluation::Boolean(a), Evaluation::Boolean(b)) => a == b,
885 (Evaluation::Number(a), Evaluation::Number(b)) => a == b,
886 (Evaluation::String(a), Evaluation::String(b)) => {
888 Self::upper_special(a) == Self::upper_special(b)
889 }
890 (a, b) => a.as_number() == b.as_number(),
892 }
893 }
894}
895
896impl PartialOrd for EvaluationSema<'_> {
897 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
898 match (self.0, other.0) {
899 (Evaluation::Null, Evaluation::Null) => Some(std::cmp::Ordering::Equal),
900 (Evaluation::Boolean(a), Evaluation::Boolean(b)) => a.partial_cmp(b),
901 (Evaluation::Number(a), Evaluation::Number(b)) => a.partial_cmp(b),
902 (Evaluation::String(a), Evaluation::String(b)) => {
903 Self::upper_special(a).partial_cmp(&Self::upper_special(b))
905 }
906 (a, b) => a.as_number().partial_cmp(&b.as_number()),
908 }
909 }
910}
911
912impl std::fmt::Display for EvaluationSema<'_> {
913 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
914 match self.0 {
915 Evaluation::String(s) => write!(f, "{}", s),
916 Evaluation::Number(n) => {
917 if n == &f64::INFINITY {
919 write!(f, "Infinity")
920 } else if n == &f64::NEG_INFINITY {
921 write!(f, "-Infinity")
922 } else {
923 let rounded: f64 = format!("{:.15}", n)
927 .parse()
928 .expect("impossible f64 round-trip error");
929 if rounded.fract() == 0.0 {
930 write!(f, "{}", rounded as i64)
931 } else {
932 write!(f, "{}", rounded)
933 }
934 }
935 }
936 Evaluation::Boolean(b) => write!(f, "{}", b),
937 Evaluation::Null => write!(f, ""),
938 Evaluation::Array(_) => write!(f, "Array"),
939 Evaluation::Object(_) => write!(f, "Object"),
940 }
941 }
942}
943
944impl<'src> Expr<'src> {
945 pub fn consteval(&self) -> Option<Evaluation> {
968 match self {
969 Expr::Literal(literal) => Some(literal.consteval()),
970
971 Expr::BinOp { lhs, op, rhs } => {
972 let lhs_val = lhs.consteval()?;
973 let rhs_val = rhs.consteval()?;
974
975 match op {
976 BinOp::And => {
977 if lhs_val.as_boolean() {
979 Some(rhs_val)
980 } else {
981 Some(lhs_val)
982 }
983 }
984 BinOp::Or => {
985 if lhs_val.as_boolean() {
987 Some(lhs_val)
988 } else {
989 Some(rhs_val)
990 }
991 }
992 BinOp::Eq => Some(Evaluation::Boolean(lhs_val.sema() == rhs_val.sema())),
993 BinOp::Neq => Some(Evaluation::Boolean(lhs_val.sema() != rhs_val.sema())),
994 BinOp::Lt => Some(Evaluation::Boolean(lhs_val.sema() < rhs_val.sema())),
995 BinOp::Le => Some(Evaluation::Boolean(lhs_val.sema() <= rhs_val.sema())),
996 BinOp::Gt => Some(Evaluation::Boolean(lhs_val.sema() > rhs_val.sema())),
997 BinOp::Ge => Some(Evaluation::Boolean(lhs_val.sema() >= rhs_val.sema())),
998 }
999 }
1000
1001 Expr::UnOp { op, expr } => {
1002 let val = expr.consteval()?;
1003 match op {
1004 UnOp::Not => Some(Evaluation::Boolean(!val.as_boolean())),
1005 }
1006 }
1007
1008 Expr::Call(call) => call.consteval(),
1009
1010 _ => None,
1012 }
1013 }
1014}
1015
1016#[cfg(test)]
1017mod tests {
1018 use std::borrow::Cow;
1019
1020 use pest::Parser as _;
1021 use pretty_assertions::assert_eq;
1022
1023 use crate::{Call, Error, Literal, Origin, SpannedExpr};
1024
1025 use super::{BinOp, Expr, ExprParser, Function, Rule, UnOp};
1026
1027 #[test]
1028 fn test_literal_string_borrows() {
1029 let cases = &[
1030 ("'foo'", true),
1031 ("'foo bar'", true),
1032 ("'foo '' bar'", false),
1033 ("'foo''bar'", false),
1034 ("'foo''''bar'", false),
1035 ];
1036
1037 for (expr, borrows) in cases {
1038 let Expr::Literal(Literal::String(s)) = &*Expr::parse(expr).unwrap() else {
1039 panic!("expected a literal string expression for {expr}");
1040 };
1041
1042 assert!(matches!(
1043 (s, borrows),
1044 (Cow::Borrowed(_), true) | (Cow::Owned(_), false)
1045 ));
1046 }
1047 }
1048
1049 #[test]
1050 fn test_literal_as_str() {
1051 let cases = &[
1052 ("'foo'", "foo"),
1053 ("'foo '' bar'", "foo ' bar"),
1054 ("123", "123"),
1055 ("123.000", "123"),
1056 ("0.0", "0"),
1057 ("0.1", "0.1"),
1058 ("0.12345", "0.12345"),
1059 ("true", "true"),
1060 ("false", "false"),
1061 ("null", "null"),
1062 ];
1063
1064 for (expr, expected) in cases {
1065 let Expr::Literal(expr) = &*Expr::parse(expr).unwrap() else {
1066 panic!("expected a literal expression for {expr}");
1067 };
1068
1069 assert_eq!(expr.as_str(), *expected);
1070 }
1071 }
1072
1073 #[test]
1074 fn test_parse_string_rule() {
1075 let cases = &[
1076 ("''", ""),
1077 ("' '", " "),
1078 ("''''", "''"),
1079 ("'test'", "test"),
1080 ("'spaces are ok'", "spaces are ok"),
1081 ("'escaping '' works'", "escaping '' works"),
1082 ];
1083
1084 for (case, expected) in cases {
1085 let s = ExprParser::parse(Rule::string, case)
1086 .unwrap()
1087 .next()
1088 .unwrap();
1089
1090 assert_eq!(s.into_inner().next().unwrap().as_str(), *expected);
1091 }
1092 }
1093
1094 #[test]
1095 fn test_parse_context_rule() {
1096 let cases = &[
1097 "foo.bar",
1098 "github.action_path",
1099 "inputs.foo-bar",
1100 "inputs.also--valid",
1101 "inputs.this__too",
1102 "inputs.this__too",
1103 "secrets.GH_TOKEN",
1104 "foo.*.bar",
1105 "github.event.issue.labels.*.name",
1106 ];
1107
1108 for case in cases {
1109 assert_eq!(
1110 ExprParser::parse(Rule::context, case)
1111 .unwrap()
1112 .next()
1113 .unwrap()
1114 .as_str(),
1115 *case
1116 );
1117 }
1118 }
1119
1120 #[test]
1121 fn test_parse_call_rule() {
1122 let cases = &[
1123 "foo()",
1124 "foo(bar)",
1125 "foo(bar())",
1126 "foo(1.23)",
1127 "foo(1,2)",
1128 "foo(1, 2)",
1129 "foo(1, 2, secret.GH_TOKEN)",
1130 "foo( )",
1131 "fromJSON(inputs.free-threading)",
1132 ];
1133
1134 for case in cases {
1135 assert_eq!(
1136 ExprParser::parse(Rule::function_call, case)
1137 .unwrap()
1138 .next()
1139 .unwrap()
1140 .as_str(),
1141 *case
1142 );
1143 }
1144 }
1145
1146 #[test]
1147 fn test_parse_expr_rule() -> Result<(), Error> {
1148 let multiline = "github.repository_owner == 'Homebrew' &&
1150 ((github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
1151 (github.event_name == 'pull_request_target' &&
1152 (github.event.action == 'ready_for_review' || github.event.label.name == 'automerge-skip')))";
1153
1154 let multiline2 = "foo.bar.baz[
1155 0
1156 ]";
1157
1158 let cases = &[
1159 "true",
1160 "fromJSON(inputs.free-threading) && '--disable-gil' || ''",
1161 "foo || bar || baz",
1162 "foo || bar && baz || foo && 1 && 2 && 3 || 4",
1163 "(github.actor != 'github-actions[bot]' && github.actor) || 'BrewTestBot'",
1164 "(true || false) == true",
1165 "!(!true || false)",
1166 "!(!true || false) == true",
1167 "(true == false) == true",
1168 "(true == (false || true && (true || false))) == true",
1169 "(github.actor != 'github-actions[bot]' && github.actor) == 'BrewTestBot'",
1170 "foo()[0]",
1171 "fromJson(steps.runs.outputs.data).workflow_runs[0].id",
1172 multiline,
1173 "'a' == 'b' && 'c' || 'd'",
1174 "github.event['a']",
1175 "github.event['a' == 'b']",
1176 "github.event['a' == 'b' && 'c' || 'd']",
1177 "github['event']['inputs']['dry-run']",
1178 "github[format('{0}', 'event')]",
1179 "github['event']['inputs'][github.event.inputs.magic]",
1180 "github['event']['inputs'].*",
1181 "1 == 1",
1182 "1 > 1",
1183 "1 >= 1",
1184 "matrix.node_version >= 20",
1185 "true||false",
1186 "0xFF",
1188 "0xff",
1189 "0x0",
1190 "0xFF == 255",
1191 "0o10",
1193 "0o77",
1194 "0o0",
1195 "1e2",
1197 "1.5E-3",
1198 "1.2e+2",
1199 "5e0",
1200 "NaN",
1202 "Infinity",
1203 "+Infinity",
1204 "-Infinity",
1205 "NaN == NaN",
1206 "Infinity == Infinity",
1207 "2 <= (3 == true)",
1209 "0 > (0 < 1)",
1210 "(foo || bar) == baz",
1211 "+42",
1213 "-42",
1214 ".5",
1216 "123.",
1217 multiline2,
1219 "fromJSON( github.event.inputs.hmm ) [ 0 ]",
1220 "(fromJson('{\"one\": \"one val\"}')).one",
1222 "(fromJson('[\"one\", \"two\"]'))[1]",
1223 ];
1224
1225 for case in cases {
1226 assert_eq!(
1227 ExprParser::parse(Rule::expression, case)?
1228 .next()
1229 .unwrap()
1230 .as_str(),
1231 *case
1232 );
1233 }
1234
1235 Ok(())
1236 }
1237
1238 #[test]
1239 fn test_parse_expr_rule_rejects() {
1240 let cases = &[
1241 "-Inf", "+Inf",
1243 ];
1244
1245 for case in cases {
1246 assert!(
1247 ExprParser::parse(Rule::expression, case).is_err(),
1248 "{case:?} should not parse as a valid expression"
1249 );
1250 }
1251 }
1252
1253 #[test]
1254 fn test_parse() {
1255 let cases = &[
1256 (
1257 "!true || false || true",
1258 SpannedExpr::new(
1259 Origin::new(0..22, "!true || false || true"),
1260 Expr::BinOp {
1261 lhs: SpannedExpr::new(
1262 Origin::new(0..22, "!true || false || true"),
1263 Expr::BinOp {
1264 lhs: SpannedExpr::new(
1265 Origin::new(0..5, "!true"),
1266 Expr::UnOp {
1267 op: UnOp::Not,
1268 expr: SpannedExpr::new(
1269 Origin::new(1..5, "true"),
1270 true.into(),
1271 )
1272 .into(),
1273 },
1274 )
1275 .into(),
1276 op: BinOp::Or,
1277 rhs: SpannedExpr::new(Origin::new(9..14, "false"), false.into())
1278 .into(),
1279 },
1280 )
1281 .into(),
1282 op: BinOp::Or,
1283 rhs: SpannedExpr::new(Origin::new(18..22, "true"), true.into()).into(),
1284 },
1285 ),
1286 ),
1287 (
1288 "'foo '' bar'",
1289 SpannedExpr::new(
1290 Origin::new(0..12, "'foo '' bar'"),
1291 Expr::Literal(Literal::String("foo ' bar".into())),
1292 ),
1293 ),
1294 (
1295 "('foo '' bar')",
1296 SpannedExpr::new(
1297 Origin::new(1..13, "'foo '' bar'"),
1298 Expr::Literal(Literal::String("foo ' bar".into())),
1299 ),
1300 ),
1301 (
1302 "((('foo '' bar')))",
1303 SpannedExpr::new(
1304 Origin::new(3..15, "'foo '' bar'"),
1305 Expr::Literal(Literal::String("foo ' bar".into())),
1306 ),
1307 ),
1308 (
1309 "format('{0} {1}', 2, 3)",
1310 SpannedExpr::new(
1311 Origin::new(0..23, "format('{0} {1}', 2, 3)"),
1312 Expr::Call(Call {
1313 func: Function::Format,
1314 args: vec![
1315 SpannedExpr::new(Origin::new(7..16, "'{0} {1}'"), "{0} {1}".into()),
1316 SpannedExpr::new(Origin::new(18..19, "2"), 2.0.into()),
1317 SpannedExpr::new(Origin::new(21..22, "3"), 3.0.into()),
1318 ],
1319 }),
1320 ),
1321 ),
1322 (
1323 "foo.bar.baz",
1324 SpannedExpr::new(
1325 Origin::new(0..11, "foo.bar.baz"),
1326 Expr::context(vec![
1327 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1328 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1329 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1330 ]),
1331 ),
1332 ),
1333 (
1334 "foo.bar.baz[1][2]",
1335 SpannedExpr::new(
1336 Origin::new(0..17, "foo.bar.baz[1][2]"),
1337 Expr::context(vec![
1338 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1339 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1340 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1341 SpannedExpr::new(
1342 Origin::new(11..14, "[1]"),
1343 Expr::Index(Box::new(SpannedExpr::new(
1344 Origin::new(12..13, "1"),
1345 1.0.into(),
1346 ))),
1347 ),
1348 SpannedExpr::new(
1349 Origin::new(14..17, "[2]"),
1350 Expr::Index(Box::new(SpannedExpr::new(
1351 Origin::new(15..16, "2"),
1352 2.0.into(),
1353 ))),
1354 ),
1355 ]),
1356 ),
1357 ),
1358 (
1359 "foo.bar.baz[*]",
1360 SpannedExpr::new(
1361 Origin::new(0..14, "foo.bar.baz[*]"),
1362 Expr::context([
1363 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1364 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1365 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1366 SpannedExpr::new(
1367 Origin::new(11..14, "[*]"),
1368 Expr::Index(Box::new(SpannedExpr::new(
1369 Origin::new(12..13, "*"),
1370 Expr::Star,
1371 ))),
1372 ),
1373 ]),
1374 ),
1375 ),
1376 (
1377 "vegetables.*.ediblePortions",
1378 SpannedExpr::new(
1379 Origin::new(0..27, "vegetables.*.ediblePortions"),
1380 Expr::context(vec![
1381 SpannedExpr::new(
1382 Origin::new(0..10, "vegetables"),
1383 Expr::ident("vegetables"),
1384 ),
1385 SpannedExpr::new(Origin::new(11..12, "*"), Expr::Star),
1386 SpannedExpr::new(
1387 Origin::new(13..27, "ediblePortions"),
1388 Expr::ident("ediblePortions"),
1389 ),
1390 ]),
1391 ),
1392 ),
1393 (
1394 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1397 SpannedExpr::new(
1398 Origin::new(
1399 0..88,
1400 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1401 ),
1402 Expr::BinOp {
1403 lhs: Box::new(SpannedExpr::new(
1404 Origin::new(
1405 0..59,
1406 "github.ref == 'refs/heads/main' && 'value_for_main_branch'",
1407 ),
1408 Expr::BinOp {
1409 lhs: Box::new(SpannedExpr::new(
1410 Origin::new(0..32, "github.ref == 'refs/heads/main'"),
1411 Expr::BinOp {
1412 lhs: Box::new(SpannedExpr::new(
1413 Origin::new(0..10, "github.ref"),
1414 Expr::context(vec![
1415 SpannedExpr::new(
1416 Origin::new(0..6, "github"),
1417 Expr::ident("github"),
1418 ),
1419 SpannedExpr::new(
1420 Origin::new(7..10, "ref"),
1421 Expr::ident("ref"),
1422 ),
1423 ]),
1424 )),
1425 op: BinOp::Eq,
1426 rhs: Box::new(SpannedExpr::new(
1427 Origin::new(14..31, "'refs/heads/main'"),
1428 Expr::Literal(Literal::String(
1429 "refs/heads/main".into(),
1430 )),
1431 )),
1432 },
1433 )),
1434 op: BinOp::And,
1435 rhs: Box::new(SpannedExpr::new(
1436 Origin::new(35..58, "'value_for_main_branch'"),
1437 Expr::Literal(Literal::String("value_for_main_branch".into())),
1438 )),
1439 },
1440 )),
1441 op: BinOp::Or,
1442 rhs: Box::new(SpannedExpr::new(
1443 Origin::new(62..88, "'value_for_other_branches'"),
1444 Expr::Literal(Literal::String("value_for_other_branches".into())),
1445 )),
1446 },
1447 ),
1448 ),
1449 (
1450 "(true || false) == true",
1451 SpannedExpr::new(
1452 Origin::new(0..23, "(true || false) == true"),
1453 Expr::BinOp {
1454 lhs: Box::new(SpannedExpr::new(
1455 Origin::new(1..14, "true || false"),
1456 Expr::BinOp {
1457 lhs: Box::new(SpannedExpr::new(
1458 Origin::new(1..5, "true"),
1459 true.into(),
1460 )),
1461 op: BinOp::Or,
1462 rhs: Box::new(SpannedExpr::new(
1463 Origin::new(9..14, "false"),
1464 false.into(),
1465 )),
1466 },
1467 )),
1468 op: BinOp::Eq,
1469 rhs: Box::new(SpannedExpr::new(Origin::new(19..23, "true"), true.into())),
1470 },
1471 ),
1472 ),
1473 (
1474 "!(!true || false)",
1475 SpannedExpr::new(
1476 Origin::new(0..17, "!(!true || false)"),
1477 Expr::UnOp {
1478 op: UnOp::Not,
1479 expr: Box::new(SpannedExpr::new(
1480 Origin::new(2..16, "!true || false"),
1481 Expr::BinOp {
1482 lhs: Box::new(SpannedExpr::new(
1483 Origin::new(2..7, "!true"),
1484 Expr::UnOp {
1485 op: UnOp::Not,
1486 expr: Box::new(SpannedExpr::new(
1487 Origin::new(3..7, "true"),
1488 true.into(),
1489 )),
1490 },
1491 )),
1492 op: BinOp::Or,
1493 rhs: Box::new(SpannedExpr::new(
1494 Origin::new(11..16, "false"),
1495 false.into(),
1496 )),
1497 },
1498 )),
1499 },
1500 ),
1501 ),
1502 (
1503 "foobar[format('{0}', 'event')]",
1504 SpannedExpr::new(
1505 Origin::new(0..30, "foobar[format('{0}', 'event')]"),
1506 Expr::context([
1507 SpannedExpr::new(Origin::new(0..6, "foobar"), Expr::ident("foobar")),
1508 SpannedExpr::new(
1509 Origin::new(6..30, "[format('{0}', 'event')]"),
1510 Expr::Index(Box::new(SpannedExpr::new(
1511 Origin::new(7..29, "format('{0}', 'event')"),
1512 Expr::Call(Call {
1513 func: Function::Format,
1514 args: vec![
1515 SpannedExpr::new(
1516 Origin::new(14..19, "'{0}'"),
1517 Expr::from("{0}"),
1518 ),
1519 SpannedExpr::new(
1520 Origin::new(21..28, "'event'"),
1521 Expr::from("event"),
1522 ),
1523 ],
1524 }),
1525 ))),
1526 ),
1527 ]),
1528 ),
1529 ),
1530 (
1531 "github.actor_id == '49699333'",
1532 SpannedExpr::new(
1533 Origin::new(0..29, "github.actor_id == '49699333'"),
1534 Expr::BinOp {
1535 lhs: SpannedExpr::new(
1536 Origin::new(0..15, "github.actor_id"),
1537 Expr::context(vec![
1538 SpannedExpr::new(
1539 Origin::new(0..6, "github"),
1540 Expr::ident("github"),
1541 ),
1542 SpannedExpr::new(
1543 Origin::new(7..15, "actor_id"),
1544 Expr::ident("actor_id"),
1545 ),
1546 ]),
1547 )
1548 .into(),
1549 op: BinOp::Eq,
1550 rhs: Box::new(SpannedExpr::new(
1551 Origin::new(19..29, "'49699333'"),
1552 Expr::from("49699333"),
1553 )),
1554 },
1555 ),
1556 ),
1557 (
1559 "(fromJSON('[]'))[1]",
1560 SpannedExpr::new(
1561 Origin::new(0..19, "(fromJSON('[]'))[1]"),
1562 Expr::context(vec![
1563 SpannedExpr::new(
1564 Origin::new(1..15, "fromJSON('[]')"),
1565 Expr::Call(Call {
1566 func: Function::FromJSON,
1567 args: vec![SpannedExpr::new(
1568 Origin::new(10..14, "'[]'"),
1569 Expr::from("[]"),
1570 )],
1571 }),
1572 ),
1573 SpannedExpr::new(
1574 Origin::new(16..19, "[1]"),
1575 Expr::Index(Box::new(SpannedExpr::new(
1576 Origin::new(17..18, "1"),
1577 1.0.into(),
1578 ))),
1579 ),
1580 ]),
1581 ),
1582 ),
1583 ];
1584
1585 for (case, expr) in cases {
1586 assert_eq!(*expr, Expr::parse(case).unwrap());
1587 }
1588 }
1589
1590 #[test]
1591 fn test_expr_constant_reducible() -> Result<(), Error> {
1592 for (expr, reducible) in &[
1593 ("'foo'", true),
1594 ("1", true),
1595 ("true", true),
1596 ("null", true),
1597 ("!true", true),
1600 ("!null", true),
1601 ("true && false", true),
1602 ("true || false", true),
1603 ("null && !null && true", true),
1604 ("format('{0} {1}', 'foo', 'bar')", true),
1607 ("format('{0} {1}', 1, 2)", true),
1608 ("format('{0} {1}', 1, '2')", true),
1609 ("contains('foo', 'bar')", true),
1610 ("startsWith('foo', 'bar')", true),
1611 ("endsWith('foo', 'bar')", true),
1612 ("startsWith(some.context, 'bar')", false),
1613 ("endsWith(some.context, 'bar')", false),
1614 ("format('{0} {1}', '1', format('{0}', null))", true),
1616 ("format('{0} {1}', '1', startsWith('foo', 'foo'))", true),
1617 ("format('{0} {1}', '1', startsWith(foo.bar, 'foo'))", false),
1618 ("foo", false),
1619 ("foo.bar", false),
1620 ("foo.bar[1]", false),
1621 ("foo.bar == 'bar'", false),
1622 ("foo.bar || bar || baz", false),
1623 ("foo.bar && bar && baz", false),
1624 ] {
1625 let expr = Expr::parse(expr)?;
1626 assert_eq!(expr.constant_reducible(), *reducible);
1627 }
1628
1629 Ok(())
1630 }
1631
1632 #[test]
1633 fn test_evaluate_constant_complex_expressions() -> Result<(), Error> {
1634 use crate::Evaluation;
1635
1636 let test_cases = &[
1637 ("!false", Evaluation::Boolean(true)),
1639 ("!true", Evaluation::Boolean(false)),
1640 ("!(true && false)", Evaluation::Boolean(true)),
1641 ("true && (false || true)", Evaluation::Boolean(true)),
1643 ("false || (true && false)", Evaluation::Boolean(false)),
1644 (
1646 "contains(format('{0} {1}', 'hello', 'world'), 'world')",
1647 Evaluation::Boolean(true),
1648 ),
1649 (
1650 "startsWith(format('prefix_{0}', 'test'), 'prefix')",
1651 Evaluation::Boolean(true),
1652 ),
1653 ];
1654
1655 for (expr_str, expected) in test_cases {
1656 let expr = Expr::parse(expr_str)?;
1657 let result = expr.consteval().unwrap();
1658 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1659 }
1660
1661 Ok(())
1662 }
1663
1664 #[test]
1665 fn test_case_insensitive_string_comparison() -> Result<(), Error> {
1666 use crate::Evaluation;
1667
1668 let test_cases = &[
1669 ("'hello' == 'hello'", Evaluation::Boolean(true)),
1671 ("'hello' == 'HELLO'", Evaluation::Boolean(true)),
1672 ("'Hello' == 'hELLO'", Evaluation::Boolean(true)),
1673 ("'abc' == 'def'", Evaluation::Boolean(false)),
1674 ("'hello' != 'HELLO'", Evaluation::Boolean(false)),
1676 ("'abc' != 'def'", Evaluation::Boolean(true)),
1677 ("'abc' < 'DEF'", Evaluation::Boolean(true)),
1679 ("'ABC' < 'def'", Evaluation::Boolean(true)),
1680 ("'abc' >= 'ABC'", Evaluation::Boolean(true)),
1681 ("'ABC' <= 'abc'", Evaluation::Boolean(true)),
1682 ("'\u{03C3}' == '\u{03C2}'", Evaluation::Boolean(true)), ("'\u{03A3}' == '\u{03C3}'", Evaluation::Boolean(true)), ("'\u{03A3}' == '\u{03C2}'", Evaluation::Boolean(true)), (
1689 "contains(fromJSON('[\"Hello\", \"World\"]'), 'hello')",
1690 Evaluation::Boolean(true),
1691 ),
1692 (
1693 "contains(fromJSON('[\"hello\", \"world\"]'), 'WORLD')",
1694 Evaluation::Boolean(true),
1695 ),
1696 (
1697 "contains(fromJSON('[\"ABC\"]'), 'abc')",
1698 Evaluation::Boolean(true),
1699 ),
1700 (
1701 "contains(fromJSON('[\"abc\"]'), 'def')",
1702 Evaluation::Boolean(false),
1703 ),
1704 ];
1705
1706 for (expr_str, expected) in test_cases {
1707 let expr = Expr::parse(expr_str)?;
1708 let result = expr.consteval().unwrap();
1709 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1710 }
1711
1712 Ok(())
1713 }
1714
1715 #[test]
1716 fn test_evaluation_sema_display() {
1717 use crate::Evaluation;
1718
1719 let test_cases = &[
1720 (Evaluation::String("hello".to_string()), "hello"),
1721 (Evaluation::Number(42.0), "42"),
1722 (Evaluation::Number(3.14), "3.14"),
1723 (Evaluation::Boolean(true), "true"),
1724 (Evaluation::Boolean(false), "false"),
1725 (Evaluation::Null, ""),
1726 ];
1727
1728 for (result, expected) in test_cases {
1729 assert_eq!(result.sema().to_string(), *expected);
1730 }
1731 }
1732
1733 #[test]
1734 fn test_evaluation_result_to_boolean() {
1735 use crate::Evaluation;
1736
1737 let test_cases = &[
1738 (Evaluation::Boolean(true), true),
1739 (Evaluation::Boolean(false), false),
1740 (Evaluation::Null, false),
1741 (Evaluation::Number(0.0), false),
1742 (Evaluation::Number(1.0), true),
1743 (Evaluation::Number(-1.0), true),
1744 (Evaluation::Number(f64::NAN), false), (Evaluation::String("".to_string()), false),
1746 (Evaluation::String("hello".to_string()), true),
1747 (Evaluation::Array(vec![]), true), (Evaluation::Object(std::collections::HashMap::new()), true), ];
1750
1751 for (result, expected) in test_cases {
1752 assert_eq!(result.as_boolean(), *expected);
1753 }
1754 }
1755
1756 #[test]
1757 fn test_evaluation_result_to_number() {
1758 use crate::Evaluation;
1759
1760 let test_cases = &[
1762 (Evaluation::Number(42.0), 42.0),
1763 (Evaluation::Number(0.0), 0.0),
1764 (Evaluation::Boolean(true), 1.0),
1765 (Evaluation::Boolean(false), 0.0),
1766 (Evaluation::Null, 0.0),
1767 ];
1768
1769 for (eval, expected) in test_cases {
1770 assert_eq!(eval.as_number(), *expected, "as_number() for {:?}", eval);
1771 }
1772
1773 let string_cases: &[(&str, f64)] = &[
1774 ("", 0.0),
1776 (" ", 0.0),
1777 ("\t", 0.0),
1778 (" 123 ", 123.0),
1780 (" 42 ", 42.0),
1781 (" 1 ", 1.0),
1782 ("\t5\n", 5.0),
1783 (" \t123\t ", 123.0),
1784 ("42", 42.0),
1786 ("3.14", 3.14),
1787 ("0xff", 255.0),
1789 ("0xfF", 255.0),
1790 ("0xFF", 255.0),
1791 (" 0xff ", 255.0),
1792 ("0x0", 0.0),
1793 ("0x11", 17.0),
1794 ("0x7FFFFFFF", 2147483647.0),
1796 ("0x80000000", -2147483648.0),
1797 ("0xFFFFFFFF", -1.0),
1798 ("0o10", 8.0),
1800 (" 0o10 ", 8.0),
1801 ("0o0", 0.0),
1802 ("0o11", 9.0),
1803 ("0o17777777777", 2147483647.0),
1805 ("0o20000000000", -2147483648.0),
1806 ("1.2e2", 120.0),
1808 ("1.2E2", 120.0),
1809 ("1.2e-2", 0.012),
1810 (" 1.2e2 ", 120.0),
1811 ("1.2e+2", 120.0),
1812 ("5e0", 5.0),
1813 ("1e3", 1000.0),
1814 ("123e-1", 12.3),
1815 (" +1.2e2 ", 120.0),
1816 (" -1.2E+2 ", -120.0),
1817 ("+42", 42.0),
1819 (" -42 ", -42.0),
1820 (" 3.14 ", 3.14),
1821 ("+0", 0.0),
1822 ("-0", 0.0),
1823 (" +123456.789 ", 123456.789),
1824 (" -123456.789 ", -123456.789),
1825 ("0123", 123.0),
1827 ("00", 0.0),
1828 ("007", 7.0),
1829 ("010", 10.0),
1830 ("123.", 123.0),
1832 (".5", 0.5),
1833 ];
1834
1835 for (input, expected) in string_cases {
1836 let eval = Evaluation::String(input.to_string());
1837 assert_eq!(eval.as_number(), *expected, "as_number() for {:?}", input);
1838 }
1839
1840 let infinity_cases: &[(&str, f64)] = &[
1842 ("Infinity", f64::INFINITY),
1843 (" Infinity ", f64::INFINITY),
1844 ("+Infinity", f64::INFINITY),
1845 ("-Infinity", f64::NEG_INFINITY),
1846 (" -Infinity ", f64::NEG_INFINITY),
1847 ];
1848
1849 for (input, expected) in infinity_cases {
1850 let eval = Evaluation::String(input.to_string());
1851 assert_eq!(eval.as_number(), *expected, "as_number() for {:?}", input);
1852 }
1853
1854 let nan_cases: &[&str] = &[
1856 "hello",
1858 "abc",
1859 " abc ",
1860 " NaN ",
1861 "123abc",
1863 "abc123",
1864 "100a",
1865 "12.3.4",
1866 "1e2e3",
1867 "1 2",
1868 "1_000",
1869 "+",
1870 "-",
1871 ".",
1872 "0b1010",
1874 "0B1010",
1875 "0b0",
1876 "0b1",
1877 "0b11",
1878 " 0b11 ",
1879 "0XFF",
1881 "0O10",
1882 "-0xff",
1884 "+0xff",
1885 "-0o10",
1886 "+0o10",
1887 "-0b11",
1888 "0x",
1890 "0o",
1891 "0b",
1892 "0xZZ",
1894 "0o89",
1895 "0b23",
1896 "0x100000000",
1898 "0o40000000000",
1899 "inf",
1901 "Inf",
1902 "INF",
1903 "+inf",
1904 "-inf",
1905 " inf ",
1906 ];
1907
1908 for input in nan_cases {
1909 let eval = Evaluation::String(input.to_string());
1910 assert!(
1911 eval.as_number().is_nan(),
1912 "as_number() for {:?} should be NaN",
1913 input
1914 );
1915 }
1916 }
1917
1918 #[test]
1919 fn test_github_actions_logical_semantics() -> Result<(), Error> {
1920 use crate::Evaluation;
1921
1922 let test_cases = &[
1924 ("false && 'hello'", Evaluation::Boolean(false)),
1926 ("null && 'hello'", Evaluation::Null),
1927 ("'' && 'hello'", Evaluation::String("".to_string())),
1928 (
1929 "'hello' && 'world'",
1930 Evaluation::String("world".to_string()),
1931 ),
1932 ("true && 42", Evaluation::Number(42.0)),
1933 ("true || 'hello'", Evaluation::Boolean(true)),
1935 (
1936 "'hello' || 'world'",
1937 Evaluation::String("hello".to_string()),
1938 ),
1939 ("false || 'hello'", Evaluation::String("hello".to_string())),
1940 ("null || false", Evaluation::Boolean(false)),
1941 ("'' || null", Evaluation::Null),
1942 ("!NaN", Evaluation::Boolean(true)),
1943 ("!!NaN", Evaluation::Boolean(false)),
1944 ];
1945
1946 for (expr_str, expected) in test_cases {
1947 let expr = Expr::parse(expr_str)?;
1948 let result = expr.consteval().unwrap();
1949 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1950 }
1951
1952 Ok(())
1953 }
1954
1955 #[test]
1956 fn test_expr_has_constant_reducible_subexpr() -> Result<(), Error> {
1957 for (expr, reducible) in &[
1958 ("'foo'", false),
1960 ("1", false),
1961 ("true", false),
1962 ("null", false),
1963 (
1965 "format('{0}, {1}', github.event.number, format('{0}', 'abc'))",
1966 true,
1967 ),
1968 ("foobar[format('{0}', 'event')]", true),
1969 ] {
1970 let expr = Expr::parse(expr)?;
1971 assert_eq!(!expr.constant_reducible_subexprs().is_empty(), *reducible);
1972 }
1973 Ok(())
1974 }
1975
1976 #[test]
1977 fn test_expr_contexts() -> Result<(), Error> {
1978 let expr = Expr::parse("foo.bar.baz[1].qux")?;
1980 assert_eq!(
1981 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1982 ["foo.bar.baz[1].qux",]
1983 );
1984
1985 let expr = Expr::parse("foo.bar[1].baz || abc.def")?;
1987 assert_eq!(
1988 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1989 ["foo.bar[1].baz", "abc.def",]
1990 );
1991
1992 let expr = Expr::parse("foo.bar[abc.def]")?;
1994 assert_eq!(
1995 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1996 ["foo.bar[abc.def]", "abc.def",]
1997 );
1998
1999 Ok(())
2000 }
2001
2002 #[test]
2003 fn test_expr_dataflow_contexts() -> Result<(), Error> {
2004 let expr = Expr::parse("foo.bar")?;
2006 assert_eq!(
2007 expr.dataflow_contexts()
2008 .iter()
2009 .map(|t| t.1.raw)
2010 .collect::<Vec<_>>(),
2011 ["foo.bar"]
2012 );
2013
2014 let expr = Expr::parse("foo.bar[1]")?;
2015 assert_eq!(
2016 expr.dataflow_contexts()
2017 .iter()
2018 .map(|t| t.1.raw)
2019 .collect::<Vec<_>>(),
2020 ["foo.bar[1]"]
2021 );
2022
2023 let expr = Expr::parse("foo.bar == 'bar'")?;
2025 assert!(expr.dataflow_contexts().is_empty());
2026
2027 let expr = Expr::parse("foo.bar || abc || d.e.f")?;
2029 assert_eq!(
2030 expr.dataflow_contexts()
2031 .iter()
2032 .map(|t| t.1.raw)
2033 .collect::<Vec<_>>(),
2034 ["foo.bar", "abc", "d.e.f"]
2035 );
2036
2037 let expr = Expr::parse("foo.bar && abc && d.e.f")?;
2039 assert_eq!(
2040 expr.dataflow_contexts()
2041 .iter()
2042 .map(|t| t.1.raw)
2043 .collect::<Vec<_>>(),
2044 ["d.e.f"]
2045 );
2046
2047 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || 'false'")?;
2048 assert_eq!(
2049 expr.dataflow_contexts()
2050 .iter()
2051 .map(|t| t.1.raw)
2052 .collect::<Vec<_>>(),
2053 ["foo.bar"]
2054 );
2055
2056 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || foo.baz")?;
2057 assert_eq!(
2058 expr.dataflow_contexts()
2059 .iter()
2060 .map(|t| t.1.raw)
2061 .collect::<Vec<_>>(),
2062 ["foo.bar", "foo.baz"]
2063 );
2064
2065 let expr = Expr::parse("fromJson(steps.runs.outputs.data).workflow_runs[0].id")?;
2066 assert_eq!(
2067 expr.dataflow_contexts()
2068 .iter()
2069 .map(|t| t.1.raw)
2070 .collect::<Vec<_>>(),
2071 ["fromJson(steps.runs.outputs.data).workflow_runs[0].id"]
2072 );
2073
2074 let expr = Expr::parse("format('{0} {1} {2}', foo.bar, tojson(github), toJSON(github))")?;
2075 assert_eq!(
2076 expr.dataflow_contexts()
2077 .iter()
2078 .map(|t| t.1.raw)
2079 .collect::<Vec<_>>(),
2080 ["foo.bar", "github", "github"]
2081 );
2082
2083 Ok(())
2084 }
2085
2086 #[test]
2087 fn test_spannedexpr_computed_indices() -> Result<(), Error> {
2088 for (expr, computed_indices) in &[
2089 ("foo.bar", vec![]),
2090 ("foo.bar[1]", vec![]),
2091 ("foo.bar[*]", vec![]),
2092 ("foo.bar[abc]", vec!["[abc]"]),
2093 (
2094 "foo.bar[format('{0}', 'foo')]",
2095 vec!["[format('{0}', 'foo')]"],
2096 ),
2097 ("foo.bar[abc].def[efg]", vec!["[abc]", "[efg]"]),
2098 ] {
2099 let expr = Expr::parse(expr)?;
2100
2101 assert_eq!(
2102 expr.computed_indices()
2103 .iter()
2104 .map(|e| e.origin.raw)
2105 .collect::<Vec<_>>(),
2106 *computed_indices
2107 );
2108 }
2109
2110 Ok(())
2111 }
2112
2113 #[test]
2114 fn test_fragment_from_expr() {
2115 for (expr, expected) in &[
2116 ("foo==bar", "foo==bar"),
2117 ("foo == bar", r"foo\s+==\s+bar"),
2118 ("foo == bar", r"foo\s+==\s+bar"),
2119 ("fromJSON('{}')", "fromJSON('{}')"),
2120 ("fromJSON('{ }')", r"fromJSON\('\{\s+\}'\)"),
2121 ("fromJSON ('{ }')", r"fromJSON\s+\('\{\s+\}'\)"),
2122 ("a . b . c . d", r"a\s+\.\s+b\s+\.\s+c\s+\.\s+d"),
2123 ("true \n && \n false", r"true\s+\&\&\s+false"),
2124 ] {
2125 let expr = Expr::parse(expr).unwrap();
2126 match subfeature::Fragment::from(&expr) {
2127 subfeature::Fragment::Raw(actual) => assert_eq!(actual, *expected),
2128 subfeature::Fragment::Regex(actual) => assert_eq!(actual.as_str(), *expected),
2129 };
2130 }
2131 }
2132
2133 #[test]
2134 fn test_leaf_expressions() -> Result<(), Error> {
2135 let expr = Expr::parse("'hello'")?;
2137 let leaves = expr.leaf_expressions();
2138 assert_eq!(leaves.len(), 1);
2139 assert!(matches!(&leaves[0].inner, Expr::Literal(Literal::String(s)) if s == "hello"));
2140
2141 let expr = Expr::parse("foo.bar")?;
2143 let leaves = expr.leaf_expressions();
2144 assert_eq!(leaves.len(), 1);
2145 assert!(matches!(&leaves[0].inner, Expr::Context(_)));
2146
2147 let expr = Expr::parse("foo.abc || foo.def")?;
2149 let leaves = expr.leaf_expressions();
2150 assert_eq!(leaves.len(), 2);
2151 assert!(matches!(&leaves[0].inner, Expr::Context(_)));
2152 assert!(matches!(&leaves[1].inner, Expr::Context(_)));
2153
2154 let expr = Expr::parse("foo.bar && 'hello'")?;
2156 let leaves = expr.leaf_expressions();
2157 assert_eq!(leaves.len(), 1);
2158 assert!(matches!(&leaves[0].inner, Expr::Literal(Literal::String(s)) if s == "hello"));
2159
2160 let expr = Expr::parse("foo.bar == 'true' && 'redis:7' || ''")?;
2162 let leaves = expr.leaf_expressions();
2163 assert_eq!(leaves.len(), 2);
2164 assert!(matches!(&leaves[0].inner, Expr::Literal(Literal::String(s)) if s == "redis:7"));
2165 assert!(matches!(&leaves[1].inner, Expr::Literal(Literal::String(s)) if s == ""));
2166
2167 let expr = Expr::parse("foo.bar == 'abc'")?;
2169 let leaves = expr.leaf_expressions();
2170 assert_eq!(leaves.len(), 1);
2171 assert!(matches!(&leaves[0].inner, Expr::BinOp { .. }));
2172
2173 Ok(())
2174 }
2175
2176 #[test]
2177 fn test_upper_special() {
2178 use super::EvaluationSema;
2179
2180 let cases = &[
2181 ("", ""),
2182 ("abc", "ABC"),
2183 ("ıabc", "ıABC"),
2184 ("ııabc", "ııABC"),
2185 ("abcı", "ABCı"),
2186 ("abcıı", "ABCıı"),
2187 ("abcıdef", "ABCıDEF"),
2188 ("abcııdef", "ABCııDEF"),
2189 ("abcıdefıghi", "ABCıDEFıGHI"),
2190 ];
2191
2192 for (input, want) in cases {
2193 assert_eq!(
2194 EvaluationSema::upper_special(input),
2195 *want,
2196 "input: {input}"
2197 );
2198 }
2199 }
2200}