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