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