Files
act_runner/internal/eval/v2/evaluator.go
Christopher Homberger 09d18916bf more lint errors
2026-02-23 00:30:44 +01:00

277 lines
7.4 KiB
Go

package v2
import (
"errors"
"fmt"
exprparser "gitea.com/gitea/act_runner/internal/expr"
)
// EvaluationContext holds variables that can be referenced in expressions.
type EvaluationContext struct {
Variables ReadOnlyObject[any]
Functions ReadOnlyObject[Function]
}
func NewEvaluationContext() *EvaluationContext {
return &EvaluationContext{}
}
type Function interface {
Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error)
}
// Evaluator evaluates workflow expressions using the lexer and parser from workflow.
type Evaluator struct {
ctx *EvaluationContext
}
// NewEvaluator creates an Evaluator with the supplied context.
func NewEvaluator(ctx *EvaluationContext) *Evaluator {
return &Evaluator{ctx: ctx}
}
func (e *Evaluator) Context() *EvaluationContext {
return e.ctx
}
func (e *Evaluator) Evaluate(root exprparser.Node) (*EvaluationResult, error) {
result, err := e.evalNode(root)
if err != nil {
return nil, err
}
return result, nil
}
// EvaluateBoolean parses and evaluates the expression, returning a boolean result.
func (e *Evaluator) EvaluateBoolean(expr string) (bool, error) {
root, err := exprparser.Parse(expr)
if err != nil {
return false, fmt.Errorf("parse error: %w", err)
}
result, err := e.evalNode(root)
if err != nil {
return false, err
}
return result.IsTruthy(), nil
}
func (e *Evaluator) ToRaw(result *EvaluationResult) (any, error) {
if col, ok := result.TryGetCollectionInterface(); ok {
switch node := col.(type) {
case ReadOnlyObject[any]:
rawMap := map[string]any{}
for k, v := range node.GetEnumerator() {
rawRes, err := e.ToRaw(CreateIntermediateResult(e.Context(), v))
if err != nil {
return nil, err
}
rawMap[k] = rawRes
}
return rawMap, nil
case ReadOnlyArray[any]:
rawArray := []any{}
for _, v := range node.GetEnumerator() {
rawRes, err := e.ToRaw(CreateIntermediateResult(e.Context(), v))
if err != nil {
return nil, err
}
rawArray = append(rawArray, rawRes)
}
return rawArray, nil
}
}
return result.Value(), nil
}
// Evaluate parses and evaluates the expression, returning a boolean result.
func (e *Evaluator) EvaluateRaw(expr string) (any, error) {
root, err := exprparser.Parse(expr)
if err != nil {
return false, fmt.Errorf("parse error: %w", err)
}
result, err := e.evalNode(root)
if err != nil {
return false, err
}
return e.ToRaw(result)
}
type FilteredArray []any
func (a FilteredArray) GetAt(i int64) any {
if int(i) > len(a) {
return nil
}
return a[i]
}
func (a FilteredArray) GetEnumerator() []any {
return a
}
// evalNode recursively evaluates a parser node and returns an EvaluationResult.
func (e *Evaluator) evalNode(n exprparser.Node) (*EvaluationResult, error) {
switch node := n.(type) {
case *exprparser.ValueNode:
return e.evalValueNode(node)
case *exprparser.FunctionNode:
return e.evalFunctionNode(node)
case *exprparser.BinaryNode:
return e.evalBinaryNode(node)
case *exprparser.UnaryNode:
return e.evalUnaryNode(node)
}
return nil, errors.New("unknown node type")
}
func (e *Evaluator) evalValueNode(node *exprparser.ValueNode) (*EvaluationResult, error) {
if node.Kind == exprparser.TokenKindNamedValue {
if e.ctx != nil {
val := e.ctx.Variables.Get(node.Value.(string))
if val == nil {
return nil, fmt.Errorf("undefined variable %s", node.Value)
}
return CreateIntermediateResult(e.Context(), val), nil
}
return nil, errors.New("no evaluation context")
}
return CreateIntermediateResult(e.Context(), node.Value), nil
}
func (e *Evaluator) evalFunctionNode(node *exprparser.FunctionNode) (*EvaluationResult, error) {
fn := e.ctx.Functions.Get(node.Name)
if fn == nil {
return nil, fmt.Errorf("unknown function %v", node.Name)
}
return fn.Evaluate(e, node.Args)
}
func (e *Evaluator) evalBinaryNode(node *exprparser.BinaryNode) (*EvaluationResult, error) {
left, err := e.evalNode(node.Left)
if err != nil {
return nil, err
}
if res, err := e.evalBinaryNodeLeft(node, left); res != nil || err != nil {
return res, err
}
right, err := e.evalNode(node.Right)
if err != nil {
return nil, err
}
return e.evalBinaryNodeRight(node, left, right)
}
func (e *Evaluator) evalBinaryNodeLeft(node *exprparser.BinaryNode, left *EvaluationResult) (*EvaluationResult, error) {
switch node.Op {
case "&&":
if left.IsFalsy() {
return left, nil
}
case "||":
if left.IsTruthy() {
return left, nil
}
case ".":
if v, ok := node.Right.(*exprparser.ValueNode); ok && v.Kind == exprparser.TokenKindWildcard {
var ret FilteredArray
if col, ok := left.TryGetCollectionInterface(); ok {
if farray, ok := col.(FilteredArray); ok {
for _, subcol := range farray.GetEnumerator() {
ret = processStar(CreateIntermediateResult(e.Context(), subcol).Value(), ret)
}
} else {
ret = processStar(col, ret)
}
}
return CreateIntermediateResult(e.Context(), ret), nil
}
}
return nil, errors.ErrUnsupported
}
func (e *Evaluator) evalBinaryNodeRight(node *exprparser.BinaryNode, left *EvaluationResult, right *EvaluationResult) (*EvaluationResult, error) {
switch node.Op {
case "&&":
return right, nil
case "||":
return right, nil
case "==":
// Use abstract equality per spec
return CreateIntermediateResult(e.Context(), left.AbstractEqual(right)), nil
case "!=":
return CreateIntermediateResult(e.Context(), left.AbstractNotEqual(right)), nil
case ">":
return CreateIntermediateResult(e.Context(), left.AbstractGreaterThan(right)), nil
case "<":
return CreateIntermediateResult(e.Context(), left.AbstractLessThan(right)), nil
case ">=":
return CreateIntermediateResult(e.Context(), left.AbstractGreaterThanOrEqual(right)), nil
case "<=":
return CreateIntermediateResult(e.Context(), left.AbstractLessThanOrEqual(right)), nil
case ".", "[":
if farray, ok := left.Value().(FilteredArray); ok {
var ret FilteredArray
for _, subcol := range farray.GetEnumerator() {
res := processIndex(CreateIntermediateResult(e.Context(), subcol).Value(), right)
if res != nil {
ret = append(ret, res)
}
}
if ret == nil {
return CreateIntermediateResult(e.Context(), nil), nil
}
return CreateIntermediateResult(e.Context(), ret), nil
}
col, _ := left.TryGetCollectionInterface()
result := processIndex(col, right)
return CreateIntermediateResult(e.Context(), result), nil
default:
return nil, fmt.Errorf("unsupported operator %s", node.Op)
}
}
func (e *Evaluator) evalUnaryNode(node *exprparser.UnaryNode) (*EvaluationResult, error) {
operand, err := e.evalNode(node.Operand)
if err != nil {
return nil, err
}
switch node.Op {
case "!":
return CreateIntermediateResult(e.Context(), !operand.IsTruthy()), nil
default:
return nil, fmt.Errorf("unsupported unary operator %s", node.Op)
}
}
func processIndex(col any, right *EvaluationResult) any {
if mapVal, ok := col.(ReadOnlyObject[any]); ok {
key, ok := right.Value().(string)
if !ok {
return nil
}
val := mapVal.Get(key)
return val
}
if arrayVal, ok := col.(ReadOnlyArray[any]); ok {
key, ok := right.Value().(float64)
if !ok || key < 0 {
return nil
}
val := arrayVal.GetAt(int64(key))
return val
}
return nil
}
func processStar(subcol any, ret FilteredArray) FilteredArray {
if array, ok := subcol.(ReadOnlyArray[any]); ok {
ret = append(ret, array.GetEnumerator()...)
} else if obj, ok := subcol.(ReadOnlyObject[any]); ok {
for _, v := range obj.GetEnumerator() {
ret = append(ret, v)
}
}
return ret
}