1#![forbid(unsafe_code)]
4#![deny(missing_docs)]
5
6use std::{borrow::Cow, ops::Deref};
7
8use crate::context::Context;
9
10use self::parser::{ExprParser, Rule};
11use anyhow::Result;
12use itertools::Itertools;
13use pest::{Parser, iterators::Pair};
14
15pub mod context;
16
17mod parser {
21 use pest_derive::Parser;
22
23 #[derive(Parser)]
25 #[grammar = "expr.pest"]
26 pub struct ExprParser;
27}
28
29#[derive(Debug)]
33pub struct Function<'src>(pub(crate) &'src str);
34
35impl PartialEq for Function<'_> {
36 fn eq(&self, other: &Self) -> bool {
37 self.0.eq_ignore_ascii_case(other.0)
38 }
39}
40impl PartialEq<str> for Function<'_> {
41 fn eq(&self, other: &str) -> bool {
42 self.0.eq_ignore_ascii_case(other)
43 }
44}
45
46#[derive(Debug)]
51pub struct Identifier<'src>(&'src str);
52
53impl Identifier<'_> {
54 pub fn as_str(&self) -> &str {
60 self.0
61 }
62}
63
64impl PartialEq for Identifier<'_> {
65 fn eq(&self, other: &Self) -> bool {
66 self.0.eq_ignore_ascii_case(other.0)
67 }
68}
69
70impl PartialEq<str> for Identifier<'_> {
71 fn eq(&self, other: &str) -> bool {
72 self.0.eq_ignore_ascii_case(other)
73 }
74}
75
76#[derive(Debug, PartialEq)]
78pub enum BinOp {
79 And,
81 Or,
83 Eq,
85 Neq,
87 Gt,
89 Ge,
91 Lt,
93 Le,
95}
96
97#[derive(Debug, PartialEq)]
99pub enum UnOp {
100 Not,
102}
103
104#[derive(Debug, PartialEq)]
106pub enum Literal<'src> {
107 Number(f64),
109 String(Cow<'src, str>),
111 Boolean(bool),
113 Null,
115}
116
117impl<'src> Literal<'src> {
118 pub fn as_str(&self) -> Cow<'src, str> {
125 match self {
126 Literal::String(s) => s.clone(),
127 Literal::Number(n) => Cow::Owned(n.to_string()),
128 Literal::Boolean(b) => Cow::Owned(b.to_string()),
129 Literal::Null => Cow::Borrowed("null"),
130 }
131 }
132}
133
134#[derive(Copy, Clone, Debug, PartialEq)]
139pub struct Span {
140 pub start: usize,
142 pub end: usize,
144}
145
146impl Span {
147 pub fn adjust(self, bias: usize) -> Self {
149 Self {
150 start: self.start + bias,
151 end: self.end + bias,
152 }
153 }
154}
155
156impl From<pest::Span<'_>> for Span {
157 fn from(span: pest::Span<'_>) -> Self {
158 Self {
159 start: span.start(),
160 end: span.end(),
161 }
162 }
163}
164
165impl From<std::ops::Range<usize>> for Span {
166 fn from(range: std::ops::Range<usize>) -> Self {
167 Self {
168 start: range.start,
169 end: range.end,
170 }
171 }
172}
173
174impl From<Span> for std::ops::Range<usize> {
175 fn from(span: Span) -> Self {
176 span.start..span.end
177 }
178}
179
180#[derive(Copy, Clone, Debug, PartialEq)]
183pub struct Origin<'src> {
184 pub span: Span,
186 pub raw: &'src str,
193}
194
195impl<'a> Origin<'a> {
196 pub fn new(span: impl Into<Span>, raw: &'a str) -> Self {
198 Self {
199 span: span.into(),
200 raw: raw.trim(),
201 }
202 }
203}
204
205#[derive(Debug, PartialEq)]
213pub struct SpannedExpr<'src> {
214 pub origin: Origin<'src>,
216 pub inner: Expr<'src>,
218}
219
220impl<'a> SpannedExpr<'a> {
221 pub(crate) fn new(origin: Origin<'a>, inner: Expr<'a>) -> Self {
223 Self { origin, inner }
224 }
225
226 pub fn dataflow_contexts(&self) -> Vec<(&Context<'a>, &Origin<'a>)> {
235 let mut contexts = vec![];
236
237 match self.deref() {
238 Expr::Call { func, args } => {
239 if func == "toJSON" || func == "format" || func == "join" {
243 for arg in args {
244 contexts.extend(arg.dataflow_contexts());
245 }
246 }
247 }
248 Expr::Context(ctx) => contexts.push((ctx, &self.origin)),
255 Expr::BinOp { lhs, op, rhs } => match op {
256 BinOp::And => {
259 contexts.extend(rhs.dataflow_contexts());
260 }
261 BinOp::Or => {
263 contexts.extend(lhs.dataflow_contexts());
264 contexts.extend(rhs.dataflow_contexts());
265 }
266 _ => (),
267 },
268 _ => (),
269 }
270
271 contexts
272 }
273
274 pub fn computed_indices(&self) -> Vec<&SpannedExpr<'a>> {
279 let mut index_exprs = vec![];
280
281 match self.deref() {
282 Expr::Call { func: _, args } => {
283 for arg in args {
284 index_exprs.extend(arg.computed_indices());
285 }
286 }
287 Expr::Index(spanned_expr) => {
288 if !spanned_expr.is_literal() && !matches!(spanned_expr.inner, Expr::Star) {
290 index_exprs.push(self);
291 }
292 }
293 Expr::Context(context) => {
294 for part in &context.parts {
295 index_exprs.extend(part.computed_indices());
296 }
297 }
298 Expr::BinOp { lhs, op: _, rhs } => {
299 index_exprs.extend(lhs.computed_indices());
300 index_exprs.extend(rhs.computed_indices());
301 }
302 Expr::UnOp { op: _, expr } => {
303 index_exprs.extend(expr.computed_indices());
304 }
305 _ => {}
306 }
307
308 index_exprs
309 }
310
311 pub fn constant_reducible_subexprs(&self) -> Vec<&SpannedExpr<'a>> {
319 if !self.is_literal() && self.constant_reducible() {
320 return vec![self];
321 }
322
323 let mut subexprs = vec![];
324
325 match self.deref() {
326 Expr::Call { func: _, args } => {
327 for arg in args {
328 subexprs.extend(arg.constant_reducible_subexprs());
329 }
330 }
331 Expr::Context(ctx) => {
332 for part in &ctx.parts {
335 subexprs.extend(part.constant_reducible_subexprs());
336 }
337 }
338 Expr::BinOp { lhs, op: _, rhs } => {
339 subexprs.extend(lhs.constant_reducible_subexprs());
340 subexprs.extend(rhs.constant_reducible_subexprs());
341 }
342 Expr::UnOp { op: _, expr } => subexprs.extend(expr.constant_reducible_subexprs()),
343
344 Expr::Index(expr) => subexprs.extend(expr.constant_reducible_subexprs()),
345 _ => {}
346 }
347
348 subexprs
349 }
350}
351
352impl<'a> Deref for SpannedExpr<'a> {
353 type Target = Expr<'a>;
354
355 fn deref(&self) -> &Self::Target {
356 &self.inner
357 }
358}
359
360#[derive(Debug, PartialEq)]
362pub enum Expr<'src> {
363 Literal(Literal<'src>),
365 Star,
367 Call {
369 func: Function<'src>,
371 args: Vec<SpannedExpr<'src>>,
373 },
374 Identifier(Identifier<'src>),
376 Index(Box<SpannedExpr<'src>>),
378 Context(Context<'src>),
380 BinOp {
382 lhs: Box<SpannedExpr<'src>>,
384 op: BinOp,
386 rhs: Box<SpannedExpr<'src>>,
388 },
389 UnOp {
391 op: UnOp,
393 expr: Box<SpannedExpr<'src>>,
395 },
396}
397
398impl<'src> Expr<'src> {
399 fn ident(i: &'src str) -> Self {
401 Self::Identifier(Identifier(i))
402 }
403
404 fn context(components: impl Into<Vec<SpannedExpr<'src>>>) -> Self {
406 Self::Context(Context::new(components))
407 }
408
409 pub fn is_literal(&self) -> bool {
411 matches!(self, Expr::Literal(_))
412 }
413
414 pub fn constant_reducible(&self) -> bool {
433 match self {
434 Expr::Literal(_) => true,
436 Expr::BinOp { lhs, op: _, rhs } => lhs.constant_reducible() && rhs.constant_reducible(),
438 Expr::UnOp { op: _, expr } => expr.constant_reducible(),
440 Expr::Call { func, args } => {
441 if func == "format"
443 || func == "contains"
444 || func == "startsWith"
445 || func == "endsWith"
446 {
447 args.iter().all(|e| e.constant_reducible())
448 } else {
449 false
451 }
452 }
453 _ => false,
455 }
456 }
457
458 pub fn parse(expr: &'src str) -> Result<SpannedExpr<'src>> {
460 let or_expr = ExprParser::parse(Rule::expression, expr)?
462 .next()
463 .unwrap()
464 .into_inner()
465 .next()
466 .unwrap();
467
468 fn parse_pair(pair: Pair<'_, Rule>) -> Result<Box<SpannedExpr>> {
469 match pair.as_rule() {
481 Rule::or_expr => {
482 let (span, raw) = (pair.as_span(), pair.as_str());
483 let mut pairs = pair.into_inner();
484 let lhs = parse_pair(pairs.next().unwrap())?;
485 pairs.try_fold(lhs, |expr, next| {
486 Ok(SpannedExpr::new(
487 Origin::new(span, raw),
488 Expr::BinOp {
489 lhs: expr,
490 op: BinOp::Or,
491 rhs: parse_pair(next)?,
492 },
493 )
494 .into())
495 })
496 }
497 Rule::and_expr => {
498 let (span, raw) = (pair.as_span(), pair.as_str());
499 let mut pairs = pair.into_inner();
500 let lhs = parse_pair(pairs.next().unwrap())?;
501 pairs.try_fold(lhs, |expr, next| {
502 Ok(SpannedExpr::new(
503 Origin::new(span, raw),
504 Expr::BinOp {
505 lhs: expr,
506 op: BinOp::And,
507 rhs: parse_pair(next)?,
508 },
509 )
510 .into())
511 })
512 }
513 Rule::eq_expr => {
514 let (span, raw) = (pair.as_span(), pair.as_str());
518 let mut pairs = pair.into_inner();
519 let lhs = parse_pair(pairs.next().unwrap())?;
520
521 let pair_chunks = pairs.chunks(2);
522 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
523 let eq_op = next.next().unwrap();
524 let comp_expr = next.next().unwrap();
525
526 let eq_op = match eq_op.as_str() {
527 "==" => BinOp::Eq,
528 "!=" => BinOp::Neq,
529 _ => unreachable!(),
530 };
531
532 Ok(SpannedExpr::new(
533 Origin::new(span, raw),
534 Expr::BinOp {
535 lhs: expr,
536 op: eq_op,
537 rhs: parse_pair(comp_expr)?,
538 },
539 )
540 .into())
541 })
542 }
543 Rule::comp_expr => {
544 let (span, raw) = (pair.as_span(), pair.as_str());
546 let mut pairs = pair.into_inner();
547 let lhs = parse_pair(pairs.next().unwrap())?;
548
549 let pair_chunks = pairs.chunks(2);
550 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
551 let comp_op = next.next().unwrap();
552 let unary_expr = next.next().unwrap();
553
554 let eq_op = match comp_op.as_str() {
555 ">" => BinOp::Gt,
556 ">=" => BinOp::Ge,
557 "<" => BinOp::Lt,
558 "<=" => BinOp::Le,
559 _ => unreachable!(),
560 };
561
562 Ok(SpannedExpr::new(
563 Origin::new(span, raw),
564 Expr::BinOp {
565 lhs: expr,
566 op: eq_op,
567 rhs: parse_pair(unary_expr)?,
568 },
569 )
570 .into())
571 })
572 }
573 Rule::unary_expr => {
574 let (span, raw) = (pair.as_span(), pair.as_str());
575 let mut pairs = pair.into_inner();
576 let inner_pair = pairs.next().unwrap();
577
578 match inner_pair.as_rule() {
579 Rule::unary_op => Ok(SpannedExpr::new(
580 Origin::new(span, raw),
581 Expr::UnOp {
582 op: UnOp::Not,
583 expr: parse_pair(pairs.next().unwrap())?,
584 },
585 )
586 .into()),
587 Rule::primary_expr => parse_pair(inner_pair),
588 _ => unreachable!(),
589 }
590 }
591 Rule::primary_expr => {
592 parse_pair(pair.into_inner().next().unwrap())
594 }
595 Rule::number => Ok(SpannedExpr::new(
596 Origin::new(pair.as_span(), pair.as_str()),
597 pair.as_str().parse::<f64>().unwrap().into(),
598 )
599 .into()),
600 Rule::string => {
601 let (span, raw) = (pair.as_span(), pair.as_str());
602 let string_inner = pair.into_inner().next().unwrap().as_str();
604
605 if !string_inner.contains('\'') {
608 Ok(SpannedExpr::new(Origin::new(span, raw), string_inner.into()).into())
609 } else {
610 Ok(SpannedExpr::new(
611 Origin::new(span, raw),
612 string_inner.replace("''", "'").into(),
613 )
614 .into())
615 }
616 }
617 Rule::boolean => Ok(SpannedExpr::new(
618 Origin::new(pair.as_span(), pair.as_str()),
619 pair.as_str().parse::<bool>().unwrap().into(),
620 )
621 .into()),
622 Rule::null => Ok(SpannedExpr::new(
623 Origin::new(pair.as_span(), pair.as_str()),
624 Expr::Literal(Literal::Null),
625 )
626 .into()),
627 Rule::star => Ok(SpannedExpr::new(
628 Origin::new(pair.as_span(), pair.as_str()),
629 Expr::Star,
630 )
631 .into()),
632 Rule::function_call => {
633 let (span, raw) = (pair.as_span(), pair.as_str());
634 let mut pairs = pair.into_inner();
635
636 let identifier = pairs.next().unwrap();
637 let args = pairs
638 .map(|pair| parse_pair(pair).map(|e| *e))
639 .collect::<Result<_, _>>()?;
640
641 Ok(SpannedExpr::new(
642 Origin::new(span, raw),
643 Expr::Call {
644 func: Function(identifier.as_str()),
645 args,
646 },
647 )
648 .into())
649 }
650 Rule::identifier => Ok(SpannedExpr::new(
651 Origin::new(pair.as_span(), pair.as_str()),
652 Expr::ident(pair.as_str()),
653 )
654 .into()),
655 Rule::index => Ok(SpannedExpr::new(
656 Origin::new(pair.as_span(), pair.as_str()),
657 Expr::Index(parse_pair(pair.into_inner().next().unwrap())?),
658 )
659 .into()),
660 Rule::context => {
661 let (span, raw) = (pair.as_span(), pair.as_str());
662 let pairs = pair.into_inner();
663
664 let mut inner: Vec<SpannedExpr> = pairs
665 .map(|pair| parse_pair(pair).map(|e| *e))
666 .collect::<Result<_, _>>()?;
667
668 if inner.len() == 1 && matches!(inner[0].inner, Expr::Call { .. }) {
672 Ok(inner.remove(0).into())
673 } else {
674 Ok(SpannedExpr::new(Origin::new(span, raw), Expr::context(inner)).into())
675 }
676 }
677 r => panic!("unrecognized rule: {r:?}"),
678 }
679 }
680
681 parse_pair(or_expr).map(|e| *e)
682 }
683}
684
685impl<'src> From<&'src str> for Expr<'src> {
686 fn from(s: &'src str) -> Self {
687 Expr::Literal(Literal::String(s.into()))
688 }
689}
690
691impl From<String> for Expr<'_> {
692 fn from(s: String) -> Self {
693 Expr::Literal(Literal::String(s.into()))
694 }
695}
696
697impl From<f64> for Expr<'_> {
698 fn from(n: f64) -> Self {
699 Expr::Literal(Literal::Number(n))
700 }
701}
702
703impl From<bool> for Expr<'_> {
704 fn from(b: bool) -> Self {
705 Expr::Literal(Literal::Boolean(b))
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use std::borrow::Cow;
712
713 use anyhow::Result;
714 use pest::Parser as _;
715 use pretty_assertions::assert_eq;
716
717 use crate::{Literal, Origin, SpannedExpr};
718
719 use super::{BinOp, Expr, ExprParser, Function, Rule, UnOp};
720
721 #[test]
722 fn test_literal_string_borrows() {
723 let cases = &[
724 ("'foo'", true),
725 ("'foo bar'", true),
726 ("'foo '' bar'", false),
727 ("'foo''bar'", false),
728 ("'foo''''bar'", false),
729 ];
730
731 for (expr, borrows) in cases {
732 let Expr::Literal(Literal::String(s)) = &*Expr::parse(expr).unwrap() else {
733 panic!("expected a literal string expression for {expr}");
734 };
735
736 assert!(matches!(
737 (s, borrows),
738 (Cow::Borrowed(_), true) | (Cow::Owned(_), false)
739 ));
740 }
741 }
742
743 #[test]
744 fn test_literal_as_str() {
745 let cases = &[
746 ("'foo'", "foo"),
747 ("'foo '' bar'", "foo ' bar"),
748 ("123", "123"),
749 ("123.000", "123"),
750 ("0.0", "0"),
751 ("0.1", "0.1"),
752 ("0.12345", "0.12345"),
753 ("true", "true"),
754 ("false", "false"),
755 ("null", "null"),
756 ];
757
758 for (expr, expected) in cases {
759 let Expr::Literal(expr) = &*Expr::parse(expr).unwrap() else {
760 panic!("expected a literal expression for {expr}");
761 };
762
763 assert_eq!(expr.as_str(), *expected);
764 }
765 }
766
767 #[test]
768 fn test_function_eq() {
769 let func = Function("foo");
770 assert_eq!(&func, "foo");
771 assert_eq!(&func, "FOO");
772 assert_eq!(&func, "Foo");
773
774 assert_eq!(func, Function("FOO"));
775 }
776
777 #[test]
778 fn test_parse_string_rule() {
779 let cases = &[
780 ("''", ""),
781 ("' '", " "),
782 ("''''", "''"),
783 ("'test'", "test"),
784 ("'spaces are ok'", "spaces are ok"),
785 ("'escaping '' works'", "escaping '' works"),
786 ];
787
788 for (case, expected) in cases {
789 let s = ExprParser::parse(Rule::string, case)
790 .unwrap()
791 .next()
792 .unwrap();
793
794 assert_eq!(s.into_inner().next().unwrap().as_str(), *expected);
795 }
796 }
797
798 #[test]
799 fn test_parse_context_rule() {
800 let cases = &[
801 "foo.bar",
802 "github.action_path",
803 "inputs.foo-bar",
804 "inputs.also--valid",
805 "inputs.this__too",
806 "inputs.this__too",
807 "secrets.GH_TOKEN",
808 "foo.*.bar",
809 "github.event.issue.labels.*.name",
810 ];
811
812 for case in cases {
813 assert_eq!(
814 ExprParser::parse(Rule::context, case)
815 .unwrap()
816 .next()
817 .unwrap()
818 .as_str(),
819 *case
820 );
821 }
822 }
823
824 #[test]
825 fn test_parse_call_rule() {
826 let cases = &[
827 "foo()",
828 "foo(bar)",
829 "foo(bar())",
830 "foo(1.23)",
831 "foo(1,2)",
832 "foo(1, 2)",
833 "foo(1, 2, secret.GH_TOKEN)",
834 "foo( )",
835 "fromJSON(inputs.free-threading)",
836 ];
837
838 for case in cases {
839 assert_eq!(
840 ExprParser::parse(Rule::function_call, case)
841 .unwrap()
842 .next()
843 .unwrap()
844 .as_str(),
845 *case
846 );
847 }
848 }
849
850 #[test]
851 fn test_parse_expr_rule() -> Result<()> {
852 let multiline = "github.repository_owner == 'Homebrew' &&
854 ((github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
855 (github.event_name == 'pull_request_target' &&
856 (github.event.action == 'ready_for_review' || github.event.label.name == 'automerge-skip')))";
857
858 let multiline2 = "foo.bar.baz[
859 0
860 ]";
861
862 let cases = &[
863 "fromJSON(inputs.free-threading) && '--disable-gil' || ''",
864 "foo || bar || baz",
865 "foo || bar && baz || foo && 1 && 2 && 3 || 4",
866 "(github.actor != 'github-actions[bot]' && github.actor) || 'BrewTestBot'",
867 "(true || false) == true",
868 "!(!true || false)",
869 "!(!true || false) == true",
870 "(true == false) == true",
871 "(true == (false || true && (true || false))) == true",
872 "(github.actor != 'github-actions[bot]' && github.actor) == 'BrewTestBot'",
873 "foo()[0]",
874 "fromJson(steps.runs.outputs.data).workflow_runs[0].id",
875 multiline,
876 "'a' == 'b' && 'c' || 'd'",
877 "github.event['a']",
878 "github.event['a' == 'b']",
879 "github.event['a' == 'b' && 'c' || 'd']",
880 "github['event']['inputs']['dry-run']",
881 "github[format('{0}', 'event')]",
882 "github['event']['inputs'][github.event.inputs.magic]",
883 "github['event']['inputs'].*",
884 "1 == 1",
885 "1 > 1",
886 "1 >= 1",
887 "matrix.node_version >= 20",
888 "true||false",
889 multiline2,
890 "fromJSON( github.event.inputs.hmm ) [ 0 ]",
891 ];
892
893 for case in cases {
894 assert_eq!(
895 ExprParser::parse(Rule::expression, case)?
896 .next()
897 .unwrap()
898 .as_str(),
899 *case
900 );
901 }
902
903 Ok(())
904 }
905
906 #[test]
907 fn test_parse() {
908 let cases = &[
909 (
910 "!true || false || true",
911 SpannedExpr::new(
912 Origin::new(0..22, "!true || false || true"),
913 Expr::BinOp {
914 lhs: SpannedExpr::new(
915 Origin::new(0..22, "!true || false || true"),
916 Expr::BinOp {
917 lhs: SpannedExpr::new(
918 Origin::new(0..5, "!true"),
919 Expr::UnOp {
920 op: UnOp::Not,
921 expr: SpannedExpr::new(
922 Origin::new(1..5, "true"),
923 true.into(),
924 )
925 .into(),
926 },
927 )
928 .into(),
929 op: BinOp::Or,
930 rhs: SpannedExpr::new(Origin::new(9..14, "false"), false.into())
931 .into(),
932 },
933 )
934 .into(),
935 op: BinOp::Or,
936 rhs: SpannedExpr::new(Origin::new(18..22, "true"), true.into()).into(),
937 },
938 ),
939 ),
940 (
941 "'foo '' bar'",
942 SpannedExpr::new(
943 Origin::new(0..12, "'foo '' bar'"),
944 Expr::Literal(Literal::String("foo ' bar".into())),
945 ),
946 ),
947 (
948 "('foo '' bar')",
949 SpannedExpr::new(
950 Origin::new(1..13, "'foo '' bar'"),
951 Expr::Literal(Literal::String("foo ' bar".into())),
952 ),
953 ),
954 (
955 "((('foo '' bar')))",
956 SpannedExpr::new(
957 Origin::new(3..15, "'foo '' bar'"),
958 Expr::Literal(Literal::String("foo ' bar".into())),
959 ),
960 ),
961 (
962 "foo(1, 2, 3)",
963 SpannedExpr::new(
964 Origin::new(0..12, "foo(1, 2, 3)"),
965 Expr::Call {
966 func: Function("foo"),
967 args: vec![
968 SpannedExpr::new(Origin::new(4..5, "1"), 1.0.into()),
969 SpannedExpr::new(Origin::new(7..8, "2"), 2.0.into()),
970 SpannedExpr::new(Origin::new(10..11, "3"), 3.0.into()),
971 ],
972 },
973 ),
974 ),
975 (
976 "foo.bar.baz",
977 SpannedExpr::new(
978 Origin::new(0..11, "foo.bar.baz"),
979 Expr::context(vec![
980 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
981 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
982 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
983 ]),
984 ),
985 ),
986 (
987 "foo.bar.baz[1][2]",
988 SpannedExpr::new(
989 Origin::new(0..17, "foo.bar.baz[1][2]"),
990 Expr::context(vec![
991 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
992 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
993 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
994 SpannedExpr::new(
995 Origin::new(11..14, "[1]"),
996 Expr::Index(Box::new(SpannedExpr::new(
997 Origin::new(12..13, "1"),
998 1.0.into(),
999 ))),
1000 ),
1001 SpannedExpr::new(
1002 Origin::new(14..17, "[2]"),
1003 Expr::Index(Box::new(SpannedExpr::new(
1004 Origin::new(15..16, "2"),
1005 2.0.into(),
1006 ))),
1007 ),
1008 ]),
1009 ),
1010 ),
1011 (
1012 "foo.bar.baz[*]",
1013 SpannedExpr::new(
1014 Origin::new(0..14, "foo.bar.baz[*]"),
1015 Expr::context([
1016 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1017 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1018 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1019 SpannedExpr::new(
1020 Origin::new(11..14, "[*]"),
1021 Expr::Index(Box::new(SpannedExpr::new(
1022 Origin::new(12..13, "*"),
1023 Expr::Star,
1024 ))),
1025 ),
1026 ]),
1027 ),
1028 ),
1029 (
1030 "vegetables.*.ediblePortions",
1031 SpannedExpr::new(
1032 Origin::new(0..27, "vegetables.*.ediblePortions"),
1033 Expr::context(vec![
1034 SpannedExpr::new(
1035 Origin::new(0..10, "vegetables"),
1036 Expr::ident("vegetables"),
1037 ),
1038 SpannedExpr::new(Origin::new(11..12, "*"), Expr::Star),
1039 SpannedExpr::new(
1040 Origin::new(13..27, "ediblePortions"),
1041 Expr::ident("ediblePortions"),
1042 ),
1043 ]),
1044 ),
1045 ),
1046 (
1047 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1050 SpannedExpr::new(
1051 Origin::new(
1052 0..88,
1053 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1054 ),
1055 Expr::BinOp {
1056 lhs: Box::new(SpannedExpr::new(
1057 Origin::new(
1058 0..59,
1059 "github.ref == 'refs/heads/main' && 'value_for_main_branch'",
1060 ),
1061 Expr::BinOp {
1062 lhs: Box::new(SpannedExpr::new(
1063 Origin::new(0..32, "github.ref == 'refs/heads/main'"),
1064 Expr::BinOp {
1065 lhs: Box::new(SpannedExpr::new(
1066 Origin::new(0..10, "github.ref"),
1067 Expr::context(vec![
1068 SpannedExpr::new(
1069 Origin::new(0..6, "github"),
1070 Expr::ident("github"),
1071 ),
1072 SpannedExpr::new(
1073 Origin::new(7..10, "ref"),
1074 Expr::ident("ref"),
1075 ),
1076 ]),
1077 )),
1078 op: BinOp::Eq,
1079 rhs: Box::new(SpannedExpr::new(
1080 Origin::new(14..31, "'refs/heads/main'"),
1081 Expr::Literal(Literal::String(
1082 "refs/heads/main".into(),
1083 )),
1084 )),
1085 },
1086 )),
1087 op: BinOp::And,
1088 rhs: Box::new(SpannedExpr::new(
1089 Origin::new(35..58, "'value_for_main_branch'"),
1090 Expr::Literal(Literal::String("value_for_main_branch".into())),
1091 )),
1092 },
1093 )),
1094 op: BinOp::Or,
1095 rhs: Box::new(SpannedExpr::new(
1096 Origin::new(62..88, "'value_for_other_branches'"),
1097 Expr::Literal(Literal::String("value_for_other_branches".into())),
1098 )),
1099 },
1100 ),
1101 ),
1102 (
1103 "(true || false) == true",
1104 SpannedExpr::new(
1105 Origin::new(0..23, "(true || false) == true"),
1106 Expr::BinOp {
1107 lhs: Box::new(SpannedExpr::new(
1108 Origin::new(1..14, "true || false"),
1109 Expr::BinOp {
1110 lhs: Box::new(SpannedExpr::new(
1111 Origin::new(1..5, "true"),
1112 true.into(),
1113 )),
1114 op: BinOp::Or,
1115 rhs: Box::new(SpannedExpr::new(
1116 Origin::new(9..14, "false"),
1117 false.into(),
1118 )),
1119 },
1120 )),
1121 op: BinOp::Eq,
1122 rhs: Box::new(SpannedExpr::new(Origin::new(19..23, "true"), true.into())),
1123 },
1124 ),
1125 ),
1126 (
1127 "!(!true || false)",
1128 SpannedExpr::new(
1129 Origin::new(0..17, "!(!true || false)"),
1130 Expr::UnOp {
1131 op: UnOp::Not,
1132 expr: Box::new(SpannedExpr::new(
1133 Origin::new(2..16, "!true || false"),
1134 Expr::BinOp {
1135 lhs: Box::new(SpannedExpr::new(
1136 Origin::new(2..7, "!true"),
1137 Expr::UnOp {
1138 op: UnOp::Not,
1139 expr: Box::new(SpannedExpr::new(
1140 Origin::new(3..7, "true"),
1141 true.into(),
1142 )),
1143 },
1144 )),
1145 op: BinOp::Or,
1146 rhs: Box::new(SpannedExpr::new(
1147 Origin::new(11..16, "false"),
1148 false.into(),
1149 )),
1150 },
1151 )),
1152 },
1153 ),
1154 ),
1155 (
1156 "foobar[format('{0}', 'event')]",
1157 SpannedExpr::new(
1158 Origin::new(0..30, "foobar[format('{0}', 'event')]"),
1159 Expr::context([
1160 SpannedExpr::new(Origin::new(0..6, "foobar"), Expr::ident("foobar")),
1161 SpannedExpr::new(
1162 Origin::new(6..30, "[format('{0}', 'event')]"),
1163 Expr::Index(Box::new(SpannedExpr::new(
1164 Origin::new(7..29, "format('{0}', 'event')"),
1165 Expr::Call {
1166 func: Function("format"),
1167 args: vec![
1168 SpannedExpr::new(
1169 Origin::new(14..19, "'{0}'"),
1170 Expr::from("{0}"),
1171 ),
1172 SpannedExpr::new(
1173 Origin::new(21..28, "'event'"),
1174 Expr::from("event"),
1175 ),
1176 ],
1177 },
1178 ))),
1179 ),
1180 ]),
1181 ),
1182 ),
1183 (
1184 "github.actor_id == '49699333'",
1185 SpannedExpr::new(
1186 Origin::new(0..29, "github.actor_id == '49699333'"),
1187 Expr::BinOp {
1188 lhs: SpannedExpr::new(
1189 Origin::new(0..15, "github.actor_id"),
1190 Expr::context(vec![
1191 SpannedExpr::new(
1192 Origin::new(0..6, "github"),
1193 Expr::ident("github"),
1194 ),
1195 SpannedExpr::new(
1196 Origin::new(7..15, "actor_id"),
1197 Expr::ident("actor_id"),
1198 ),
1199 ]),
1200 )
1201 .into(),
1202 op: BinOp::Eq,
1203 rhs: Box::new(SpannedExpr::new(
1204 Origin::new(19..29, "'49699333'"),
1205 Expr::from("49699333"),
1206 )),
1207 },
1208 ),
1209 ),
1210 ];
1211
1212 for (case, expr) in cases {
1213 assert_eq!(*expr, Expr::parse(case).unwrap());
1214 }
1215 }
1216
1217 #[test]
1218 fn test_expr_constant_reducible() -> Result<()> {
1219 for (expr, reducible) in &[
1220 ("'foo'", true),
1221 ("1", true),
1222 ("true", true),
1223 ("null", true),
1224 ("!true", true),
1227 ("!null", true),
1228 ("true && false", true),
1229 ("true || false", true),
1230 ("null && !null && true", true),
1231 ("format('{0} {1}', 'foo', 'bar')", true),
1234 ("format('{0} {1}', 1, 2)", true),
1235 ("format('{0} {1}', 1, '2')", true),
1236 ("contains('foo', 'bar')", true),
1237 ("startsWith('foo', 'bar')", true),
1238 ("endsWith('foo', 'bar')", true),
1239 ("startsWith(some.context, 'bar')", false),
1240 ("endsWith(some.context, 'bar')", false),
1241 ("format('{0} {1}', '1', format('{0}', null))", true),
1243 ("format('{0} {1}', '1', startsWith('foo', 'foo'))", true),
1244 ("format('{0} {1}', '1', startsWith(foo.bar, 'foo'))", false),
1245 ("foo", false),
1246 ("foo.bar", false),
1247 ("foo.bar[1]", false),
1248 ("foo.bar == 'bar'", false),
1249 ("foo.bar || bar || baz", false),
1250 ("foo.bar && bar && baz", false),
1251 ] {
1252 let expr = Expr::parse(expr)?;
1253 assert_eq!(expr.constant_reducible(), *reducible);
1254 }
1255
1256 Ok(())
1257 }
1258
1259 #[test]
1260 fn test_expr_has_constant_reducible_subexpr() -> Result<()> {
1261 for (expr, reducible) in &[
1262 ("'foo'", false),
1264 ("1", false),
1265 ("true", false),
1266 ("null", false),
1267 (
1269 "format('{0}, {1}', github.event.number, format('{0}', 'abc'))",
1270 true,
1271 ),
1272 ("foobar[format('{0}', 'event')]", true),
1273 ] {
1274 let expr = Expr::parse(expr)?;
1275 assert_eq!(!expr.constant_reducible_subexprs().is_empty(), *reducible);
1276 }
1277 Ok(())
1278 }
1279
1280 #[test]
1281 fn test_expr_dataflow_contexts() -> Result<()> {
1282 let expr = Expr::parse("foo.bar")?;
1284 assert_eq!(
1285 expr.dataflow_contexts()
1286 .iter()
1287 .map(|t| t.1.raw)
1288 .collect::<Vec<_>>(),
1289 ["foo.bar"]
1290 );
1291
1292 let expr = Expr::parse("foo.bar[1]")?;
1293 assert_eq!(
1294 expr.dataflow_contexts()
1295 .iter()
1296 .map(|t| t.1.raw)
1297 .collect::<Vec<_>>(),
1298 ["foo.bar[1]"]
1299 );
1300
1301 let expr = Expr::parse("foo.bar == 'bar'")?;
1303 assert!(expr.dataflow_contexts().is_empty());
1304
1305 let expr = Expr::parse("foo.bar || abc || d.e.f")?;
1307 assert_eq!(
1308 expr.dataflow_contexts()
1309 .iter()
1310 .map(|t| t.1.raw)
1311 .collect::<Vec<_>>(),
1312 ["foo.bar", "abc", "d.e.f"]
1313 );
1314
1315 let expr = Expr::parse("foo.bar && abc && d.e.f")?;
1317 assert_eq!(
1318 expr.dataflow_contexts()
1319 .iter()
1320 .map(|t| t.1.raw)
1321 .collect::<Vec<_>>(),
1322 ["d.e.f"]
1323 );
1324
1325 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || 'false'")?;
1326 assert_eq!(
1327 expr.dataflow_contexts()
1328 .iter()
1329 .map(|t| t.1.raw)
1330 .collect::<Vec<_>>(),
1331 ["foo.bar"]
1332 );
1333
1334 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || foo.baz")?;
1335 assert_eq!(
1336 expr.dataflow_contexts()
1337 .iter()
1338 .map(|t| t.1.raw)
1339 .collect::<Vec<_>>(),
1340 ["foo.bar", "foo.baz"]
1341 );
1342
1343 let expr = Expr::parse("fromJson(steps.runs.outputs.data).workflow_runs[0].id")?;
1344 assert_eq!(
1345 expr.dataflow_contexts()
1346 .iter()
1347 .map(|t| t.1.raw)
1348 .collect::<Vec<_>>(),
1349 ["fromJson(steps.runs.outputs.data).workflow_runs[0].id"]
1350 );
1351
1352 let expr = Expr::parse("format('{0} {1} {2}', foo.bar, tojson(github), toJSON(github))")?;
1353 assert_eq!(
1354 expr.dataflow_contexts()
1355 .iter()
1356 .map(|t| t.1.raw)
1357 .collect::<Vec<_>>(),
1358 ["foo.bar", "github", "github"]
1359 );
1360
1361 Ok(())
1362 }
1363
1364 #[test]
1365 fn test_spannedexpr_computed_indices() -> Result<()> {
1366 for (expr, computed_indices) in &[
1367 ("foo.bar", vec![]),
1368 ("foo.bar[1]", vec![]),
1369 ("foo.bar[*]", vec![]),
1370 ("foo.bar[abc]", vec!["[abc]"]),
1371 (
1372 "foo.bar[format('{0}', 'foo')]",
1373 vec!["[format('{0}', 'foo')]"],
1374 ),
1375 ("foo.bar[abc].def[efg]", vec!["[abc]", "[efg]"]),
1376 ] {
1377 let expr = Expr::parse(expr)?;
1378
1379 assert_eq!(
1380 expr.computed_indices()
1381 .iter()
1382 .map(|e| e.origin.raw)
1383 .collect::<Vec<_>>(),
1384 *computed_indices
1385 );
1386 }
1387
1388 Ok(())
1389 }
1390}