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