Skip to content

1.15.4 breaks function patcher, with error: expected float64, but got interface {} #472

@tw1nk

Description

@tw1nk

In our codebase we have a Patcher that let's us not specify the context as a function value if the function requires a context.

However when we compile scripts with the 1.15.4 version we get errors similar to: expected float64, but got interface {}

Embedded test:


import (
	"context"
	"fmt"
	"reflect"
	"strconv"
	"testing"

	"github.com/antonmedv/expr"
	"github.com/antonmedv/expr/ast"
)

type TestStruct struct {
	Data string
}

func (ts *TestStruct) GetFloat() (float64, error) {
	return strconv.ParseFloat(ts.Data, 64)
}

func testMeFunc(arguments ...any) (any, error) {
	if len(arguments) != 2 {
		return nil, fmt.Errorf("expected 2 argument. got: %d", len(arguments))
	}

	ctx, ok := arguments[0].(context.Context)
	if !ok {
		return nil, fmt.Errorf("exprGetCurveTable() first argument should be context")
	}

	// imagine we need something from the context.
	ctxValue := ctx.Value("test").(string)
	if ctxValue != "value" {
		return nil, fmt.Errorf("invalid context value")
	}

	data, ok := arguments[1].(string)
	if !ok {
		return nil, fmt.Errorf("expected argument 2 to be a string. was: %T", arguments[0])
	}

	return &TestStruct{
		Data: data,
	}, nil
}

type EvaluationData struct {
	Ctx context.Context `expr:"ctx"`
}

type ContextPatcher struct{}

func (p *ContextPatcher) Visit(node *ast.Node) {
	contextType := reflect.TypeOf((*context.Context)(nil)).Elem()

	//nolint:nestif
	if callNode, ok := (*node).(*ast.CallNode); ok {
		// patch context calls so we don't have to pass the context in the code
		if callNode.Func == nil {
			return
		}

		funcType := callNode.Func.Types[0]
		if funcType.NumIn() > 0 {
			firstIn := funcType.In(0)

			if firstIn.Implements(contextType) {
				// prepend patch context calls so we don't need to specify the context in the function invocation
				args := append([]ast.Node{
					&ast.IdentifierNode{
						Value: "ctx",
					},
				}, callNode.Arguments...)

				newNode := &ast.CallNode{
					Callee:    callNode.Callee,
					Arguments: args,
					Typed:     callNode.Typed,
					Fast:      callNode.Fast,
					Func:      callNode.Func,
				}

				ast.Patch(node, newNode)
			}
		}
	}
}

func TestFunctionCallReturnsFloat(t *testing.T) {
	opts := []expr.Option{
		expr.Function("TestMe",
			testMeFunc,
			new(func(context.Context, string) *TestStruct),
		),

		expr.Env(&EvaluationData{}),

		expr.AsFloat64(),

		expr.Patch(&ContextPatcher{}),
	}

	program, err := expr.Compile(`TestMe("1.33").GetFloat()`, opts...)
	if err != nil {
		t.Fatalf("failed to compile script. %v", err)
	}

	ctx := context.WithValue(context.Background(), "test", "value")

	evalData := &EvaluationData{
		Ctx: ctx,
	}

	result, err := expr.Run(program, evalData)
	if err != nil {
		t.Fatalf("failed to run program. %v", err)
	}

	value, ok := result.(float64)
	if !ok {
		t.Errorf("expected result to be a float. was: %T", result)
	}

	if value != 1.33 {
		t.Errorf("expected result to be 1.33, was: %v", value)
	}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions