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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions