1#![forbid(unsafe_code)]
4#![deny(missing_docs)]
5
6use std::ops::Deref;
7
8use crate::{
9 call::{Call, Function},
10 context::Context,
11 identifier::Identifier,
12 literal::Literal,
13 op::{BinOp, UnOp},
14};
15
16use self::parser::{ExprParser, Rule};
17use anyhow::Result;
18use itertools::Itertools;
19use pest::{Parser, iterators::Pair};
20
21pub mod call;
22pub mod context;
23pub mod identifier;
24pub mod literal;
25pub mod op;
26
27mod parser {
31 use pest_derive::Parser;
32
33 #[derive(Parser)]
35 #[grammar = "expr.pest"]
36 pub struct ExprParser;
37}
38
39#[derive(Copy, Clone, Debug, PartialEq)]
42pub struct Origin<'src> {
43 pub span: subfeature::Span,
45 pub raw: &'src str,
52}
53
54impl<'a> Origin<'a> {
55 pub fn new(span: impl Into<subfeature::Span>, raw: &'a str) -> Self {
57 Self {
58 span: span.into(),
59 raw: raw.trim(),
60 }
61 }
62}
63
64#[derive(Debug, PartialEq)]
72pub struct SpannedExpr<'src> {
73 pub origin: Origin<'src>,
75 pub inner: Expr<'src>,
77}
78
79impl<'a> SpannedExpr<'a> {
80 pub(crate) fn new(origin: Origin<'a>, inner: Expr<'a>) -> Self {
82 Self { origin, inner }
83 }
84
85 pub fn contexts(&self) -> Vec<(&Context<'a>, &Origin<'a>)> {
94 let mut contexts = vec![];
95
96 match self.deref() {
97 Expr::Index(expr) => contexts.extend(expr.contexts()),
98 Expr::Call(Call { func: _, args }) => {
99 for arg in args {
100 contexts.extend(arg.contexts());
101 }
102 }
103 Expr::Context(ctx) => {
104 contexts.push((ctx, &self.origin));
106
107 ctx.parts
110 .iter()
111 .for_each(|part| contexts.extend(part.contexts()));
112 }
113 Expr::BinOp { lhs, op: _, rhs } => {
114 contexts.extend(lhs.contexts());
115 contexts.extend(rhs.contexts());
116 }
117 Expr::UnOp { op: _, expr } => contexts.extend(expr.contexts()),
118 _ => (),
119 }
120
121 contexts
122 }
123
124 pub fn dataflow_contexts(&self) -> Vec<(&Context<'a>, &Origin<'a>)> {
133 let mut contexts = vec![];
134
135 match self.deref() {
136 Expr::Call(Call { func, args }) => {
137 if func == "toJSON" || func == "format" || func == "join" {
141 for arg in args {
142 contexts.extend(arg.dataflow_contexts());
143 }
144 }
145 }
146 Expr::Context(ctx) => contexts.push((ctx, &self.origin)),
153 Expr::BinOp { lhs, op, rhs } => match op {
154 BinOp::And => {
157 contexts.extend(rhs.dataflow_contexts());
158 }
159 BinOp::Or => {
161 contexts.extend(lhs.dataflow_contexts());
162 contexts.extend(rhs.dataflow_contexts());
163 }
164 _ => (),
165 },
166 _ => (),
167 }
168
169 contexts
170 }
171
172 pub fn computed_indices(&self) -> Vec<&SpannedExpr<'a>> {
177 let mut index_exprs = vec![];
178
179 match self.deref() {
180 Expr::Call(Call { func: _, args }) => {
181 for arg in args {
182 index_exprs.extend(arg.computed_indices());
183 }
184 }
185 Expr::Index(spanned_expr) => {
186 if !spanned_expr.is_literal() && !matches!(spanned_expr.inner, Expr::Star) {
188 index_exprs.push(self);
189 }
190 }
191 Expr::Context(context) => {
192 for part in &context.parts {
193 index_exprs.extend(part.computed_indices());
194 }
195 }
196 Expr::BinOp { lhs, op: _, rhs } => {
197 index_exprs.extend(lhs.computed_indices());
198 index_exprs.extend(rhs.computed_indices());
199 }
200 Expr::UnOp { op: _, expr } => {
201 index_exprs.extend(expr.computed_indices());
202 }
203 _ => {}
204 }
205
206 index_exprs
207 }
208
209 pub fn constant_reducible_subexprs(&self) -> Vec<&SpannedExpr<'a>> {
217 if !self.is_literal() && self.constant_reducible() {
218 return vec![self];
219 }
220
221 let mut subexprs = vec![];
222
223 match self.deref() {
224 Expr::Call(Call { func: _, args }) => {
225 for arg in args {
226 subexprs.extend(arg.constant_reducible_subexprs());
227 }
228 }
229 Expr::Context(ctx) => {
230 for part in &ctx.parts {
233 subexprs.extend(part.constant_reducible_subexprs());
234 }
235 }
236 Expr::BinOp { lhs, op: _, rhs } => {
237 subexprs.extend(lhs.constant_reducible_subexprs());
238 subexprs.extend(rhs.constant_reducible_subexprs());
239 }
240 Expr::UnOp { op: _, expr } => subexprs.extend(expr.constant_reducible_subexprs()),
241
242 Expr::Index(expr) => subexprs.extend(expr.constant_reducible_subexprs()),
243 _ => {}
244 }
245
246 subexprs
247 }
248}
249
250impl<'a> Deref for SpannedExpr<'a> {
251 type Target = Expr<'a>;
252
253 fn deref(&self) -> &Self::Target {
254 &self.inner
255 }
256}
257
258impl<'doc> From<&SpannedExpr<'doc>> for subfeature::Fragment<'doc> {
259 fn from(expr: &SpannedExpr<'doc>) -> Self {
260 Self::new(expr.origin.raw)
261 }
262}
263
264#[derive(Debug, PartialEq)]
266pub enum Expr<'src> {
267 Literal(Literal<'src>),
269 Star,
271 Call(Call<'src>),
273 Identifier(Identifier<'src>),
275 Index(Box<SpannedExpr<'src>>),
277 Context(Context<'src>),
279 BinOp {
281 lhs: Box<SpannedExpr<'src>>,
283 op: BinOp,
285 rhs: Box<SpannedExpr<'src>>,
287 },
288 UnOp {
290 op: UnOp,
292 expr: Box<SpannedExpr<'src>>,
294 },
295}
296
297impl<'src> Expr<'src> {
298 fn ident(i: &'src str) -> Self {
300 Self::Identifier(Identifier(i))
301 }
302
303 fn context(components: impl Into<Vec<SpannedExpr<'src>>>) -> Self {
305 Self::Context(Context::new(components))
306 }
307
308 pub fn is_literal(&self) -> bool {
310 matches!(self, Expr::Literal(_))
311 }
312
313 pub fn constant_reducible(&self) -> bool {
332 match self {
333 Expr::Literal(_) => true,
335 Expr::BinOp { lhs, op: _, rhs } => lhs.constant_reducible() && rhs.constant_reducible(),
337 Expr::UnOp { op: _, expr } => expr.constant_reducible(),
339 Expr::Call(Call { func, args }) => {
340 if func == "format"
342 || func == "contains"
343 || func == "startsWith"
344 || func == "endsWith"
345 || func == "toJSON"
346 || func == "join"
351 {
352 args.iter().all(|e| e.constant_reducible())
353 } else {
354 false
355 }
356 }
357 _ => false,
359 }
360 }
361
362 #[allow(clippy::unwrap_used)]
364 pub fn parse(expr: &'src str) -> Result<SpannedExpr<'src>> {
365 let or_expr = ExprParser::parse(Rule::expression, expr)?
367 .next()
368 .unwrap()
369 .into_inner()
370 .next()
371 .unwrap();
372
373 fn parse_pair(pair: Pair<'_, Rule>) -> Result<Box<SpannedExpr<'_>>> {
374 match pair.as_rule() {
386 Rule::or_expr => {
387 let (span, raw) = (pair.as_span(), pair.as_str());
388 let mut pairs = pair.into_inner();
389 let lhs = parse_pair(pairs.next().unwrap())?;
390 pairs.try_fold(lhs, |expr, next| {
391 Ok(SpannedExpr::new(
392 Origin::new(span.start()..span.end(), raw),
393 Expr::BinOp {
394 lhs: expr,
395 op: BinOp::Or,
396 rhs: parse_pair(next)?,
397 },
398 )
399 .into())
400 })
401 }
402 Rule::and_expr => {
403 let (span, raw) = (pair.as_span(), pair.as_str());
404 let mut pairs = pair.into_inner();
405 let lhs = parse_pair(pairs.next().unwrap())?;
406 pairs.try_fold(lhs, |expr, next| {
407 Ok(SpannedExpr::new(
408 Origin::new(span.start()..span.end(), raw),
409 Expr::BinOp {
410 lhs: expr,
411 op: BinOp::And,
412 rhs: parse_pair(next)?,
413 },
414 )
415 .into())
416 })
417 }
418 Rule::eq_expr => {
419 let (span, raw) = (pair.as_span(), pair.as_str());
423 let mut pairs = pair.into_inner();
424 let lhs = parse_pair(pairs.next().unwrap())?;
425
426 let pair_chunks = pairs.chunks(2);
427 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
428 let eq_op = next.next().unwrap();
429 let comp_expr = next.next().unwrap();
430
431 let eq_op = match eq_op.as_str() {
432 "==" => BinOp::Eq,
433 "!=" => BinOp::Neq,
434 _ => unreachable!(),
435 };
436
437 Ok(SpannedExpr::new(
438 Origin::new(span.start()..span.end(), raw),
439 Expr::BinOp {
440 lhs: expr,
441 op: eq_op,
442 rhs: parse_pair(comp_expr)?,
443 },
444 )
445 .into())
446 })
447 }
448 Rule::comp_expr => {
449 let (span, raw) = (pair.as_span(), pair.as_str());
451 let mut pairs = pair.into_inner();
452 let lhs = parse_pair(pairs.next().unwrap())?;
453
454 let pair_chunks = pairs.chunks(2);
455 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
456 let comp_op = next.next().unwrap();
457 let unary_expr = next.next().unwrap();
458
459 let eq_op = match comp_op.as_str() {
460 ">" => BinOp::Gt,
461 ">=" => BinOp::Ge,
462 "<" => BinOp::Lt,
463 "<=" => BinOp::Le,
464 _ => unreachable!(),
465 };
466
467 Ok(SpannedExpr::new(
468 Origin::new(span.start()..span.end(), raw),
469 Expr::BinOp {
470 lhs: expr,
471 op: eq_op,
472 rhs: parse_pair(unary_expr)?,
473 },
474 )
475 .into())
476 })
477 }
478 Rule::unary_expr => {
479 let (span, raw) = (pair.as_span(), pair.as_str());
480 let mut pairs = pair.into_inner();
481 let inner_pair = pairs.next().unwrap();
482
483 match inner_pair.as_rule() {
484 Rule::unary_op => Ok(SpannedExpr::new(
485 Origin::new(span.start()..span.end(), raw),
486 Expr::UnOp {
487 op: UnOp::Not,
488 expr: parse_pair(pairs.next().unwrap())?,
489 },
490 )
491 .into()),
492 Rule::primary_expr => parse_pair(inner_pair),
493 _ => unreachable!(),
494 }
495 }
496 Rule::primary_expr => {
497 parse_pair(pair.into_inner().next().unwrap())
499 }
500 Rule::number => Ok(SpannedExpr::new(
501 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
502 parse_number(pair.as_str()).into(),
503 )
504 .into()),
505 Rule::string => {
506 let (span, raw) = (pair.as_span(), pair.as_str());
507 let string_inner = pair.into_inner().next().unwrap().as_str();
509
510 if !string_inner.contains('\'') {
513 Ok(SpannedExpr::new(
514 Origin::new(span.start()..span.end(), raw),
515 string_inner.into(),
516 )
517 .into())
518 } else {
519 Ok(SpannedExpr::new(
520 Origin::new(span.start()..span.end(), raw),
521 string_inner.replace("''", "'").into(),
522 )
523 .into())
524 }
525 }
526 Rule::boolean => Ok(SpannedExpr::new(
527 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
528 pair.as_str().parse::<bool>().unwrap().into(),
529 )
530 .into()),
531 Rule::null => Ok(SpannedExpr::new(
532 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
533 Expr::Literal(Literal::Null),
534 )
535 .into()),
536 Rule::star => Ok(SpannedExpr::new(
537 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
538 Expr::Star,
539 )
540 .into()),
541 Rule::function_call => {
542 let (span, raw) = (pair.as_span(), pair.as_str());
543 let mut pairs = pair.into_inner();
544
545 let identifier = pairs.next().unwrap();
546 let args = pairs
547 .map(|pair| parse_pair(pair).map(|e| *e))
548 .collect::<Result<_, _>>()?;
549
550 Ok(SpannedExpr::new(
551 Origin::new(span.start()..span.end(), raw),
552 Expr::Call(Call {
553 func: Function(identifier.as_str()),
554 args,
555 }),
556 )
557 .into())
558 }
559 Rule::identifier => Ok(SpannedExpr::new(
560 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
561 Expr::ident(pair.as_str()),
562 )
563 .into()),
564 Rule::index => Ok(SpannedExpr::new(
565 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
566 Expr::Index(parse_pair(pair.into_inner().next().unwrap())?),
567 )
568 .into()),
569 Rule::context => {
570 let (span, raw) = (pair.as_span(), pair.as_str());
571 let pairs = pair.into_inner();
572
573 let mut inner: Vec<SpannedExpr> = pairs
574 .map(|pair| parse_pair(pair).map(|e| *e))
575 .collect::<Result<_, _>>()?;
576
577 if inner.len() == 1 && matches!(inner[0].inner, Expr::Call { .. }) {
581 Ok(inner.remove(0).into())
582 } else {
583 Ok(SpannedExpr::new(
584 Origin::new(span.start()..span.end(), raw),
585 Expr::context(inner),
586 )
587 .into())
588 }
589 }
590 r => panic!("unrecognized rule: {r:?}"),
591 }
592 }
593
594 parse_pair(or_expr).map(|e| *e)
595 }
596}
597
598impl<'src> From<&'src str> for Expr<'src> {
599 fn from(s: &'src str) -> Self {
600 Expr::Literal(Literal::String(s.into()))
601 }
602}
603
604impl From<String> for Expr<'_> {
605 fn from(s: String) -> Self {
606 Expr::Literal(Literal::String(s.into()))
607 }
608}
609
610impl From<f64> for Expr<'_> {
611 fn from(n: f64) -> Self {
612 Expr::Literal(Literal::Number(n))
613 }
614}
615
616impl From<bool> for Expr<'_> {
617 fn from(b: bool) -> Self {
618 Expr::Literal(Literal::Boolean(b))
619 }
620}
621
622#[derive(Debug, Clone, PartialEq)]
627pub enum Evaluation {
628 String(String),
630 Number(f64),
632 Boolean(bool),
634 Null,
636 Array(Vec<Evaluation>),
638 Object(std::collections::HashMap<String, Evaluation>),
640}
641
642impl TryFrom<serde_json::Value> for Evaluation {
643 type Error = ();
644
645 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
646 match value {
647 serde_json::Value::Null => Ok(Evaluation::Null),
648 serde_json::Value::Bool(b) => Ok(Evaluation::Boolean(b)),
649 serde_json::Value::Number(n) => {
650 if let Some(f) = n.as_f64() {
651 Ok(Evaluation::Number(f))
652 } else {
653 Err(())
654 }
655 }
656 serde_json::Value::String(s) => Ok(Evaluation::String(s)),
657 serde_json::Value::Array(arr) => {
658 let elements = arr
659 .into_iter()
660 .map(|elem| elem.try_into())
661 .collect::<Result<_, _>>()?;
662 Ok(Evaluation::Array(elements))
663 }
664 serde_json::Value::Object(obj) => {
665 let mut map = std::collections::HashMap::new();
666 for (key, value) in obj {
667 map.insert(key, value.try_into()?);
668 }
669 Ok(Evaluation::Object(map))
670 }
671 }
672 }
673}
674
675impl TryInto<serde_json::Value> for Evaluation {
676 type Error = ();
677
678 fn try_into(self) -> Result<serde_json::Value, Self::Error> {
679 match self {
680 Evaluation::Null => Ok(serde_json::Value::Null),
681 Evaluation::Boolean(b) => Ok(serde_json::Value::Bool(b)),
682 Evaluation::Number(n) => {
683 if n.fract() == 0.0 {
687 Ok(serde_json::Value::Number(serde_json::Number::from(
688 n as i64,
689 )))
690 } else if let Some(num) = serde_json::Number::from_f64(n) {
691 Ok(serde_json::Value::Number(num))
692 } else {
693 Err(())
694 }
695 }
696 Evaluation::String(s) => Ok(serde_json::Value::String(s)),
697 Evaluation::Array(arr) => {
698 let elements = arr
699 .into_iter()
700 .map(|elem| elem.try_into())
701 .collect::<Result<_, _>>()?;
702 Ok(serde_json::Value::Array(elements))
703 }
704 Evaluation::Object(obj) => {
705 let mut map = serde_json::Map::new();
706 for (key, value) in obj {
707 map.insert(key, value.try_into()?);
708 }
709 Ok(serde_json::Value::Object(map))
710 }
711 }
712 }
713}
714
715impl Evaluation {
716 pub fn as_boolean(&self) -> bool {
724 match self {
725 Evaluation::Boolean(b) => *b,
726 Evaluation::Null => false,
727 Evaluation::Number(n) => *n != 0.0,
728 Evaluation::String(s) => !s.is_empty(),
729 Evaluation::Array(_) | Evaluation::Object(_) => true,
731 }
732 }
733
734 pub fn as_number(&self) -> f64 {
738 match self {
739 Evaluation::String(s) => parse_number(s),
740 Evaluation::Number(n) => *n,
741 Evaluation::Boolean(b) => {
742 if *b {
743 1.0
744 } else {
745 0.0
746 }
747 }
748 Evaluation::Null => 0.0,
749 Evaluation::Array(_) | Evaluation::Object(_) => f64::NAN,
750 }
751 }
752
753 pub fn sema(&self) -> EvaluationSema<'_> {
756 EvaluationSema(self)
757 }
758}
759
760fn parse_number(s: &str) -> f64 {
766 let trimmed = s.trim();
767 if trimmed.is_empty() {
768 return 0.0;
769 }
770
771 if let Ok(value) = trimmed.parse::<f64>()
774 && value.is_finite()
775 {
776 return value;
777 }
778
779 if let Some(hex_digits) = trimmed.strip_prefix("0x") {
782 return u32::from_str_radix(hex_digits, 16)
783 .map(|n| (n as i32) as f64)
784 .unwrap_or(f64::NAN);
785 }
786
787 if let Some(oct_digits) = trimmed.strip_prefix("0o") {
789 return u32::from_str_radix(oct_digits, 8)
790 .map(|n| (n as i32) as f64)
791 .unwrap_or(f64::NAN);
792 }
793
794 let after_sign = trimmed
797 .strip_prefix(['+', '-'].as_slice())
798 .unwrap_or(trimmed);
799 if after_sign.eq_ignore_ascii_case("infinity") {
800 return if trimmed.starts_with('-') {
801 f64::NEG_INFINITY
802 } else {
803 f64::INFINITY
804 };
805 }
806
807 f64::NAN
808}
809
810pub struct EvaluationSema<'a>(&'a Evaluation);
813
814impl PartialEq for EvaluationSema<'_> {
815 fn eq(&self, other: &Self) -> bool {
816 match (self.0, other.0) {
817 (Evaluation::Null, Evaluation::Null) => true,
818 (Evaluation::Boolean(a), Evaluation::Boolean(b)) => a == b,
819 (Evaluation::Number(a), Evaluation::Number(b)) => a == b,
820 (Evaluation::String(a), Evaluation::String(b)) => a.to_uppercase() == b.to_uppercase(),
822
823 (a, b) => a.as_number() == b.as_number(),
825 }
826 }
827}
828
829impl PartialOrd for EvaluationSema<'_> {
830 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
831 match (self.0, other.0) {
832 (Evaluation::Null, Evaluation::Null) => Some(std::cmp::Ordering::Equal),
833 (Evaluation::Boolean(a), Evaluation::Boolean(b)) => a.partial_cmp(b),
834 (Evaluation::Number(a), Evaluation::Number(b)) => a.partial_cmp(b),
835 (Evaluation::String(a), Evaluation::String(b)) => {
836 a.to_uppercase().partial_cmp(&b.to_uppercase())
837 }
838 (a, b) => a.as_number().partial_cmp(&b.as_number()),
840 }
841 }
842}
843
844impl std::fmt::Display for EvaluationSema<'_> {
845 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846 match self.0 {
847 Evaluation::String(s) => write!(f, "{}", s),
848 Evaluation::Number(n) => {
849 if n.fract() == 0.0 {
851 write!(f, "{}", *n as i64)
852 } else {
853 write!(f, "{}", n)
854 }
855 }
856 Evaluation::Boolean(b) => write!(f, "{}", b),
857 Evaluation::Null => write!(f, ""),
858 Evaluation::Array(_) => write!(f, "Array"),
859 Evaluation::Object(_) => write!(f, "Object"),
860 }
861 }
862}
863
864impl<'src> Expr<'src> {
865 pub fn consteval(&self) -> Option<Evaluation> {
888 match self {
889 Expr::Literal(literal) => Some(literal.consteval()),
890
891 Expr::BinOp { lhs, op, rhs } => {
892 let lhs_val = lhs.consteval()?;
893 let rhs_val = rhs.consteval()?;
894
895 match op {
896 BinOp::And => {
897 if lhs_val.as_boolean() {
899 Some(rhs_val)
900 } else {
901 Some(lhs_val)
902 }
903 }
904 BinOp::Or => {
905 if lhs_val.as_boolean() {
907 Some(lhs_val)
908 } else {
909 Some(rhs_val)
910 }
911 }
912 BinOp::Eq => Some(Evaluation::Boolean(lhs_val.sema() == rhs_val.sema())),
913 BinOp::Neq => Some(Evaluation::Boolean(lhs_val.sema() != rhs_val.sema())),
914 BinOp::Lt => Some(Evaluation::Boolean(lhs_val.sema() < rhs_val.sema())),
915 BinOp::Le => Some(Evaluation::Boolean(lhs_val.sema() <= rhs_val.sema())),
916 BinOp::Gt => Some(Evaluation::Boolean(lhs_val.sema() > rhs_val.sema())),
917 BinOp::Ge => Some(Evaluation::Boolean(lhs_val.sema() >= rhs_val.sema())),
918 }
919 }
920
921 Expr::UnOp { op, expr } => {
922 let val = expr.consteval()?;
923 match op {
924 UnOp::Not => Some(Evaluation::Boolean(!val.as_boolean())),
925 }
926 }
927
928 Expr::Call(call) => call.consteval(),
929
930 _ => None,
932 }
933 }
934}
935
936#[cfg(test)]
937mod tests {
938 use std::borrow::Cow;
939
940 use anyhow::Result;
941 use pest::Parser as _;
942 use pretty_assertions::assert_eq;
943
944 use crate::{Call, Literal, Origin, SpannedExpr};
945
946 use super::{BinOp, Expr, ExprParser, Function, Rule, UnOp};
947
948 #[test]
949 fn test_literal_string_borrows() {
950 let cases = &[
951 ("'foo'", true),
952 ("'foo bar'", true),
953 ("'foo '' bar'", false),
954 ("'foo''bar'", false),
955 ("'foo''''bar'", false),
956 ];
957
958 for (expr, borrows) in cases {
959 let Expr::Literal(Literal::String(s)) = &*Expr::parse(expr).unwrap() else {
960 panic!("expected a literal string expression for {expr}");
961 };
962
963 assert!(matches!(
964 (s, borrows),
965 (Cow::Borrowed(_), true) | (Cow::Owned(_), false)
966 ));
967 }
968 }
969
970 #[test]
971 fn test_literal_as_str() {
972 let cases = &[
973 ("'foo'", "foo"),
974 ("'foo '' bar'", "foo ' bar"),
975 ("123", "123"),
976 ("123.000", "123"),
977 ("0.0", "0"),
978 ("0.1", "0.1"),
979 ("0.12345", "0.12345"),
980 ("true", "true"),
981 ("false", "false"),
982 ("null", "null"),
983 ];
984
985 for (expr, expected) in cases {
986 let Expr::Literal(expr) = &*Expr::parse(expr).unwrap() else {
987 panic!("expected a literal expression for {expr}");
988 };
989
990 assert_eq!(expr.as_str(), *expected);
991 }
992 }
993
994 #[test]
995 fn test_function_eq() {
996 let func = Function("foo");
997 assert_eq!(&func, "foo");
998 assert_eq!(&func, "FOO");
999 assert_eq!(&func, "Foo");
1000
1001 assert_eq!(func, Function("FOO"));
1002 }
1003
1004 #[test]
1005 fn test_parse_string_rule() {
1006 let cases = &[
1007 ("''", ""),
1008 ("' '", " "),
1009 ("''''", "''"),
1010 ("'test'", "test"),
1011 ("'spaces are ok'", "spaces are ok"),
1012 ("'escaping '' works'", "escaping '' works"),
1013 ];
1014
1015 for (case, expected) in cases {
1016 let s = ExprParser::parse(Rule::string, case)
1017 .unwrap()
1018 .next()
1019 .unwrap();
1020
1021 assert_eq!(s.into_inner().next().unwrap().as_str(), *expected);
1022 }
1023 }
1024
1025 #[test]
1026 fn test_parse_context_rule() {
1027 let cases = &[
1028 "foo.bar",
1029 "github.action_path",
1030 "inputs.foo-bar",
1031 "inputs.also--valid",
1032 "inputs.this__too",
1033 "inputs.this__too",
1034 "secrets.GH_TOKEN",
1035 "foo.*.bar",
1036 "github.event.issue.labels.*.name",
1037 ];
1038
1039 for case in cases {
1040 assert_eq!(
1041 ExprParser::parse(Rule::context, case)
1042 .unwrap()
1043 .next()
1044 .unwrap()
1045 .as_str(),
1046 *case
1047 );
1048 }
1049 }
1050
1051 #[test]
1052 fn test_parse_call_rule() {
1053 let cases = &[
1054 "foo()",
1055 "foo(bar)",
1056 "foo(bar())",
1057 "foo(1.23)",
1058 "foo(1,2)",
1059 "foo(1, 2)",
1060 "foo(1, 2, secret.GH_TOKEN)",
1061 "foo( )",
1062 "fromJSON(inputs.free-threading)",
1063 ];
1064
1065 for case in cases {
1066 assert_eq!(
1067 ExprParser::parse(Rule::function_call, case)
1068 .unwrap()
1069 .next()
1070 .unwrap()
1071 .as_str(),
1072 *case
1073 );
1074 }
1075 }
1076
1077 #[test]
1078 fn test_parse_expr_rule() -> Result<()> {
1079 let multiline = "github.repository_owner == 'Homebrew' &&
1081 ((github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
1082 (github.event_name == 'pull_request_target' &&
1083 (github.event.action == 'ready_for_review' || github.event.label.name == 'automerge-skip')))";
1084
1085 let multiline2 = "foo.bar.baz[
1086 0
1087 ]";
1088
1089 let cases = &[
1090 "true",
1091 "fromJSON(inputs.free-threading) && '--disable-gil' || ''",
1092 "foo || bar || baz",
1093 "foo || bar && baz || foo && 1 && 2 && 3 || 4",
1094 "(github.actor != 'github-actions[bot]' && github.actor) || 'BrewTestBot'",
1095 "(true || false) == true",
1096 "!(!true || false)",
1097 "!(!true || false) == true",
1098 "(true == false) == true",
1099 "(true == (false || true && (true || false))) == true",
1100 "(github.actor != 'github-actions[bot]' && github.actor) == 'BrewTestBot'",
1101 "foo()[0]",
1102 "fromJson(steps.runs.outputs.data).workflow_runs[0].id",
1103 multiline,
1104 "'a' == 'b' && 'c' || 'd'",
1105 "github.event['a']",
1106 "github.event['a' == 'b']",
1107 "github.event['a' == 'b' && 'c' || 'd']",
1108 "github['event']['inputs']['dry-run']",
1109 "github[format('{0}', 'event')]",
1110 "github['event']['inputs'][github.event.inputs.magic]",
1111 "github['event']['inputs'].*",
1112 "1 == 1",
1113 "1 > 1",
1114 "1 >= 1",
1115 "matrix.node_version >= 20",
1116 "true||false",
1117 "0xFF",
1119 "0xff",
1120 "0x0",
1121 "0xFF == 255",
1122 "0o10",
1124 "0o77",
1125 "0o0",
1126 "1e2",
1128 "1.5E-3",
1129 "1.2e+2",
1130 "5e0",
1131 "+42",
1133 "-42",
1134 ".5",
1136 "123.",
1137 multiline2,
1138 "fromJSON( github.event.inputs.hmm ) [ 0 ]",
1139 ];
1140
1141 for case in cases {
1142 assert_eq!(
1143 ExprParser::parse(Rule::expression, case)?
1144 .next()
1145 .unwrap()
1146 .as_str(),
1147 *case
1148 );
1149 }
1150
1151 Ok(())
1152 }
1153
1154 #[test]
1155 fn test_parse() {
1156 let cases = &[
1157 (
1158 "!true || false || true",
1159 SpannedExpr::new(
1160 Origin::new(0..22, "!true || false || true"),
1161 Expr::BinOp {
1162 lhs: SpannedExpr::new(
1163 Origin::new(0..22, "!true || false || true"),
1164 Expr::BinOp {
1165 lhs: SpannedExpr::new(
1166 Origin::new(0..5, "!true"),
1167 Expr::UnOp {
1168 op: UnOp::Not,
1169 expr: SpannedExpr::new(
1170 Origin::new(1..5, "true"),
1171 true.into(),
1172 )
1173 .into(),
1174 },
1175 )
1176 .into(),
1177 op: BinOp::Or,
1178 rhs: SpannedExpr::new(Origin::new(9..14, "false"), false.into())
1179 .into(),
1180 },
1181 )
1182 .into(),
1183 op: BinOp::Or,
1184 rhs: SpannedExpr::new(Origin::new(18..22, "true"), true.into()).into(),
1185 },
1186 ),
1187 ),
1188 (
1189 "'foo '' bar'",
1190 SpannedExpr::new(
1191 Origin::new(0..12, "'foo '' bar'"),
1192 Expr::Literal(Literal::String("foo ' bar".into())),
1193 ),
1194 ),
1195 (
1196 "('foo '' bar')",
1197 SpannedExpr::new(
1198 Origin::new(1..13, "'foo '' bar'"),
1199 Expr::Literal(Literal::String("foo ' bar".into())),
1200 ),
1201 ),
1202 (
1203 "((('foo '' bar')))",
1204 SpannedExpr::new(
1205 Origin::new(3..15, "'foo '' bar'"),
1206 Expr::Literal(Literal::String("foo ' bar".into())),
1207 ),
1208 ),
1209 (
1210 "foo(1, 2, 3)",
1211 SpannedExpr::new(
1212 Origin::new(0..12, "foo(1, 2, 3)"),
1213 Expr::Call(Call {
1214 func: Function("foo"),
1215 args: vec![
1216 SpannedExpr::new(Origin::new(4..5, "1"), 1.0.into()),
1217 SpannedExpr::new(Origin::new(7..8, "2"), 2.0.into()),
1218 SpannedExpr::new(Origin::new(10..11, "3"), 3.0.into()),
1219 ],
1220 }),
1221 ),
1222 ),
1223 (
1224 "foo.bar.baz",
1225 SpannedExpr::new(
1226 Origin::new(0..11, "foo.bar.baz"),
1227 Expr::context(vec![
1228 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1229 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1230 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1231 ]),
1232 ),
1233 ),
1234 (
1235 "foo.bar.baz[1][2]",
1236 SpannedExpr::new(
1237 Origin::new(0..17, "foo.bar.baz[1][2]"),
1238 Expr::context(vec![
1239 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1240 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1241 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1242 SpannedExpr::new(
1243 Origin::new(11..14, "[1]"),
1244 Expr::Index(Box::new(SpannedExpr::new(
1245 Origin::new(12..13, "1"),
1246 1.0.into(),
1247 ))),
1248 ),
1249 SpannedExpr::new(
1250 Origin::new(14..17, "[2]"),
1251 Expr::Index(Box::new(SpannedExpr::new(
1252 Origin::new(15..16, "2"),
1253 2.0.into(),
1254 ))),
1255 ),
1256 ]),
1257 ),
1258 ),
1259 (
1260 "foo.bar.baz[*]",
1261 SpannedExpr::new(
1262 Origin::new(0..14, "foo.bar.baz[*]"),
1263 Expr::context([
1264 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1265 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1266 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1267 SpannedExpr::new(
1268 Origin::new(11..14, "[*]"),
1269 Expr::Index(Box::new(SpannedExpr::new(
1270 Origin::new(12..13, "*"),
1271 Expr::Star,
1272 ))),
1273 ),
1274 ]),
1275 ),
1276 ),
1277 (
1278 "vegetables.*.ediblePortions",
1279 SpannedExpr::new(
1280 Origin::new(0..27, "vegetables.*.ediblePortions"),
1281 Expr::context(vec![
1282 SpannedExpr::new(
1283 Origin::new(0..10, "vegetables"),
1284 Expr::ident("vegetables"),
1285 ),
1286 SpannedExpr::new(Origin::new(11..12, "*"), Expr::Star),
1287 SpannedExpr::new(
1288 Origin::new(13..27, "ediblePortions"),
1289 Expr::ident("ediblePortions"),
1290 ),
1291 ]),
1292 ),
1293 ),
1294 (
1295 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1298 SpannedExpr::new(
1299 Origin::new(
1300 0..88,
1301 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1302 ),
1303 Expr::BinOp {
1304 lhs: Box::new(SpannedExpr::new(
1305 Origin::new(
1306 0..59,
1307 "github.ref == 'refs/heads/main' && 'value_for_main_branch'",
1308 ),
1309 Expr::BinOp {
1310 lhs: Box::new(SpannedExpr::new(
1311 Origin::new(0..32, "github.ref == 'refs/heads/main'"),
1312 Expr::BinOp {
1313 lhs: Box::new(SpannedExpr::new(
1314 Origin::new(0..10, "github.ref"),
1315 Expr::context(vec![
1316 SpannedExpr::new(
1317 Origin::new(0..6, "github"),
1318 Expr::ident("github"),
1319 ),
1320 SpannedExpr::new(
1321 Origin::new(7..10, "ref"),
1322 Expr::ident("ref"),
1323 ),
1324 ]),
1325 )),
1326 op: BinOp::Eq,
1327 rhs: Box::new(SpannedExpr::new(
1328 Origin::new(14..31, "'refs/heads/main'"),
1329 Expr::Literal(Literal::String(
1330 "refs/heads/main".into(),
1331 )),
1332 )),
1333 },
1334 )),
1335 op: BinOp::And,
1336 rhs: Box::new(SpannedExpr::new(
1337 Origin::new(35..58, "'value_for_main_branch'"),
1338 Expr::Literal(Literal::String("value_for_main_branch".into())),
1339 )),
1340 },
1341 )),
1342 op: BinOp::Or,
1343 rhs: Box::new(SpannedExpr::new(
1344 Origin::new(62..88, "'value_for_other_branches'"),
1345 Expr::Literal(Literal::String("value_for_other_branches".into())),
1346 )),
1347 },
1348 ),
1349 ),
1350 (
1351 "(true || false) == true",
1352 SpannedExpr::new(
1353 Origin::new(0..23, "(true || false) == true"),
1354 Expr::BinOp {
1355 lhs: Box::new(SpannedExpr::new(
1356 Origin::new(1..14, "true || false"),
1357 Expr::BinOp {
1358 lhs: Box::new(SpannedExpr::new(
1359 Origin::new(1..5, "true"),
1360 true.into(),
1361 )),
1362 op: BinOp::Or,
1363 rhs: Box::new(SpannedExpr::new(
1364 Origin::new(9..14, "false"),
1365 false.into(),
1366 )),
1367 },
1368 )),
1369 op: BinOp::Eq,
1370 rhs: Box::new(SpannedExpr::new(Origin::new(19..23, "true"), true.into())),
1371 },
1372 ),
1373 ),
1374 (
1375 "!(!true || false)",
1376 SpannedExpr::new(
1377 Origin::new(0..17, "!(!true || false)"),
1378 Expr::UnOp {
1379 op: UnOp::Not,
1380 expr: Box::new(SpannedExpr::new(
1381 Origin::new(2..16, "!true || false"),
1382 Expr::BinOp {
1383 lhs: Box::new(SpannedExpr::new(
1384 Origin::new(2..7, "!true"),
1385 Expr::UnOp {
1386 op: UnOp::Not,
1387 expr: Box::new(SpannedExpr::new(
1388 Origin::new(3..7, "true"),
1389 true.into(),
1390 )),
1391 },
1392 )),
1393 op: BinOp::Or,
1394 rhs: Box::new(SpannedExpr::new(
1395 Origin::new(11..16, "false"),
1396 false.into(),
1397 )),
1398 },
1399 )),
1400 },
1401 ),
1402 ),
1403 (
1404 "foobar[format('{0}', 'event')]",
1405 SpannedExpr::new(
1406 Origin::new(0..30, "foobar[format('{0}', 'event')]"),
1407 Expr::context([
1408 SpannedExpr::new(Origin::new(0..6, "foobar"), Expr::ident("foobar")),
1409 SpannedExpr::new(
1410 Origin::new(6..30, "[format('{0}', 'event')]"),
1411 Expr::Index(Box::new(SpannedExpr::new(
1412 Origin::new(7..29, "format('{0}', 'event')"),
1413 Expr::Call(Call {
1414 func: Function("format"),
1415 args: vec![
1416 SpannedExpr::new(
1417 Origin::new(14..19, "'{0}'"),
1418 Expr::from("{0}"),
1419 ),
1420 SpannedExpr::new(
1421 Origin::new(21..28, "'event'"),
1422 Expr::from("event"),
1423 ),
1424 ],
1425 }),
1426 ))),
1427 ),
1428 ]),
1429 ),
1430 ),
1431 (
1432 "github.actor_id == '49699333'",
1433 SpannedExpr::new(
1434 Origin::new(0..29, "github.actor_id == '49699333'"),
1435 Expr::BinOp {
1436 lhs: SpannedExpr::new(
1437 Origin::new(0..15, "github.actor_id"),
1438 Expr::context(vec![
1439 SpannedExpr::new(
1440 Origin::new(0..6, "github"),
1441 Expr::ident("github"),
1442 ),
1443 SpannedExpr::new(
1444 Origin::new(7..15, "actor_id"),
1445 Expr::ident("actor_id"),
1446 ),
1447 ]),
1448 )
1449 .into(),
1450 op: BinOp::Eq,
1451 rhs: Box::new(SpannedExpr::new(
1452 Origin::new(19..29, "'49699333'"),
1453 Expr::from("49699333"),
1454 )),
1455 },
1456 ),
1457 ),
1458 ];
1459
1460 for (case, expr) in cases {
1461 assert_eq!(*expr, Expr::parse(case).unwrap());
1462 }
1463 }
1464
1465 #[test]
1466 fn test_expr_constant_reducible() -> Result<()> {
1467 for (expr, reducible) in &[
1468 ("'foo'", true),
1469 ("1", true),
1470 ("true", true),
1471 ("null", true),
1472 ("!true", true),
1475 ("!null", true),
1476 ("true && false", true),
1477 ("true || false", true),
1478 ("null && !null && true", true),
1479 ("format('{0} {1}', 'foo', 'bar')", true),
1482 ("format('{0} {1}', 1, 2)", true),
1483 ("format('{0} {1}', 1, '2')", true),
1484 ("contains('foo', 'bar')", true),
1485 ("startsWith('foo', 'bar')", true),
1486 ("endsWith('foo', 'bar')", true),
1487 ("startsWith(some.context, 'bar')", false),
1488 ("endsWith(some.context, 'bar')", false),
1489 ("format('{0} {1}', '1', format('{0}', null))", true),
1491 ("format('{0} {1}', '1', startsWith('foo', 'foo'))", true),
1492 ("format('{0} {1}', '1', startsWith(foo.bar, 'foo'))", false),
1493 ("foo", false),
1494 ("foo.bar", false),
1495 ("foo.bar[1]", false),
1496 ("foo.bar == 'bar'", false),
1497 ("foo.bar || bar || baz", false),
1498 ("foo.bar && bar && baz", false),
1499 ] {
1500 let expr = Expr::parse(expr)?;
1501 assert_eq!(expr.constant_reducible(), *reducible);
1502 }
1503
1504 Ok(())
1505 }
1506
1507 #[test]
1508 fn test_evaluate_constant_complex_expressions() -> Result<()> {
1509 use crate::Evaluation;
1510
1511 let test_cases = &[
1512 ("!false", Evaluation::Boolean(true)),
1514 ("!true", Evaluation::Boolean(false)),
1515 ("!(true && false)", Evaluation::Boolean(true)),
1516 ("true && (false || true)", Evaluation::Boolean(true)),
1518 ("false || (true && false)", Evaluation::Boolean(false)),
1519 (
1521 "contains(format('{0} {1}', 'hello', 'world'), 'world')",
1522 Evaluation::Boolean(true),
1523 ),
1524 (
1525 "startsWith(format('prefix_{0}', 'test'), 'prefix')",
1526 Evaluation::Boolean(true),
1527 ),
1528 ];
1529
1530 for (expr_str, expected) in test_cases {
1531 let expr = Expr::parse(expr_str)?;
1532 let result = expr.consteval().unwrap();
1533 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1534 }
1535
1536 Ok(())
1537 }
1538
1539 #[test]
1540 fn test_case_insensitive_string_comparison() -> Result<()> {
1541 use crate::Evaluation;
1542
1543 let test_cases = &[
1544 ("'hello' == 'hello'", Evaluation::Boolean(true)),
1546 ("'hello' == 'HELLO'", Evaluation::Boolean(true)),
1547 ("'Hello' == 'hELLO'", Evaluation::Boolean(true)),
1548 ("'abc' == 'def'", Evaluation::Boolean(false)),
1549 ("'hello' != 'HELLO'", Evaluation::Boolean(false)),
1551 ("'abc' != 'def'", Evaluation::Boolean(true)),
1552 ("'abc' < 'DEF'", Evaluation::Boolean(true)),
1554 ("'ABC' < 'def'", Evaluation::Boolean(true)),
1555 ("'abc' >= 'ABC'", Evaluation::Boolean(true)),
1556 ("'ABC' <= 'abc'", Evaluation::Boolean(true)),
1557 ("'\u{03C3}' == '\u{03C2}'", Evaluation::Boolean(true)), ("'\u{03A3}' == '\u{03C3}'", Evaluation::Boolean(true)), ("'\u{03A3}' == '\u{03C2}'", Evaluation::Boolean(true)), (
1564 "contains(fromJSON('[\"Hello\", \"World\"]'), 'hello')",
1565 Evaluation::Boolean(true),
1566 ),
1567 (
1568 "contains(fromJSON('[\"hello\", \"world\"]'), 'WORLD')",
1569 Evaluation::Boolean(true),
1570 ),
1571 (
1572 "contains(fromJSON('[\"ABC\"]'), 'abc')",
1573 Evaluation::Boolean(true),
1574 ),
1575 (
1576 "contains(fromJSON('[\"abc\"]'), 'def')",
1577 Evaluation::Boolean(false),
1578 ),
1579 ];
1580
1581 for (expr_str, expected) in test_cases {
1582 let expr = Expr::parse(expr_str)?;
1583 let result = expr.consteval().unwrap();
1584 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1585 }
1586
1587 Ok(())
1588 }
1589
1590 #[test]
1591 fn test_evaluation_sema_display() {
1592 use crate::Evaluation;
1593
1594 let test_cases = &[
1595 (Evaluation::String("hello".to_string()), "hello"),
1596 (Evaluation::Number(42.0), "42"),
1597 (Evaluation::Number(3.14), "3.14"),
1598 (Evaluation::Boolean(true), "true"),
1599 (Evaluation::Boolean(false), "false"),
1600 (Evaluation::Null, ""),
1601 ];
1602
1603 for (result, expected) in test_cases {
1604 assert_eq!(result.sema().to_string(), *expected);
1605 }
1606 }
1607
1608 #[test]
1609 fn test_evaluation_result_to_boolean() {
1610 use crate::Evaluation;
1611
1612 let test_cases = &[
1613 (Evaluation::Boolean(true), true),
1614 (Evaluation::Boolean(false), false),
1615 (Evaluation::Null, false),
1616 (Evaluation::Number(0.0), false),
1617 (Evaluation::Number(1.0), true),
1618 (Evaluation::Number(-1.0), true),
1619 (Evaluation::String("".to_string()), false),
1620 (Evaluation::String("hello".to_string()), true),
1621 (Evaluation::Array(vec![]), true), (Evaluation::Object(std::collections::HashMap::new()), true), ];
1624
1625 for (result, expected) in test_cases {
1626 assert_eq!(result.as_boolean(), *expected);
1627 }
1628 }
1629
1630 #[test]
1631 fn test_evaluation_result_to_number() {
1632 use crate::Evaluation;
1633
1634 let test_cases = &[
1636 (Evaluation::Number(42.0), 42.0),
1637 (Evaluation::Number(0.0), 0.0),
1638 (Evaluation::Boolean(true), 1.0),
1639 (Evaluation::Boolean(false), 0.0),
1640 (Evaluation::Null, 0.0),
1641 ];
1642
1643 for (eval, expected) in test_cases {
1644 assert_eq!(eval.as_number(), *expected, "as_number() for {:?}", eval);
1645 }
1646
1647 let string_cases: &[(&str, f64)] = &[
1648 ("", 0.0),
1650 (" ", 0.0),
1651 ("\t", 0.0),
1652 (" 123 ", 123.0),
1654 (" 42 ", 42.0),
1655 (" 1 ", 1.0),
1656 ("\t5\n", 5.0),
1657 (" \t123\t ", 123.0),
1658 ("42", 42.0),
1660 ("3.14", 3.14),
1661 ("0xff", 255.0),
1663 ("0xfF", 255.0),
1664 ("0xFF", 255.0),
1665 (" 0xff ", 255.0),
1666 ("0x0", 0.0),
1667 ("0x11", 17.0),
1668 ("0x7FFFFFFF", 2147483647.0),
1670 ("0x80000000", -2147483648.0),
1671 ("0xFFFFFFFF", -1.0),
1672 ("0o10", 8.0),
1674 (" 0o10 ", 8.0),
1675 ("0o0", 0.0),
1676 ("0o11", 9.0),
1677 ("0o17777777777", 2147483647.0),
1679 ("0o20000000000", -2147483648.0),
1680 ("1.2e2", 120.0),
1682 ("1.2E2", 120.0),
1683 ("1.2e-2", 0.012),
1684 (" 1.2e2 ", 120.0),
1685 ("1.2e+2", 120.0),
1686 ("5e0", 5.0),
1687 ("1e3", 1000.0),
1688 ("123e-1", 12.3),
1689 (" +1.2e2 ", 120.0),
1690 (" -1.2E+2 ", -120.0),
1691 ("+42", 42.0),
1693 (" -42 ", -42.0),
1694 (" 3.14 ", 3.14),
1695 ("+0", 0.0),
1696 ("-0", 0.0),
1697 (" +123456.789 ", 123456.789),
1698 (" -123456.789 ", -123456.789),
1699 ("0123", 123.0),
1701 ("00", 0.0),
1702 ("007", 7.0),
1703 ("010", 10.0),
1704 ("123.", 123.0),
1706 (".5", 0.5),
1707 ];
1708
1709 for (input, expected) in string_cases {
1710 let eval = Evaluation::String(input.to_string());
1711 assert_eq!(eval.as_number(), *expected, "as_number() for {:?}", input);
1712 }
1713
1714 let infinity_cases: &[(&str, f64)] = &[
1716 ("Infinity", f64::INFINITY),
1717 (" Infinity ", f64::INFINITY),
1718 ("+Infinity", f64::INFINITY),
1719 ("-Infinity", f64::NEG_INFINITY),
1720 (" -Infinity ", f64::NEG_INFINITY),
1721 ];
1722
1723 for (input, expected) in infinity_cases {
1724 let eval = Evaluation::String(input.to_string());
1725 assert_eq!(eval.as_number(), *expected, "as_number() for {:?}", input);
1726 }
1727
1728 let nan_cases: &[&str] = &[
1730 "hello",
1732 "abc",
1733 " abc ",
1734 " NaN ",
1735 "123abc",
1737 "abc123",
1738 "100a",
1739 "12.3.4",
1740 "1e2e3",
1741 "1 2",
1742 "1_000",
1743 "+",
1744 "-",
1745 ".",
1746 "0b1010",
1748 "0B1010",
1749 "0b0",
1750 "0b1",
1751 "0b11",
1752 " 0b11 ",
1753 "0XFF",
1755 "0O10",
1756 "-0xff",
1758 "+0xff",
1759 "-0o10",
1760 "+0o10",
1761 "-0b11",
1762 "0x",
1764 "0o",
1765 "0b",
1766 "0xZZ",
1768 "0o89",
1769 "0b23",
1770 "0x100000000",
1772 "0o40000000000",
1773 "inf",
1775 "Inf",
1776 "INF",
1777 "+inf",
1778 "-inf",
1779 " inf ",
1780 ];
1781
1782 for input in nan_cases {
1783 let eval = Evaluation::String(input.to_string());
1784 assert!(
1785 eval.as_number().is_nan(),
1786 "as_number() for {:?} should be NaN",
1787 input
1788 );
1789 }
1790 }
1791
1792 #[test]
1793 fn test_github_actions_logical_semantics() -> Result<()> {
1794 use crate::Evaluation;
1795
1796 let test_cases = &[
1798 ("false && 'hello'", Evaluation::Boolean(false)),
1800 ("null && 'hello'", Evaluation::Null),
1801 ("'' && 'hello'", Evaluation::String("".to_string())),
1802 (
1803 "'hello' && 'world'",
1804 Evaluation::String("world".to_string()),
1805 ),
1806 ("true && 42", Evaluation::Number(42.0)),
1807 ("true || 'hello'", Evaluation::Boolean(true)),
1809 (
1810 "'hello' || 'world'",
1811 Evaluation::String("hello".to_string()),
1812 ),
1813 ("false || 'hello'", Evaluation::String("hello".to_string())),
1814 ("null || false", Evaluation::Boolean(false)),
1815 ("'' || null", Evaluation::Null),
1816 ];
1817
1818 for (expr_str, expected) in test_cases {
1819 let expr = Expr::parse(expr_str)?;
1820 let result = expr.consteval().unwrap();
1821 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1822 }
1823
1824 Ok(())
1825 }
1826
1827 #[test]
1828 fn test_expr_has_constant_reducible_subexpr() -> Result<()> {
1829 for (expr, reducible) in &[
1830 ("'foo'", false),
1832 ("1", false),
1833 ("true", false),
1834 ("null", false),
1835 (
1837 "format('{0}, {1}', github.event.number, format('{0}', 'abc'))",
1838 true,
1839 ),
1840 ("foobar[format('{0}', 'event')]", true),
1841 ] {
1842 let expr = Expr::parse(expr)?;
1843 assert_eq!(!expr.constant_reducible_subexprs().is_empty(), *reducible);
1844 }
1845 Ok(())
1846 }
1847
1848 #[test]
1849 fn test_expr_contexts() -> Result<()> {
1850 let expr = Expr::parse("foo.bar.baz[1].qux")?;
1852 assert_eq!(
1853 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1854 ["foo.bar.baz[1].qux",]
1855 );
1856
1857 let expr = Expr::parse("foo.bar[1].baz || abc.def")?;
1859 assert_eq!(
1860 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1861 ["foo.bar[1].baz", "abc.def",]
1862 );
1863
1864 let expr = Expr::parse("foo.bar[abc.def]")?;
1866 assert_eq!(
1867 expr.contexts().iter().map(|t| t.1.raw).collect::<Vec<_>>(),
1868 ["foo.bar[abc.def]", "abc.def",]
1869 );
1870
1871 Ok(())
1872 }
1873
1874 #[test]
1875 fn test_expr_dataflow_contexts() -> Result<()> {
1876 let expr = Expr::parse("foo.bar")?;
1878 assert_eq!(
1879 expr.dataflow_contexts()
1880 .iter()
1881 .map(|t| t.1.raw)
1882 .collect::<Vec<_>>(),
1883 ["foo.bar"]
1884 );
1885
1886 let expr = Expr::parse("foo.bar[1]")?;
1887 assert_eq!(
1888 expr.dataflow_contexts()
1889 .iter()
1890 .map(|t| t.1.raw)
1891 .collect::<Vec<_>>(),
1892 ["foo.bar[1]"]
1893 );
1894
1895 let expr = Expr::parse("foo.bar == 'bar'")?;
1897 assert!(expr.dataflow_contexts().is_empty());
1898
1899 let expr = Expr::parse("foo.bar || abc || d.e.f")?;
1901 assert_eq!(
1902 expr.dataflow_contexts()
1903 .iter()
1904 .map(|t| t.1.raw)
1905 .collect::<Vec<_>>(),
1906 ["foo.bar", "abc", "d.e.f"]
1907 );
1908
1909 let expr = Expr::parse("foo.bar && abc && d.e.f")?;
1911 assert_eq!(
1912 expr.dataflow_contexts()
1913 .iter()
1914 .map(|t| t.1.raw)
1915 .collect::<Vec<_>>(),
1916 ["d.e.f"]
1917 );
1918
1919 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || 'false'")?;
1920 assert_eq!(
1921 expr.dataflow_contexts()
1922 .iter()
1923 .map(|t| t.1.raw)
1924 .collect::<Vec<_>>(),
1925 ["foo.bar"]
1926 );
1927
1928 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || foo.baz")?;
1929 assert_eq!(
1930 expr.dataflow_contexts()
1931 .iter()
1932 .map(|t| t.1.raw)
1933 .collect::<Vec<_>>(),
1934 ["foo.bar", "foo.baz"]
1935 );
1936
1937 let expr = Expr::parse("fromJson(steps.runs.outputs.data).workflow_runs[0].id")?;
1938 assert_eq!(
1939 expr.dataflow_contexts()
1940 .iter()
1941 .map(|t| t.1.raw)
1942 .collect::<Vec<_>>(),
1943 ["fromJson(steps.runs.outputs.data).workflow_runs[0].id"]
1944 );
1945
1946 let expr = Expr::parse("format('{0} {1} {2}', foo.bar, tojson(github), toJSON(github))")?;
1947 assert_eq!(
1948 expr.dataflow_contexts()
1949 .iter()
1950 .map(|t| t.1.raw)
1951 .collect::<Vec<_>>(),
1952 ["foo.bar", "github", "github"]
1953 );
1954
1955 Ok(())
1956 }
1957
1958 #[test]
1959 fn test_spannedexpr_computed_indices() -> Result<()> {
1960 for (expr, computed_indices) in &[
1961 ("foo.bar", vec![]),
1962 ("foo.bar[1]", vec![]),
1963 ("foo.bar[*]", vec![]),
1964 ("foo.bar[abc]", vec!["[abc]"]),
1965 (
1966 "foo.bar[format('{0}', 'foo')]",
1967 vec!["[format('{0}', 'foo')]"],
1968 ),
1969 ("foo.bar[abc].def[efg]", vec!["[abc]", "[efg]"]),
1970 ] {
1971 let expr = Expr::parse(expr)?;
1972
1973 assert_eq!(
1974 expr.computed_indices()
1975 .iter()
1976 .map(|e| e.origin.raw)
1977 .collect::<Vec<_>>(),
1978 *computed_indices
1979 );
1980 }
1981
1982 Ok(())
1983 }
1984
1985 #[test]
1986 fn test_fragment_from_expr() {
1987 for (expr, expected) in &[
1988 ("foo==bar", "foo==bar"),
1989 ("foo == bar", r"foo\s+==\s+bar"),
1990 ("foo == bar", r"foo\s+==\s+bar"),
1991 ("foo(bar)", "foo(bar)"),
1992 ("foo(bar, baz)", r"foo\(bar,\s+baz\)"),
1993 ("foo (bar, baz)", r"foo\s+\(bar,\s+baz\)"),
1994 ("a . b . c . d", r"a\s+\.\s+b\s+\.\s+c\s+\.\s+d"),
1995 ("true \n && \n false", r"true\s+\&\&\s+false"),
1996 ] {
1997 let expr = Expr::parse(expr).unwrap();
1998 match subfeature::Fragment::from(&expr) {
1999 subfeature::Fragment::Raw(actual) => assert_eq!(actual, *expected),
2000 subfeature::Fragment::Regex(actual) => assert_eq!(actual.as_str(), *expected),
2001 };
2002 }
2003 }
2004}