gha-expression-proof 1.0.0

GitHub Actions expression evaluator and receipt generator for offline CI compatibility testing
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Expr {
    Literal(Value),
    Variable(String),
    Unary {
        op: UnaryOp,
        expr: Box<Expr>,
    },
    Binary {
        op: BinaryOp,
        left: Box<Expr>,
        right: Box<Expr>,
    },
    Call {
        name: String,
        args: Vec<Expr>,
    },
    Access {
        base: Box<Expr>,
        segment: AccessSegment,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum UnaryOp {
    Not,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BinaryOp {
    Or,
    And,
    Eq,
    Ne,
    Lt,
    Le,
    Gt,
    Ge,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AccessSegment {
    Property(String),
    Index(Box<Expr>),
    Wildcard,
}

impl Expr {
    pub fn collect_functions(&self, out: &mut Vec<String>) {
        match self {
            Expr::Call { name, args } => {
                out.push(name.to_ascii_lowercase());
                for arg in args {
                    arg.collect_functions(out);
                }
            }
            Expr::Unary { expr, .. } => expr.collect_functions(out),
            Expr::Binary { left, right, .. } => {
                left.collect_functions(out);
                right.collect_functions(out);
            }
            Expr::Access { base, segment } => {
                base.collect_functions(out);
                if let AccessSegment::Index(index) = segment {
                    index.collect_functions(out);
                }
            }
            Expr::Literal(_) | Expr::Variable(_) => {}
        }
    }

    pub fn collect_roots(&self, out: &mut Vec<String>) {
        match self {
            Expr::Variable(name) => out.push(name.clone()),
            Expr::Call { args, .. } => {
                for arg in args {
                    arg.collect_roots(out);
                }
            }
            Expr::Unary { expr, .. } => expr.collect_roots(out),
            Expr::Binary { left, right, .. } => {
                left.collect_roots(out);
                right.collect_roots(out);
            }
            Expr::Access { base, segment } => {
                base.collect_roots(out);
                if let AccessSegment::Index(index) = segment {
                    index.collect_roots(out);
                }
            }
            Expr::Literal(_) => {}
        }
    }

    pub fn contains_status_function(&self) -> bool {
        match self {
            Expr::Call { name, args } => {
                matches!(
                    name.to_ascii_lowercase().as_str(),
                    "success" | "failure" | "cancelled" | "always"
                ) || args.iter().any(Expr::contains_status_function)
            }
            Expr::Unary { expr, .. } => expr.contains_status_function(),
            Expr::Binary { left, right, .. } => {
                left.contains_status_function() || right.contains_status_function()
            }
            Expr::Access { base, segment } => {
                base.contains_status_function()
                    || matches!(segment, AccessSegment::Index(index) if index.contains_status_function())
            }
            Expr::Literal(_) | Expr::Variable(_) => false,
        }
    }
}