| 73 | 73 | | ]; |
| 74 | 74 | | |
| 75 | 75 | | /// Evaluates one expression (the text between `${{` and `}}`, or a bare `if:`). |
| 76 | + | /// The status functions exist only in `if:`; anywhere else they are unrecognized, as on GitHub. |
| 76 | 77 | | pub fn evaluate(expression: &str, scope: &Scope) -> Result<Value, String> { |
| 77 | | − | let ast = parse(expression, scope.contexts)?; |
| 78 | + | let ast = parse(expression, scope.contexts, false)?; |
| 78 | 79 | | Ok(eval(&ast, scope)?.into_value()) |
| 79 | 80 | | } |
| 80 | 81 | | |
| ⋯ |
| 87 | 88 | | // Text around or between expressions makes the whole thing a string, |
| 88 | 89 | | // as on GitHub: it is true whenever it interpolates to anything. |
| 89 | 90 | | None if has_expression(trimmed) => { |
| 90 | | − | let text = interpolate(trimmed, scope)?; |
| 91 | + | let text = interpolate_in(trimmed, scope, true)?; |
| 91 | 92 | | return Ok(success && !text.is_empty()); |
| 92 | 93 | | } |
| 93 | 94 | | None => trimmed, |
| ⋯ |
| 95 | 96 | | if source.trim().is_empty() { |
| 96 | 97 | | return Ok(success); |
| 97 | 98 | | } |
| 98 | | − | let ast = parse(source, scope.contexts)?; |
| 99 | + | let ast = parse(source, scope.contexts, true)?; |
| 99 | 100 | | if uses_status(&ast) { |
| 100 | 101 | | Ok(eval(&ast, scope)?.truthy()) |
| 101 | 102 | | } else { |
| ⋯ |
| 105 | 106 | | |
| 106 | 107 | | /// Replaces each `${{ expr }}` in text with the value converted to a string. Text without `${{` is returned unchanged. |
| 107 | 108 | | pub fn interpolate(text: &str, scope: &Scope) -> Result<String, String> { |
| 109 | + | interpolate_in(text, scope, false) |
| 110 | + | } |
| 111 | + | |
| 112 | + | /// `interpolate`, with the status functions allowed when `status` (inside an `if:`). |
| 113 | + | fn interpolate_in(text: &str, scope: &Scope, status: bool) -> Result<String, String> { |
| 108 | 114 | | if !has_expression(text) { |
| 109 | 115 | | return Ok(text.to_string()); |
| 110 | 116 | | } |
| ⋯ |
| 119 | 125 | | "The expression is not closed. An unescaped ${{{{ sequence was found, but the closing }}}} sequence was not found: {text}" |
| 120 | 126 | | ) |
| 121 | 127 | | })?; |
| 122 | | − | let value = evaluate(&text[body..close], scope)?; |
| 128 | + | let ast = parse(&text[body..close], scope.contexts, status)?; |
| 129 | + | let value = eval(&ast, scope)?.into_value(); |
| 123 | 130 | | out.push_str(&to_text(&value)); |
| 124 | 131 | | from = close + 2; |
| 125 | 132 | | } |
| ⋯ |
| 525 | 532 | | Call(Func, Vec<Expr>), |
| 526 | 533 | | } |
| 527 | 534 | | |
| 528 | | − | fn parse(src: &str, contexts: &Map<String, Value>) -> Result<Expr, String> { |
| 535 | + | /// `status`: whether success(), failure(), cancelled() and always() may be called. |
| 536 | + | fn parse(src: &str, contexts: &Map<String, Value>, status: bool) -> Result<Expr, String> { |
| 529 | 537 | | let toks = lex(src)?; |
| 530 | 538 | | if toks.is_empty() { |
| 531 | 539 | | return Err(format!("An expression was expected: '{src}'")); |
| ⋯ |
| 535 | 543 | | i: 0, |
| 536 | 544 | | src, |
| 537 | 545 | | contexts, |
| 546 | + | status, |
| 538 | 547 | | }; |
| 539 | 548 | | let expr = parser.or()?; |
| 540 | 549 | | if parser.i < parser.toks.len() { |
| ⋯ |
| 548 | 557 | | i: usize, |
| 549 | 558 | | src: &'s str, |
| 550 | 559 | | contexts: &'s Map<String, Value>, |
| 560 | + | status: bool, |
| 551 | 561 | | } |
| 552 | 562 | | |
| 553 | 563 | | impl Parser<'_> { |
| ⋯ |
| 711 | 721 | | let Some(&(canonical, func, min, max)) = FUNCTIONS |
| 712 | 722 | | .iter() |
| 713 | 723 | | .find(|(n, ..)| n.eq_ignore_ascii_case(name)) |
| 724 | + | .filter(|(_, func, ..)| self.status || !is_status(*func)) |
| 714 | 725 | | else { |
| 715 | 726 | | return Err(located( |
| 716 | 727 | | &format!("Unrecognized function: '{name}'"), |
| ⋯ |
| 749 | 760 | | } |
| 750 | 761 | | } |
| 751 | 762 | | |
| 763 | + | fn is_status(func: Func) -> bool { |
| 764 | + | matches!( |
| 765 | + | func, |
| 766 | + | Func::Success | Func::Failure | Func::Cancelled | Func::Always |
| 767 | + | ) |
| 768 | + | } |
| 769 | + | |
| 752 | 770 | | /// Whether the expression calls success(), failure(), cancelled() or always(). |
| 753 | 771 | | fn uses_status(expr: &Expr) -> bool { |
| 754 | 772 | | match expr { |
| ⋯ |
| 757 | 775 | | Expr::Index(a, b) | Expr::Compare(_, a, b) | Expr::And(a, b) | Expr::Or(a, b) => { |
| 758 | 776 | | uses_status(a) || uses_status(b) |
| 759 | 777 | | } |
| 760 | | − | Expr::Call(func, args) => { |
| 761 | | − | matches!( |
| 762 | | − | func, |
| 763 | | − | Func::Success | Func::Failure | Func::Cancelled | Func::Always |
| 764 | | − | ) || args.iter().any(uses_status) |
| 765 | | − | } |
| 778 | + | Expr::Call(func, args) => is_status(*func) || args.iter().any(uses_status), |
| 766 | 779 | | } |
| 767 | 780 | | } |
| 768 | 781 | | |
| ⋯ |
| 1520 | 1533 | | fn function_errors() { |
| 1521 | 1534 | | assert!(err("nope(1)").starts_with("Unrecognized function: 'nope'")); |
| 1522 | 1535 | | assert!(err("contains('a')").starts_with("Too few parameters supplied: 'contains'")); |
| 1523 | | − | assert!(err("success(1)").starts_with("Too many parameters supplied: 'success'")); |
| 1536 | + | assert!(cond(Status::Success, "success()")); |
| 1537 | + | let extra = with(Status::Success, |s| condition("success(1)", s)).unwrap_err(); |
| 1538 | + | assert!(extra.starts_with("Too many parameters supplied: 'success'")); |
| 1539 | + | assert!(err("toJSON()").starts_with("Too few parameters supplied: 'toJSON'")); |
| 1540 | + | } |
| 1541 | + | |
| 1542 | + | #[test] |
| 1543 | + | fn status_functions_only_in_conditions() { |
| 1544 | + | // GitHub: "Unrecognized function: 'success'" in env, with, run and the rest. |
| 1545 | + | for f in [ |
| 1546 | + | "success()", |
| 1547 | + | "failure()", |
| 1548 | + | "cancelled()", |
| 1549 | + | "always()", |
| 1550 | + | "!SUCCESS()", |
| 1551 | + | ] { |
| 1552 | + | assert!(err(f).contains("Unrecognized function:"), "{f}"); |
| 1553 | + | } |
| 1554 | + | let interpolated = with(Status::Success, |s| interpolate("x ${{ always() }}", s)); |
| 1555 | + | assert!(interpolated |
| 1556 | + | .unwrap_err() |
| 1557 | + | .starts_with("Unrecognized function: 'always'")); |
| 1558 | + | assert!(cond(Status::Success, "${{ success() }} and text")); |
| 1524 | 1559 | | assert!(err("toJSON()").starts_with("Too few parameters supplied: 'toJSON'")); |
| 1525 | 1560 | | } |
| 1526 | 1561 | | |