github-actions-expressions 1.24.1

GitHub Actions expression parser and data types
Documentation
//! Parser rules for Actions expressions.
//!
//! See: <https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions>

/// Whitespace handling
WHITESPACE = _{ " " | NEWLINE }

// Misc notes:
// I have pretty low confidence in this grammar -- it should parse the
// overwhelming majority of normal looking Actions expressions, but
// it makes up for a lack of left-recursion with some hacky rules that
// probably aren't sufficient for capturing all possible grammar productions.
// I've noted some of these with `HACK` below.
// I've also noted some potentials TODOs with `TODO`.

expression = { SOI ~ or_expr ~ EOI }

/// Logical OR
or_expr = { (and_expr ~ ("||" ~ and_expr)*) }

/// Logical AND
and_expr = { (eq_expr ~ ("&&" ~ eq_expr)*) }

/// Structural equality/inequality
eq_expr = { (comp_expr ~ (eq_op ~ comp_expr)*) }
eq_op   = { "==" | "!=" }

/// Structural comparison
comp_expr = {
    unary_expr ~ (comp_op ~ unary_expr)*
}
comp_op   = { ">=" | ">" | "<=" | "<" }

/// Unary operations, including the base case for expressions.
// HACK: `unary_op ~ or_expr` ensures that we handle non-trivial
// negation productions, like `!(true || false)`.
unary_expr = { (unary_op? ~ primary_expr) | unary_op ~ or_expr }
unary_op   = { "!" }

primary_expr = {
    number
  | string
  | boolean
  | null
  | context
}

/// Numbers
/// Supports decimal (with optional sign and scientific notation), hex (0x), and octal (0o).
/// See: <https://github.com/actions/runner/blob/main/src/Sdk/DTExpressions2/Expressions2/Sdk/ExpressionUtility.cs>
number = @{
    "0x" ~ ASCII_HEX_DIGIT+
  | "0o" ~ ('0'..'7')+
  | "NaN"
  | ("+" | "-")? ~ "Infinity"
  | ("+" | "-")? ~ (ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT*)? | "." ~ ASCII_DIGIT+) ~ (("e" | "E") ~ ("+" | "-")? ~ ASCII_DIGIT+)?
}

/// Strings
string       = ${ "'" ~ string_inner ~ "'" }
string_inner = @{ string_char* }
string_char  = @{
    !("'") ~ ANY
  | "'" ~ "'"
}

/// Booleans
boolean = { "true" | "false" }

/// Null
null = { "null" }

/// Context references (e.g., github.event.issue.number)
context = { (function_call | identifier | "(" ~ or_expr ~ ")") ~ (("." ~ (identifier | star)) | index)* }

/// A star within a context component.
star = { "*" }

/// A single identifier within a context component.
identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* }

/// An index within a context component.
index = !{ ("[" ~ (or_expr | star) ~ "]") }

/// Function calls
function_call = !{ identifier ~ "(" ~ (or_expr ~ ("," ~ or_expr)*)? ~ ")" }