refactor: move expr call APIs to a new module (#1143)

This commit is contained in:
William Woodruff 2025-09-11 21:34:07 -04:00 committed by GitHub
parent 5a4d4e5785
commit 4a92dfc412
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1266 additions and 1216 deletions

2
Cargo.lock generated
View file

@ -828,7 +828,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "github-actions-expressions"
version = "0.0.9"
version = "0.0.10"
dependencies = [
"anyhow",
"itertools",

View file

@ -19,7 +19,7 @@ rust-version = "1.88.0"
[workspace.dependencies]
anyhow = "1.0.99"
github-actions-expressions = { path = "crates/github-actions-expressions", version = "0.0.9" }
github-actions-expressions = { path = "crates/github-actions-expressions", version = "0.0.10" }
github-actions-models = { path = "crates/github-actions-models", version = "0.32.0" }
itertools = "0.14.0"
pest = "2.8.1"

View file

@ -2,7 +2,7 @@
name = "github-actions-expressions"
description = "GitHub Actions expression parser and data types"
repository = "https://github.com/zizmorcore/zizmor/tree/main/crates/github-actions-expressions"
version = "0.0.9"
version = "0.0.10"
readme = "README.md"
homepage.workspace = true

View file

@ -9,6 +9,12 @@
`github-actions-expressions` is a parser and library for GitHub Actions expressions.
Key features:
* Faithful parsing of GitHub Actions expressions.
* Span-aware AST nodes.
* Limited support for constant expression evaluation.
See the [documentation] for more details.
This library is part of [zizmor].

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
//! Identifiers.
/// Represents a single identifier in a GitHub Actions expression,
/// i.e. a single context component.
///
/// Identifiers are case-insensitive.
#[derive(Debug)]
pub struct Identifier<'src>(pub(crate) &'src str);
impl Identifier<'_> {
/// Returns the identifier as a string slice, as it appears in the
/// expression.
///
/// Important: identifiers are case-insensitive, so this should not
/// be used for comparisons.
pub fn as_str(&self) -> &str {
self.0
}
}
impl PartialEq for Identifier<'_> {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(other.0)
}
}
impl PartialEq<str> for Identifier<'_> {
fn eq(&self, other: &str) -> bool {
self.0.eq_ignore_ascii_case(other)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
//! Literal values.
use std::borrow::Cow;
use crate::Evaluation;
/// Represents a literal value in a GitHub Actions expression.
#[derive(Debug, PartialEq)]
pub enum Literal<'src> {
/// A number literal.
Number(f64),
/// A string literal.
String(Cow<'src, str>),
/// A boolean literal.
Boolean(bool),
/// The `null` literal.
Null,
}
impl<'src> Literal<'src> {
/// Returns a string representation of the literal.
///
/// This is not guaranteed to be an exact equivalent of the literal
/// as it appears in its source expression. For example, the string
/// representation of a floating point literal is subject to normalization,
/// and string literals are returned without surrounding quotes.
pub fn as_str(&self) -> Cow<'src, str> {
match self {
Literal::String(s) => s.clone(),
Literal::Number(n) => Cow::Owned(n.to_string()),
Literal::Boolean(b) => Cow::Owned(b.to_string()),
Literal::Null => Cow::Borrowed("null"),
}
}
/// Returns the trivial constant evaluation of the literal.
pub(crate) fn consteval(&self) -> Evaluation {
match self {
Literal::String(s) => Evaluation::String(s.to_string()),
Literal::Number(n) => Evaluation::Number(*n),
Literal::Boolean(b) => Evaluation::Boolean(*b),
Literal::Null => Evaluation::Null,
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::Expr;
#[test]
fn test_evaluate_constant_literals() -> Result<()> {
use crate::Evaluation;
let test_cases = &[
("'hello'", Evaluation::String("hello".to_string())),
("'world'", Evaluation::String("world".to_string())),
("42", Evaluation::Number(42.0)),
("3.14", Evaluation::Number(3.14)),
("true", Evaluation::Boolean(true)),
("false", Evaluation::Boolean(false)),
("null", Evaluation::Null),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
}

View file

@ -0,0 +1,80 @@
//! Unary and binary operators.
/// Binary operations allowed in an expression.
#[derive(Debug, PartialEq)]
pub enum BinOp {
/// `expr && expr`
And,
/// `expr || expr`
Or,
/// `expr == expr`
Eq,
/// `expr != expr`
Neq,
/// `expr > expr`
Gt,
/// `expr >= expr`
Ge,
/// `expr < expr`
Lt,
/// `expr <= expr`
Le,
}
/// Unary operations allowed in an expression.
#[derive(Debug, PartialEq)]
pub enum UnOp {
/// `!expr`
Not,
}
#[cfg(test)]
mod tests {
use crate::Expr;
use anyhow::Result;
#[test]
fn test_evaluate_constant_binary_operations() -> Result<()> {
use crate::Evaluation;
let test_cases = &[
// Boolean operations
("true && true", Evaluation::Boolean(true)),
("true && false", Evaluation::Boolean(false)),
("false && true", Evaluation::Boolean(false)),
("false && false", Evaluation::Boolean(false)),
("true || true", Evaluation::Boolean(true)),
("true || false", Evaluation::Boolean(true)),
("false || true", Evaluation::Boolean(true)),
("false || false", Evaluation::Boolean(false)),
// Equality operations
("1 == 1", Evaluation::Boolean(true)),
("1 == 2", Evaluation::Boolean(false)),
("'hello' == 'hello'", Evaluation::Boolean(true)),
("'hello' == 'world'", Evaluation::Boolean(false)),
("true == true", Evaluation::Boolean(true)),
("true == false", Evaluation::Boolean(false)),
("1 != 2", Evaluation::Boolean(true)),
("1 != 1", Evaluation::Boolean(false)),
// Comparison operations
("1 < 2", Evaluation::Boolean(true)),
("2 < 1", Evaluation::Boolean(false)),
("1 <= 1", Evaluation::Boolean(true)),
("1 <= 2", Evaluation::Boolean(true)),
("2 <= 1", Evaluation::Boolean(false)),
("2 > 1", Evaluation::Boolean(true)),
("1 > 2", Evaluation::Boolean(false)),
("1 >= 1", Evaluation::Boolean(true)),
("2 >= 1", Evaluation::Boolean(true)),
("1 >= 2", Evaluation::Boolean(false)),
];
for (expr_str, expected) in test_cases {
let expr = Expr::parse(expr_str)?;
let result = expr.consteval().unwrap();
assert_eq!(result, *expected, "Failed for expression: {}", expr_str);
}
Ok(())
}
}

View file

@ -1,8 +1,10 @@
use std::{ops::Deref, sync::LazyLock};
use github_actions_expressions::{
BinOp, Call, Expr, SpannedExpr, UnOp,
Expr, SpannedExpr,
call::Call,
context::{Context, ContextPattern},
op::{BinOp, UnOp},
};
use github_actions_models::{
common::If,

View file

@ -1,6 +1,6 @@
use std::ops::Deref;
use github_actions_expressions::{Call, Expr, SpannedExpr};
use github_actions_expressions::{Expr, SpannedExpr, call::Call};
use crate::{
finding::{

View file

@ -18,7 +18,7 @@
use std::{env, ops::Deref, sync::LazyLock, vec};
use fst::Map;
use github_actions_expressions::{Expr, Literal, context::Context};
use github_actions_expressions::{Expr, context::Context, literal::Literal};
use github_actions_models::{
common::{EnvValue, RepositoryUses, Uses, expr::LoE},
workflow::job::Strategy,

View file

@ -1,6 +1,6 @@
use std::ops::Deref;
use github_actions_expressions::{Call, Expr, SpannedExpr, context::Context};
use github_actions_expressions::{Expr, SpannedExpr, call::Call, context::Context};
use crate::{
Confidence, Severity,

View file

@ -1,6 +1,8 @@
use std::ops::Deref;
use github_actions_expressions::{Call, Expr, Literal, SpannedExpr, context::Context};
use github_actions_expressions::{
Expr, SpannedExpr, call::Call, context::Context, literal::Literal,
};
use github_actions_models::common::If;
use super::{Audit, AuditLoadError, AuditState, audit_meta};