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 dataflow_contexts(&self) -> Vec<(&Context<'a>, &Origin<'a>)> {
94 let mut contexts = vec![];
95
96 match self.deref() {
97 Expr::Call(Call { func, args }) => {
98 if func == "toJSON" || func == "format" || func == "join" {
102 for arg in args {
103 contexts.extend(arg.dataflow_contexts());
104 }
105 }
106 }
107 Expr::Context(ctx) => contexts.push((ctx, &self.origin)),
114 Expr::BinOp { lhs, op, rhs } => match op {
115 BinOp::And => {
118 contexts.extend(rhs.dataflow_contexts());
119 }
120 BinOp::Or => {
122 contexts.extend(lhs.dataflow_contexts());
123 contexts.extend(rhs.dataflow_contexts());
124 }
125 _ => (),
126 },
127 _ => (),
128 }
129
130 contexts
131 }
132
133 pub fn computed_indices(&self) -> Vec<&SpannedExpr<'a>> {
138 let mut index_exprs = vec![];
139
140 match self.deref() {
141 Expr::Call(Call { func: _, args }) => {
142 for arg in args {
143 index_exprs.extend(arg.computed_indices());
144 }
145 }
146 Expr::Index(spanned_expr) => {
147 if !spanned_expr.is_literal() && !matches!(spanned_expr.inner, Expr::Star) {
149 index_exprs.push(self);
150 }
151 }
152 Expr::Context(context) => {
153 for part in &context.parts {
154 index_exprs.extend(part.computed_indices());
155 }
156 }
157 Expr::BinOp { lhs, op: _, rhs } => {
158 index_exprs.extend(lhs.computed_indices());
159 index_exprs.extend(rhs.computed_indices());
160 }
161 Expr::UnOp { op: _, expr } => {
162 index_exprs.extend(expr.computed_indices());
163 }
164 _ => {}
165 }
166
167 index_exprs
168 }
169
170 pub fn constant_reducible_subexprs(&self) -> Vec<&SpannedExpr<'a>> {
178 if !self.is_literal() && self.constant_reducible() {
179 return vec![self];
180 }
181
182 let mut subexprs = vec![];
183
184 match self.deref() {
185 Expr::Call(Call { func: _, args }) => {
186 for arg in args {
187 subexprs.extend(arg.constant_reducible_subexprs());
188 }
189 }
190 Expr::Context(ctx) => {
191 for part in &ctx.parts {
194 subexprs.extend(part.constant_reducible_subexprs());
195 }
196 }
197 Expr::BinOp { lhs, op: _, rhs } => {
198 subexprs.extend(lhs.constant_reducible_subexprs());
199 subexprs.extend(rhs.constant_reducible_subexprs());
200 }
201 Expr::UnOp { op: _, expr } => subexprs.extend(expr.constant_reducible_subexprs()),
202
203 Expr::Index(expr) => subexprs.extend(expr.constant_reducible_subexprs()),
204 _ => {}
205 }
206
207 subexprs
208 }
209}
210
211impl<'a> Deref for SpannedExpr<'a> {
212 type Target = Expr<'a>;
213
214 fn deref(&self) -> &Self::Target {
215 &self.inner
216 }
217}
218
219impl<'doc> From<&SpannedExpr<'doc>> for subfeature::Fragment<'doc> {
220 fn from(expr: &SpannedExpr<'doc>) -> Self {
221 Self::new(expr.origin.raw)
222 }
223}
224
225#[derive(Debug, PartialEq)]
227pub enum Expr<'src> {
228 Literal(Literal<'src>),
230 Star,
232 Call(Call<'src>),
234 Identifier(Identifier<'src>),
236 Index(Box<SpannedExpr<'src>>),
238 Context(Context<'src>),
240 BinOp {
242 lhs: Box<SpannedExpr<'src>>,
244 op: BinOp,
246 rhs: Box<SpannedExpr<'src>>,
248 },
249 UnOp {
251 op: UnOp,
253 expr: Box<SpannedExpr<'src>>,
255 },
256}
257
258impl<'src> Expr<'src> {
259 fn ident(i: &'src str) -> Self {
261 Self::Identifier(Identifier(i))
262 }
263
264 fn context(components: impl Into<Vec<SpannedExpr<'src>>>) -> Self {
266 Self::Context(Context::new(components))
267 }
268
269 pub fn is_literal(&self) -> bool {
271 matches!(self, Expr::Literal(_))
272 }
273
274 pub fn constant_reducible(&self) -> bool {
293 match self {
294 Expr::Literal(_) => true,
296 Expr::BinOp { lhs, op: _, rhs } => lhs.constant_reducible() && rhs.constant_reducible(),
298 Expr::UnOp { op: _, expr } => expr.constant_reducible(),
300 Expr::Call(Call { func, args }) => {
301 if func == "format"
303 || func == "contains"
304 || func == "startsWith"
305 || func == "endsWith"
306 || func == "toJSON"
307 || func == "fromJSON"
308 || func == "join"
309 {
310 args.iter().all(|e| e.constant_reducible())
311 } else {
312 false
313 }
314 }
315 _ => false,
317 }
318 }
319
320 pub fn parse(expr: &'src str) -> Result<SpannedExpr<'src>> {
322 let or_expr = ExprParser::parse(Rule::expression, expr)?
324 .next()
325 .unwrap()
326 .into_inner()
327 .next()
328 .unwrap();
329
330 fn parse_pair(pair: Pair<'_, Rule>) -> Result<Box<SpannedExpr<'_>>> {
331 match pair.as_rule() {
343 Rule::or_expr => {
344 let (span, raw) = (pair.as_span(), pair.as_str());
345 let mut pairs = pair.into_inner();
346 let lhs = parse_pair(pairs.next().unwrap())?;
347 pairs.try_fold(lhs, |expr, next| {
348 Ok(SpannedExpr::new(
349 Origin::new(span.start()..span.end(), raw),
350 Expr::BinOp {
351 lhs: expr,
352 op: BinOp::Or,
353 rhs: parse_pair(next)?,
354 },
355 )
356 .into())
357 })
358 }
359 Rule::and_expr => {
360 let (span, raw) = (pair.as_span(), pair.as_str());
361 let mut pairs = pair.into_inner();
362 let lhs = parse_pair(pairs.next().unwrap())?;
363 pairs.try_fold(lhs, |expr, next| {
364 Ok(SpannedExpr::new(
365 Origin::new(span.start()..span.end(), raw),
366 Expr::BinOp {
367 lhs: expr,
368 op: BinOp::And,
369 rhs: parse_pair(next)?,
370 },
371 )
372 .into())
373 })
374 }
375 Rule::eq_expr => {
376 let (span, raw) = (pair.as_span(), pair.as_str());
380 let mut pairs = pair.into_inner();
381 let lhs = parse_pair(pairs.next().unwrap())?;
382
383 let pair_chunks = pairs.chunks(2);
384 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
385 let eq_op = next.next().unwrap();
386 let comp_expr = next.next().unwrap();
387
388 let eq_op = match eq_op.as_str() {
389 "==" => BinOp::Eq,
390 "!=" => BinOp::Neq,
391 _ => unreachable!(),
392 };
393
394 Ok(SpannedExpr::new(
395 Origin::new(span.start()..span.end(), raw),
396 Expr::BinOp {
397 lhs: expr,
398 op: eq_op,
399 rhs: parse_pair(comp_expr)?,
400 },
401 )
402 .into())
403 })
404 }
405 Rule::comp_expr => {
406 let (span, raw) = (pair.as_span(), pair.as_str());
408 let mut pairs = pair.into_inner();
409 let lhs = parse_pair(pairs.next().unwrap())?;
410
411 let pair_chunks = pairs.chunks(2);
412 pair_chunks.into_iter().try_fold(lhs, |expr, mut next| {
413 let comp_op = next.next().unwrap();
414 let unary_expr = next.next().unwrap();
415
416 let eq_op = match comp_op.as_str() {
417 ">" => BinOp::Gt,
418 ">=" => BinOp::Ge,
419 "<" => BinOp::Lt,
420 "<=" => BinOp::Le,
421 _ => unreachable!(),
422 };
423
424 Ok(SpannedExpr::new(
425 Origin::new(span.start()..span.end(), raw),
426 Expr::BinOp {
427 lhs: expr,
428 op: eq_op,
429 rhs: parse_pair(unary_expr)?,
430 },
431 )
432 .into())
433 })
434 }
435 Rule::unary_expr => {
436 let (span, raw) = (pair.as_span(), pair.as_str());
437 let mut pairs = pair.into_inner();
438 let inner_pair = pairs.next().unwrap();
439
440 match inner_pair.as_rule() {
441 Rule::unary_op => Ok(SpannedExpr::new(
442 Origin::new(span.start()..span.end(), raw),
443 Expr::UnOp {
444 op: UnOp::Not,
445 expr: parse_pair(pairs.next().unwrap())?,
446 },
447 )
448 .into()),
449 Rule::primary_expr => parse_pair(inner_pair),
450 _ => unreachable!(),
451 }
452 }
453 Rule::primary_expr => {
454 parse_pair(pair.into_inner().next().unwrap())
456 }
457 Rule::number => Ok(SpannedExpr::new(
458 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
459 pair.as_str().parse::<f64>().unwrap().into(),
460 )
461 .into()),
462 Rule::string => {
463 let (span, raw) = (pair.as_span(), pair.as_str());
464 let string_inner = pair.into_inner().next().unwrap().as_str();
466
467 if !string_inner.contains('\'') {
470 Ok(SpannedExpr::new(
471 Origin::new(span.start()..span.end(), raw),
472 string_inner.into(),
473 )
474 .into())
475 } else {
476 Ok(SpannedExpr::new(
477 Origin::new(span.start()..span.end(), raw),
478 string_inner.replace("''", "'").into(),
479 )
480 .into())
481 }
482 }
483 Rule::boolean => Ok(SpannedExpr::new(
484 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
485 pair.as_str().parse::<bool>().unwrap().into(),
486 )
487 .into()),
488 Rule::null => Ok(SpannedExpr::new(
489 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
490 Expr::Literal(Literal::Null),
491 )
492 .into()),
493 Rule::star => Ok(SpannedExpr::new(
494 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
495 Expr::Star,
496 )
497 .into()),
498 Rule::function_call => {
499 let (span, raw) = (pair.as_span(), pair.as_str());
500 let mut pairs = pair.into_inner();
501
502 let identifier = pairs.next().unwrap();
503 let args = pairs
504 .map(|pair| parse_pair(pair).map(|e| *e))
505 .collect::<Result<_, _>>()?;
506
507 Ok(SpannedExpr::new(
508 Origin::new(span.start()..span.end(), raw),
509 Expr::Call(Call {
510 func: Function(identifier.as_str()),
511 args,
512 }),
513 )
514 .into())
515 }
516 Rule::identifier => Ok(SpannedExpr::new(
517 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
518 Expr::ident(pair.as_str()),
519 )
520 .into()),
521 Rule::index => Ok(SpannedExpr::new(
522 Origin::new(pair.as_span().start()..pair.as_span().end(), pair.as_str()),
523 Expr::Index(parse_pair(pair.into_inner().next().unwrap())?),
524 )
525 .into()),
526 Rule::context => {
527 let (span, raw) = (pair.as_span(), pair.as_str());
528 let pairs = pair.into_inner();
529
530 let mut inner: Vec<SpannedExpr> = pairs
531 .map(|pair| parse_pair(pair).map(|e| *e))
532 .collect::<Result<_, _>>()?;
533
534 if inner.len() == 1 && matches!(inner[0].inner, Expr::Call { .. }) {
538 Ok(inner.remove(0).into())
539 } else {
540 Ok(SpannedExpr::new(
541 Origin::new(span.start()..span.end(), raw),
542 Expr::context(inner),
543 )
544 .into())
545 }
546 }
547 r => panic!("unrecognized rule: {r:?}"),
548 }
549 }
550
551 parse_pair(or_expr).map(|e| *e)
552 }
553}
554
555impl<'src> From<&'src str> for Expr<'src> {
556 fn from(s: &'src str) -> Self {
557 Expr::Literal(Literal::String(s.into()))
558 }
559}
560
561impl From<String> for Expr<'_> {
562 fn from(s: String) -> Self {
563 Expr::Literal(Literal::String(s.into()))
564 }
565}
566
567impl From<f64> for Expr<'_> {
568 fn from(n: f64) -> Self {
569 Expr::Literal(Literal::Number(n))
570 }
571}
572
573impl From<bool> for Expr<'_> {
574 fn from(b: bool) -> Self {
575 Expr::Literal(Literal::Boolean(b))
576 }
577}
578
579#[derive(Debug, Clone, PartialEq)]
584pub enum Evaluation {
585 String(String),
587 Number(f64),
589 Boolean(bool),
591 Null,
593 Array(Vec<Evaluation>),
595 Object(std::collections::HashMap<String, Evaluation>),
597}
598
599impl TryFrom<serde_json::Value> for Evaluation {
600 type Error = ();
601
602 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
603 match value {
604 serde_json::Value::Null => Ok(Evaluation::Null),
605 serde_json::Value::Bool(b) => Ok(Evaluation::Boolean(b)),
606 serde_json::Value::Number(n) => {
607 if let Some(f) = n.as_f64() {
608 Ok(Evaluation::Number(f))
609 } else {
610 Err(())
611 }
612 }
613 serde_json::Value::String(s) => Ok(Evaluation::String(s)),
614 serde_json::Value::Array(arr) => {
615 let elements = arr
616 .into_iter()
617 .map(|elem| elem.try_into())
618 .collect::<Result<_, _>>()?;
619 Ok(Evaluation::Array(elements))
620 }
621 serde_json::Value::Object(obj) => {
622 let mut map = std::collections::HashMap::new();
623 for (key, value) in obj {
624 map.insert(key, value.try_into()?);
625 }
626 Ok(Evaluation::Object(map))
627 }
628 }
629 }
630}
631
632impl TryInto<serde_json::Value> for Evaluation {
633 type Error = ();
634
635 fn try_into(self) -> Result<serde_json::Value, Self::Error> {
636 match self {
637 Evaluation::Null => Ok(serde_json::Value::Null),
638 Evaluation::Boolean(b) => Ok(serde_json::Value::Bool(b)),
639 Evaluation::Number(n) => {
640 if n.fract() == 0.0 {
644 Ok(serde_json::Value::Number(serde_json::Number::from(
645 n as i64,
646 )))
647 } else if let Some(num) = serde_json::Number::from_f64(n) {
648 Ok(serde_json::Value::Number(num))
649 } else {
650 Err(())
651 }
652 }
653 Evaluation::String(s) => Ok(serde_json::Value::String(s)),
654 Evaluation::Array(arr) => {
655 let elements = arr
656 .into_iter()
657 .map(|elem| elem.try_into())
658 .collect::<Result<_, _>>()?;
659 Ok(serde_json::Value::Array(elements))
660 }
661 Evaluation::Object(obj) => {
662 let mut map = serde_json::Map::new();
663 for (key, value) in obj {
664 map.insert(key, value.try_into()?);
665 }
666 Ok(serde_json::Value::Object(map))
667 }
668 }
669 }
670}
671
672impl Evaluation {
673 pub fn as_boolean(&self) -> bool {
681 match self {
682 Evaluation::Boolean(b) => *b,
683 Evaluation::Null => false,
684 Evaluation::Number(n) => *n != 0.0,
685 Evaluation::String(s) => !s.is_empty(),
686 Evaluation::Array(_) | Evaluation::Object(_) => true,
688 }
689 }
690
691 pub fn as_number(&self) -> f64 {
695 match self {
696 Evaluation::String(s) => {
697 if s.is_empty() {
698 0.0
699 } else {
700 s.parse::<f64>().unwrap_or(f64::NAN)
701 }
702 }
703 Evaluation::Number(n) => *n,
704 Evaluation::Boolean(b) => {
705 if *b {
706 1.0
707 } else {
708 0.0
709 }
710 }
711 Evaluation::Null => 0.0,
712 Evaluation::Array(_) | Evaluation::Object(_) => f64::NAN,
713 }
714 }
715
716 pub fn sema(&self) -> EvaluationSema<'_> {
719 EvaluationSema(self)
720 }
721}
722
723pub struct EvaluationSema<'a>(&'a Evaluation);
726
727impl PartialEq for EvaluationSema<'_> {
728 fn eq(&self, other: &Self) -> bool {
729 match (self.0, other.0) {
730 (Evaluation::Null, Evaluation::Null) => true,
731 (Evaluation::Boolean(a), Evaluation::Boolean(b)) => a == b,
732 (Evaluation::Number(a), Evaluation::Number(b)) => a == b,
733 (Evaluation::String(a), Evaluation::String(b)) => a == b,
734
735 (a, b) => a.as_number() == b.as_number(),
737 }
738 }
739}
740
741impl PartialOrd for EvaluationSema<'_> {
742 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
743 match (self.0, other.0) {
744 (Evaluation::Null, Evaluation::Null) => Some(std::cmp::Ordering::Equal),
745 (Evaluation::Boolean(a), Evaluation::Boolean(b)) => a.partial_cmp(b),
746 (Evaluation::Number(a), Evaluation::Number(b)) => a.partial_cmp(b),
747 (Evaluation::String(a), Evaluation::String(b)) => a.partial_cmp(b),
748 (a, b) => a.as_number().partial_cmp(&b.as_number()),
750 }
751 }
752}
753
754impl std::fmt::Display for EvaluationSema<'_> {
755 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
756 match self.0 {
757 Evaluation::String(s) => write!(f, "{}", s),
758 Evaluation::Number(n) => {
759 if n.fract() == 0.0 {
761 write!(f, "{}", *n as i64)
762 } else {
763 write!(f, "{}", n)
764 }
765 }
766 Evaluation::Boolean(b) => write!(f, "{}", b),
767 Evaluation::Null => write!(f, ""),
768 Evaluation::Array(_) => write!(f, "Array"),
769 Evaluation::Object(_) => write!(f, "Object"),
770 }
771 }
772}
773
774impl<'src> Expr<'src> {
775 pub fn consteval(&self) -> Option<Evaluation> {
798 match self {
799 Expr::Literal(literal) => Some(literal.consteval()),
800
801 Expr::BinOp { lhs, op, rhs } => {
802 let lhs_val = lhs.consteval()?;
803 let rhs_val = rhs.consteval()?;
804
805 match op {
806 BinOp::And => {
807 if lhs_val.as_boolean() {
809 Some(rhs_val)
810 } else {
811 Some(lhs_val)
812 }
813 }
814 BinOp::Or => {
815 if lhs_val.as_boolean() {
817 Some(lhs_val)
818 } else {
819 Some(rhs_val)
820 }
821 }
822 BinOp::Eq => Some(Evaluation::Boolean(lhs_val.sema() == rhs_val.sema())),
823 BinOp::Neq => Some(Evaluation::Boolean(lhs_val.sema() != rhs_val.sema())),
824 BinOp::Lt => Some(Evaluation::Boolean(lhs_val.sema() < rhs_val.sema())),
825 BinOp::Le => Some(Evaluation::Boolean(lhs_val.sema() <= rhs_val.sema())),
826 BinOp::Gt => Some(Evaluation::Boolean(lhs_val.sema() > rhs_val.sema())),
827 BinOp::Ge => Some(Evaluation::Boolean(lhs_val.sema() >= rhs_val.sema())),
828 }
829 }
830
831 Expr::UnOp { op, expr } => {
832 let val = expr.consteval()?;
833 match op {
834 UnOp::Not => Some(Evaluation::Boolean(!val.as_boolean())),
835 }
836 }
837
838 Expr::Call(call) => call.consteval(),
839
840 _ => None,
842 }
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use std::borrow::Cow;
849
850 use anyhow::Result;
851 use pest::Parser as _;
852 use pretty_assertions::assert_eq;
853
854 use crate::{Call, Literal, Origin, SpannedExpr};
855
856 use super::{BinOp, Expr, ExprParser, Function, Rule, UnOp};
857
858 #[test]
859 fn test_literal_string_borrows() {
860 let cases = &[
861 ("'foo'", true),
862 ("'foo bar'", true),
863 ("'foo '' bar'", false),
864 ("'foo''bar'", false),
865 ("'foo''''bar'", false),
866 ];
867
868 for (expr, borrows) in cases {
869 let Expr::Literal(Literal::String(s)) = &*Expr::parse(expr).unwrap() else {
870 panic!("expected a literal string expression for {expr}");
871 };
872
873 assert!(matches!(
874 (s, borrows),
875 (Cow::Borrowed(_), true) | (Cow::Owned(_), false)
876 ));
877 }
878 }
879
880 #[test]
881 fn test_literal_as_str() {
882 let cases = &[
883 ("'foo'", "foo"),
884 ("'foo '' bar'", "foo ' bar"),
885 ("123", "123"),
886 ("123.000", "123"),
887 ("0.0", "0"),
888 ("0.1", "0.1"),
889 ("0.12345", "0.12345"),
890 ("true", "true"),
891 ("false", "false"),
892 ("null", "null"),
893 ];
894
895 for (expr, expected) in cases {
896 let Expr::Literal(expr) = &*Expr::parse(expr).unwrap() else {
897 panic!("expected a literal expression for {expr}");
898 };
899
900 assert_eq!(expr.as_str(), *expected);
901 }
902 }
903
904 #[test]
905 fn test_function_eq() {
906 let func = Function("foo");
907 assert_eq!(&func, "foo");
908 assert_eq!(&func, "FOO");
909 assert_eq!(&func, "Foo");
910
911 assert_eq!(func, Function("FOO"));
912 }
913
914 #[test]
915 fn test_parse_string_rule() {
916 let cases = &[
917 ("''", ""),
918 ("' '", " "),
919 ("''''", "''"),
920 ("'test'", "test"),
921 ("'spaces are ok'", "spaces are ok"),
922 ("'escaping '' works'", "escaping '' works"),
923 ];
924
925 for (case, expected) in cases {
926 let s = ExprParser::parse(Rule::string, case)
927 .unwrap()
928 .next()
929 .unwrap();
930
931 assert_eq!(s.into_inner().next().unwrap().as_str(), *expected);
932 }
933 }
934
935 #[test]
936 fn test_parse_context_rule() {
937 let cases = &[
938 "foo.bar",
939 "github.action_path",
940 "inputs.foo-bar",
941 "inputs.also--valid",
942 "inputs.this__too",
943 "inputs.this__too",
944 "secrets.GH_TOKEN",
945 "foo.*.bar",
946 "github.event.issue.labels.*.name",
947 ];
948
949 for case in cases {
950 assert_eq!(
951 ExprParser::parse(Rule::context, case)
952 .unwrap()
953 .next()
954 .unwrap()
955 .as_str(),
956 *case
957 );
958 }
959 }
960
961 #[test]
962 fn test_parse_call_rule() {
963 let cases = &[
964 "foo()",
965 "foo(bar)",
966 "foo(bar())",
967 "foo(1.23)",
968 "foo(1,2)",
969 "foo(1, 2)",
970 "foo(1, 2, secret.GH_TOKEN)",
971 "foo( )",
972 "fromJSON(inputs.free-threading)",
973 ];
974
975 for case in cases {
976 assert_eq!(
977 ExprParser::parse(Rule::function_call, case)
978 .unwrap()
979 .next()
980 .unwrap()
981 .as_str(),
982 *case
983 );
984 }
985 }
986
987 #[test]
988 fn test_parse_expr_rule() -> Result<()> {
989 let multiline = "github.repository_owner == 'Homebrew' &&
991 ((github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
992 (github.event_name == 'pull_request_target' &&
993 (github.event.action == 'ready_for_review' || github.event.label.name == 'automerge-skip')))";
994
995 let multiline2 = "foo.bar.baz[
996 0
997 ]";
998
999 let cases = &[
1000 "true",
1001 "fromJSON(inputs.free-threading) && '--disable-gil' || ''",
1002 "foo || bar || baz",
1003 "foo || bar && baz || foo && 1 && 2 && 3 || 4",
1004 "(github.actor != 'github-actions[bot]' && github.actor) || 'BrewTestBot'",
1005 "(true || false) == true",
1006 "!(!true || false)",
1007 "!(!true || false) == true",
1008 "(true == false) == true",
1009 "(true == (false || true && (true || false))) == true",
1010 "(github.actor != 'github-actions[bot]' && github.actor) == 'BrewTestBot'",
1011 "foo()[0]",
1012 "fromJson(steps.runs.outputs.data).workflow_runs[0].id",
1013 multiline,
1014 "'a' == 'b' && 'c' || 'd'",
1015 "github.event['a']",
1016 "github.event['a' == 'b']",
1017 "github.event['a' == 'b' && 'c' || 'd']",
1018 "github['event']['inputs']['dry-run']",
1019 "github[format('{0}', 'event')]",
1020 "github['event']['inputs'][github.event.inputs.magic]",
1021 "github['event']['inputs'].*",
1022 "1 == 1",
1023 "1 > 1",
1024 "1 >= 1",
1025 "matrix.node_version >= 20",
1026 "true||false",
1027 multiline2,
1028 "fromJSON( github.event.inputs.hmm ) [ 0 ]",
1029 ];
1030
1031 for case in cases {
1032 assert_eq!(
1033 ExprParser::parse(Rule::expression, case)?
1034 .next()
1035 .unwrap()
1036 .as_str(),
1037 *case
1038 );
1039 }
1040
1041 Ok(())
1042 }
1043
1044 #[test]
1045 fn test_parse() {
1046 let cases = &[
1047 (
1048 "!true || false || true",
1049 SpannedExpr::new(
1050 Origin::new(0..22, "!true || false || true"),
1051 Expr::BinOp {
1052 lhs: SpannedExpr::new(
1053 Origin::new(0..22, "!true || false || true"),
1054 Expr::BinOp {
1055 lhs: SpannedExpr::new(
1056 Origin::new(0..5, "!true"),
1057 Expr::UnOp {
1058 op: UnOp::Not,
1059 expr: SpannedExpr::new(
1060 Origin::new(1..5, "true"),
1061 true.into(),
1062 )
1063 .into(),
1064 },
1065 )
1066 .into(),
1067 op: BinOp::Or,
1068 rhs: SpannedExpr::new(Origin::new(9..14, "false"), false.into())
1069 .into(),
1070 },
1071 )
1072 .into(),
1073 op: BinOp::Or,
1074 rhs: SpannedExpr::new(Origin::new(18..22, "true"), true.into()).into(),
1075 },
1076 ),
1077 ),
1078 (
1079 "'foo '' bar'",
1080 SpannedExpr::new(
1081 Origin::new(0..12, "'foo '' bar'"),
1082 Expr::Literal(Literal::String("foo ' bar".into())),
1083 ),
1084 ),
1085 (
1086 "('foo '' bar')",
1087 SpannedExpr::new(
1088 Origin::new(1..13, "'foo '' bar'"),
1089 Expr::Literal(Literal::String("foo ' bar".into())),
1090 ),
1091 ),
1092 (
1093 "((('foo '' bar')))",
1094 SpannedExpr::new(
1095 Origin::new(3..15, "'foo '' bar'"),
1096 Expr::Literal(Literal::String("foo ' bar".into())),
1097 ),
1098 ),
1099 (
1100 "foo(1, 2, 3)",
1101 SpannedExpr::new(
1102 Origin::new(0..12, "foo(1, 2, 3)"),
1103 Expr::Call(Call {
1104 func: Function("foo"),
1105 args: vec![
1106 SpannedExpr::new(Origin::new(4..5, "1"), 1.0.into()),
1107 SpannedExpr::new(Origin::new(7..8, "2"), 2.0.into()),
1108 SpannedExpr::new(Origin::new(10..11, "3"), 3.0.into()),
1109 ],
1110 }),
1111 ),
1112 ),
1113 (
1114 "foo.bar.baz",
1115 SpannedExpr::new(
1116 Origin::new(0..11, "foo.bar.baz"),
1117 Expr::context(vec![
1118 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1119 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1120 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1121 ]),
1122 ),
1123 ),
1124 (
1125 "foo.bar.baz[1][2]",
1126 SpannedExpr::new(
1127 Origin::new(0..17, "foo.bar.baz[1][2]"),
1128 Expr::context(vec![
1129 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1130 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1131 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1132 SpannedExpr::new(
1133 Origin::new(11..14, "[1]"),
1134 Expr::Index(Box::new(SpannedExpr::new(
1135 Origin::new(12..13, "1"),
1136 1.0.into(),
1137 ))),
1138 ),
1139 SpannedExpr::new(
1140 Origin::new(14..17, "[2]"),
1141 Expr::Index(Box::new(SpannedExpr::new(
1142 Origin::new(15..16, "2"),
1143 2.0.into(),
1144 ))),
1145 ),
1146 ]),
1147 ),
1148 ),
1149 (
1150 "foo.bar.baz[*]",
1151 SpannedExpr::new(
1152 Origin::new(0..14, "foo.bar.baz[*]"),
1153 Expr::context([
1154 SpannedExpr::new(Origin::new(0..3, "foo"), Expr::ident("foo")),
1155 SpannedExpr::new(Origin::new(4..7, "bar"), Expr::ident("bar")),
1156 SpannedExpr::new(Origin::new(8..11, "baz"), Expr::ident("baz")),
1157 SpannedExpr::new(
1158 Origin::new(11..14, "[*]"),
1159 Expr::Index(Box::new(SpannedExpr::new(
1160 Origin::new(12..13, "*"),
1161 Expr::Star,
1162 ))),
1163 ),
1164 ]),
1165 ),
1166 ),
1167 (
1168 "vegetables.*.ediblePortions",
1169 SpannedExpr::new(
1170 Origin::new(0..27, "vegetables.*.ediblePortions"),
1171 Expr::context(vec![
1172 SpannedExpr::new(
1173 Origin::new(0..10, "vegetables"),
1174 Expr::ident("vegetables"),
1175 ),
1176 SpannedExpr::new(Origin::new(11..12, "*"), Expr::Star),
1177 SpannedExpr::new(
1178 Origin::new(13..27, "ediblePortions"),
1179 Expr::ident("ediblePortions"),
1180 ),
1181 ]),
1182 ),
1183 ),
1184 (
1185 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1188 SpannedExpr::new(
1189 Origin::new(
1190 0..88,
1191 "github.ref == 'refs/heads/main' && 'value_for_main_branch' || 'value_for_other_branches'",
1192 ),
1193 Expr::BinOp {
1194 lhs: Box::new(SpannedExpr::new(
1195 Origin::new(
1196 0..59,
1197 "github.ref == 'refs/heads/main' && 'value_for_main_branch'",
1198 ),
1199 Expr::BinOp {
1200 lhs: Box::new(SpannedExpr::new(
1201 Origin::new(0..32, "github.ref == 'refs/heads/main'"),
1202 Expr::BinOp {
1203 lhs: Box::new(SpannedExpr::new(
1204 Origin::new(0..10, "github.ref"),
1205 Expr::context(vec![
1206 SpannedExpr::new(
1207 Origin::new(0..6, "github"),
1208 Expr::ident("github"),
1209 ),
1210 SpannedExpr::new(
1211 Origin::new(7..10, "ref"),
1212 Expr::ident("ref"),
1213 ),
1214 ]),
1215 )),
1216 op: BinOp::Eq,
1217 rhs: Box::new(SpannedExpr::new(
1218 Origin::new(14..31, "'refs/heads/main'"),
1219 Expr::Literal(Literal::String(
1220 "refs/heads/main".into(),
1221 )),
1222 )),
1223 },
1224 )),
1225 op: BinOp::And,
1226 rhs: Box::new(SpannedExpr::new(
1227 Origin::new(35..58, "'value_for_main_branch'"),
1228 Expr::Literal(Literal::String("value_for_main_branch".into())),
1229 )),
1230 },
1231 )),
1232 op: BinOp::Or,
1233 rhs: Box::new(SpannedExpr::new(
1234 Origin::new(62..88, "'value_for_other_branches'"),
1235 Expr::Literal(Literal::String("value_for_other_branches".into())),
1236 )),
1237 },
1238 ),
1239 ),
1240 (
1241 "(true || false) == true",
1242 SpannedExpr::new(
1243 Origin::new(0..23, "(true || false) == true"),
1244 Expr::BinOp {
1245 lhs: Box::new(SpannedExpr::new(
1246 Origin::new(1..14, "true || false"),
1247 Expr::BinOp {
1248 lhs: Box::new(SpannedExpr::new(
1249 Origin::new(1..5, "true"),
1250 true.into(),
1251 )),
1252 op: BinOp::Or,
1253 rhs: Box::new(SpannedExpr::new(
1254 Origin::new(9..14, "false"),
1255 false.into(),
1256 )),
1257 },
1258 )),
1259 op: BinOp::Eq,
1260 rhs: Box::new(SpannedExpr::new(Origin::new(19..23, "true"), true.into())),
1261 },
1262 ),
1263 ),
1264 (
1265 "!(!true || false)",
1266 SpannedExpr::new(
1267 Origin::new(0..17, "!(!true || false)"),
1268 Expr::UnOp {
1269 op: UnOp::Not,
1270 expr: Box::new(SpannedExpr::new(
1271 Origin::new(2..16, "!true || false"),
1272 Expr::BinOp {
1273 lhs: Box::new(SpannedExpr::new(
1274 Origin::new(2..7, "!true"),
1275 Expr::UnOp {
1276 op: UnOp::Not,
1277 expr: Box::new(SpannedExpr::new(
1278 Origin::new(3..7, "true"),
1279 true.into(),
1280 )),
1281 },
1282 )),
1283 op: BinOp::Or,
1284 rhs: Box::new(SpannedExpr::new(
1285 Origin::new(11..16, "false"),
1286 false.into(),
1287 )),
1288 },
1289 )),
1290 },
1291 ),
1292 ),
1293 (
1294 "foobar[format('{0}', 'event')]",
1295 SpannedExpr::new(
1296 Origin::new(0..30, "foobar[format('{0}', 'event')]"),
1297 Expr::context([
1298 SpannedExpr::new(Origin::new(0..6, "foobar"), Expr::ident("foobar")),
1299 SpannedExpr::new(
1300 Origin::new(6..30, "[format('{0}', 'event')]"),
1301 Expr::Index(Box::new(SpannedExpr::new(
1302 Origin::new(7..29, "format('{0}', 'event')"),
1303 Expr::Call(Call {
1304 func: Function("format"),
1305 args: vec![
1306 SpannedExpr::new(
1307 Origin::new(14..19, "'{0}'"),
1308 Expr::from("{0}"),
1309 ),
1310 SpannedExpr::new(
1311 Origin::new(21..28, "'event'"),
1312 Expr::from("event"),
1313 ),
1314 ],
1315 }),
1316 ))),
1317 ),
1318 ]),
1319 ),
1320 ),
1321 (
1322 "github.actor_id == '49699333'",
1323 SpannedExpr::new(
1324 Origin::new(0..29, "github.actor_id == '49699333'"),
1325 Expr::BinOp {
1326 lhs: SpannedExpr::new(
1327 Origin::new(0..15, "github.actor_id"),
1328 Expr::context(vec![
1329 SpannedExpr::new(
1330 Origin::new(0..6, "github"),
1331 Expr::ident("github"),
1332 ),
1333 SpannedExpr::new(
1334 Origin::new(7..15, "actor_id"),
1335 Expr::ident("actor_id"),
1336 ),
1337 ]),
1338 )
1339 .into(),
1340 op: BinOp::Eq,
1341 rhs: Box::new(SpannedExpr::new(
1342 Origin::new(19..29, "'49699333'"),
1343 Expr::from("49699333"),
1344 )),
1345 },
1346 ),
1347 ),
1348 ];
1349
1350 for (case, expr) in cases {
1351 assert_eq!(*expr, Expr::parse(case).unwrap());
1352 }
1353 }
1354
1355 #[test]
1356 fn test_expr_constant_reducible() -> Result<()> {
1357 for (expr, reducible) in &[
1358 ("'foo'", true),
1359 ("1", true),
1360 ("true", true),
1361 ("null", true),
1362 ("!true", true),
1365 ("!null", true),
1366 ("true && false", true),
1367 ("true || false", true),
1368 ("null && !null && true", true),
1369 ("format('{0} {1}', 'foo', 'bar')", true),
1372 ("format('{0} {1}', 1, 2)", true),
1373 ("format('{0} {1}', 1, '2')", true),
1374 ("contains('foo', 'bar')", true),
1375 ("startsWith('foo', 'bar')", true),
1376 ("endsWith('foo', 'bar')", true),
1377 ("startsWith(some.context, 'bar')", false),
1378 ("endsWith(some.context, 'bar')", false),
1379 ("format('{0} {1}', '1', format('{0}', null))", true),
1381 ("format('{0} {1}', '1', startsWith('foo', 'foo'))", true),
1382 ("format('{0} {1}', '1', startsWith(foo.bar, 'foo'))", false),
1383 ("foo", false),
1384 ("foo.bar", false),
1385 ("foo.bar[1]", false),
1386 ("foo.bar == 'bar'", false),
1387 ("foo.bar || bar || baz", false),
1388 ("foo.bar && bar && baz", false),
1389 ] {
1390 let expr = Expr::parse(expr)?;
1391 assert_eq!(expr.constant_reducible(), *reducible);
1392 }
1393
1394 Ok(())
1395 }
1396
1397 #[test]
1398 fn test_evaluate_constant_complex_expressions() -> Result<()> {
1399 use crate::Evaluation;
1400
1401 let test_cases = &[
1402 ("!false", Evaluation::Boolean(true)),
1404 ("!true", Evaluation::Boolean(false)),
1405 ("!(true && false)", Evaluation::Boolean(true)),
1406 ("true && (false || true)", Evaluation::Boolean(true)),
1408 ("false || (true && false)", Evaluation::Boolean(false)),
1409 (
1411 "contains(format('{0} {1}', 'hello', 'world'), 'world')",
1412 Evaluation::Boolean(true),
1413 ),
1414 (
1415 "startsWith(format('prefix_{0}', 'test'), 'prefix')",
1416 Evaluation::Boolean(true),
1417 ),
1418 ];
1419
1420 for (expr_str, expected) in test_cases {
1421 let expr = Expr::parse(expr_str)?;
1422 let result = expr.consteval().unwrap();
1423 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1424 }
1425
1426 Ok(())
1427 }
1428
1429 #[test]
1430 fn test_evaluation_sema_display() {
1431 use crate::Evaluation;
1432
1433 let test_cases = &[
1434 (Evaluation::String("hello".to_string()), "hello"),
1435 (Evaluation::Number(42.0), "42"),
1436 (Evaluation::Number(3.14), "3.14"),
1437 (Evaluation::Boolean(true), "true"),
1438 (Evaluation::Boolean(false), "false"),
1439 (Evaluation::Null, ""),
1440 ];
1441
1442 for (result, expected) in test_cases {
1443 assert_eq!(result.sema().to_string(), *expected);
1444 }
1445 }
1446
1447 #[test]
1448 fn test_evaluation_result_to_boolean() {
1449 use crate::Evaluation;
1450
1451 let test_cases = &[
1452 (Evaluation::Boolean(true), true),
1453 (Evaluation::Boolean(false), false),
1454 (Evaluation::Null, false),
1455 (Evaluation::Number(0.0), false),
1456 (Evaluation::Number(1.0), true),
1457 (Evaluation::Number(-1.0), true),
1458 (Evaluation::String("".to_string()), false),
1459 (Evaluation::String("hello".to_string()), true),
1460 (Evaluation::Array(vec![]), true), (Evaluation::Object(std::collections::HashMap::new()), true), ];
1463
1464 for (result, expected) in test_cases {
1465 assert_eq!(result.as_boolean(), *expected);
1466 }
1467 }
1468
1469 #[test]
1470 fn test_github_actions_logical_semantics() -> Result<()> {
1471 use crate::Evaluation;
1472
1473 let test_cases = &[
1475 ("false && 'hello'", Evaluation::Boolean(false)),
1477 ("null && 'hello'", Evaluation::Null),
1478 ("'' && 'hello'", Evaluation::String("".to_string())),
1479 (
1480 "'hello' && 'world'",
1481 Evaluation::String("world".to_string()),
1482 ),
1483 ("true && 42", Evaluation::Number(42.0)),
1484 ("true || 'hello'", Evaluation::Boolean(true)),
1486 (
1487 "'hello' || 'world'",
1488 Evaluation::String("hello".to_string()),
1489 ),
1490 ("false || 'hello'", Evaluation::String("hello".to_string())),
1491 ("null || false", Evaluation::Boolean(false)),
1492 ("'' || null", Evaluation::Null),
1493 ];
1494
1495 for (expr_str, expected) in test_cases {
1496 let expr = Expr::parse(expr_str)?;
1497 let result = expr.consteval().unwrap();
1498 assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
1499 }
1500
1501 Ok(())
1502 }
1503
1504 #[test]
1505 fn test_expr_has_constant_reducible_subexpr() -> Result<()> {
1506 for (expr, reducible) in &[
1507 ("'foo'", false),
1509 ("1", false),
1510 ("true", false),
1511 ("null", false),
1512 (
1514 "format('{0}, {1}', github.event.number, format('{0}', 'abc'))",
1515 true,
1516 ),
1517 ("foobar[format('{0}', 'event')]", true),
1518 ] {
1519 let expr = Expr::parse(expr)?;
1520 assert_eq!(!expr.constant_reducible_subexprs().is_empty(), *reducible);
1521 }
1522 Ok(())
1523 }
1524
1525 #[test]
1526 fn test_expr_dataflow_contexts() -> Result<()> {
1527 let expr = Expr::parse("foo.bar")?;
1529 assert_eq!(
1530 expr.dataflow_contexts()
1531 .iter()
1532 .map(|t| t.1.raw)
1533 .collect::<Vec<_>>(),
1534 ["foo.bar"]
1535 );
1536
1537 let expr = Expr::parse("foo.bar[1]")?;
1538 assert_eq!(
1539 expr.dataflow_contexts()
1540 .iter()
1541 .map(|t| t.1.raw)
1542 .collect::<Vec<_>>(),
1543 ["foo.bar[1]"]
1544 );
1545
1546 let expr = Expr::parse("foo.bar == 'bar'")?;
1548 assert!(expr.dataflow_contexts().is_empty());
1549
1550 let expr = Expr::parse("foo.bar || abc || d.e.f")?;
1552 assert_eq!(
1553 expr.dataflow_contexts()
1554 .iter()
1555 .map(|t| t.1.raw)
1556 .collect::<Vec<_>>(),
1557 ["foo.bar", "abc", "d.e.f"]
1558 );
1559
1560 let expr = Expr::parse("foo.bar && abc && d.e.f")?;
1562 assert_eq!(
1563 expr.dataflow_contexts()
1564 .iter()
1565 .map(|t| t.1.raw)
1566 .collect::<Vec<_>>(),
1567 ["d.e.f"]
1568 );
1569
1570 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || 'false'")?;
1571 assert_eq!(
1572 expr.dataflow_contexts()
1573 .iter()
1574 .map(|t| t.1.raw)
1575 .collect::<Vec<_>>(),
1576 ["foo.bar"]
1577 );
1578
1579 let expr = Expr::parse("foo.bar == 'bar' && foo.bar || foo.baz")?;
1580 assert_eq!(
1581 expr.dataflow_contexts()
1582 .iter()
1583 .map(|t| t.1.raw)
1584 .collect::<Vec<_>>(),
1585 ["foo.bar", "foo.baz"]
1586 );
1587
1588 let expr = Expr::parse("fromJson(steps.runs.outputs.data).workflow_runs[0].id")?;
1589 assert_eq!(
1590 expr.dataflow_contexts()
1591 .iter()
1592 .map(|t| t.1.raw)
1593 .collect::<Vec<_>>(),
1594 ["fromJson(steps.runs.outputs.data).workflow_runs[0].id"]
1595 );
1596
1597 let expr = Expr::parse("format('{0} {1} {2}', foo.bar, tojson(github), toJSON(github))")?;
1598 assert_eq!(
1599 expr.dataflow_contexts()
1600 .iter()
1601 .map(|t| t.1.raw)
1602 .collect::<Vec<_>>(),
1603 ["foo.bar", "github", "github"]
1604 );
1605
1606 Ok(())
1607 }
1608
1609 #[test]
1610 fn test_spannedexpr_computed_indices() -> Result<()> {
1611 for (expr, computed_indices) in &[
1612 ("foo.bar", vec![]),
1613 ("foo.bar[1]", vec![]),
1614 ("foo.bar[*]", vec![]),
1615 ("foo.bar[abc]", vec!["[abc]"]),
1616 (
1617 "foo.bar[format('{0}', 'foo')]",
1618 vec!["[format('{0}', 'foo')]"],
1619 ),
1620 ("foo.bar[abc].def[efg]", vec!["[abc]", "[efg]"]),
1621 ] {
1622 let expr = Expr::parse(expr)?;
1623
1624 assert_eq!(
1625 expr.computed_indices()
1626 .iter()
1627 .map(|e| e.origin.raw)
1628 .collect::<Vec<_>>(),
1629 *computed_indices
1630 );
1631 }
1632
1633 Ok(())
1634 }
1635
1636 #[test]
1637 fn test_fragment_from_expr() {
1638 for (expr, expected) in &[
1639 ("foo==bar", "foo==bar"),
1640 ("foo == bar", "foo == bar"),
1641 ("foo == bar", r"foo == bar"),
1642 ("foo(bar)", "foo(bar)"),
1643 ("foo(bar, baz)", "foo(bar, baz)"),
1644 ("foo (bar, baz)", "foo (bar, baz)"),
1645 ("a . b . c . d", "a . b . c . d"),
1646 ("true \n && \n false", r"true\s+\&\&\s+false"),
1647 ] {
1648 let expr = Expr::parse(expr).unwrap();
1649 match subfeature::Fragment::from(&expr) {
1650 subfeature::Fragment::Raw(actual) => assert_eq!(actual, *expected),
1651 subfeature::Fragment::Regex(actual) => assert_eq!(actual.as_str(), *expected),
1652 };
1653 }
1654 }
1655}