zclsyntax: FunctionCallExpr.Value
This is the first non-trivial expression Value implementation. Lots of code here, so hopefully while implementing other expressions some opportunities emerge to factor out some of these details.
This commit is contained in:
parent
8437058b60
commit
2b442985cd
@ -1,7 +1,11 @@
|
||||
package zclsyntax
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/apparentlymart/go-cty/cty"
|
||||
"github.com/apparentlymart/go-cty/cty/convert"
|
||||
"github.com/apparentlymart/go-cty/cty/function"
|
||||
"github.com/apparentlymart/go-zcl/zcl"
|
||||
)
|
||||
|
||||
@ -84,7 +88,151 @@ func (e *FunctionCallExpr) walkChildNodes(w internalWalkFunc) {
|
||||
}
|
||||
|
||||
func (e *FunctionCallExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
|
||||
panic("FunctionCallExpr.Value not yet implemented")
|
||||
var diags zcl.Diagnostics
|
||||
|
||||
f, exists := ctx.Functions[e.Name]
|
||||
if !exists {
|
||||
avail := make([]string, 0, len(ctx.Functions))
|
||||
for name := range ctx.Functions {
|
||||
avail = append(avail, name)
|
||||
}
|
||||
suggestion := nameSuggestion(e.Name, avail)
|
||||
if suggestion != "" {
|
||||
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
|
||||
}
|
||||
|
||||
return cty.DynamicVal, zcl.Diagnostics{
|
||||
{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Call to unknown function",
|
||||
Detail: fmt.Sprintf("There is no function named %q.%s", e.Name, suggestion),
|
||||
Subject: &e.NameRange,
|
||||
Context: e.Range().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
params := f.Params()
|
||||
varParam := f.VarParam()
|
||||
|
||||
if len(e.Args) < len(params) {
|
||||
missing := params[len(e.Args)]
|
||||
qual := ""
|
||||
if varParam != nil {
|
||||
qual = " at least"
|
||||
}
|
||||
return cty.DynamicVal, zcl.Diagnostics{
|
||||
{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Not enough function arguments",
|
||||
Detail: fmt.Sprintf(
|
||||
"Function %q expects%s %d argument(s). Missing value for %q.",
|
||||
e.Name, qual, len(params), missing.Name,
|
||||
),
|
||||
Subject: &e.CloseParenRange,
|
||||
Context: e.Range().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if varParam == nil && len(e.Args) > len(params) {
|
||||
return cty.DynamicVal, zcl.Diagnostics{
|
||||
{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Too many function arguments",
|
||||
Detail: fmt.Sprintf(
|
||||
"Function %q expects only %d argument(s).",
|
||||
e.Name, len(params),
|
||||
),
|
||||
Subject: e.Args[len(params)].StartRange().Ptr(),
|
||||
Context: e.Range().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
argVals := make([]cty.Value, len(e.Args))
|
||||
|
||||
for i, argExpr := range e.Args {
|
||||
var param *function.Parameter
|
||||
if i < len(params) {
|
||||
param = ¶ms[i]
|
||||
} else {
|
||||
param = varParam
|
||||
}
|
||||
|
||||
val, argDiags := argExpr.Value(ctx)
|
||||
if len(argDiags) > 0 {
|
||||
diags = append(diags, argDiags...)
|
||||
}
|
||||
|
||||
// Try to convert our value to the parameter type
|
||||
val, err := convert.Convert(val, param.Type)
|
||||
if err != nil {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Invalid function argument",
|
||||
Detail: fmt.Sprintf(
|
||||
"Invalid value for %q parameter: %s.",
|
||||
param.Name, err,
|
||||
),
|
||||
Subject: argExpr.StartRange().Ptr(),
|
||||
Context: e.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
argVals[i] = val
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
// Don't try to execute the function if we already have errors with
|
||||
// the arguments, because the result will probably be a confusing
|
||||
// error message.
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
resultVal, err := f.Call(argVals)
|
||||
if err != nil {
|
||||
switch terr := err.(type) {
|
||||
case function.ArgError:
|
||||
i := terr.Index
|
||||
var param *function.Parameter
|
||||
if i < len(params) {
|
||||
param = ¶ms[i]
|
||||
} else {
|
||||
param = varParam
|
||||
}
|
||||
argExpr := e.Args[i]
|
||||
|
||||
// TODO: we should also unpick a PathError here and show the
|
||||
// path to the deep value where the error was detected.
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Invalid function argument",
|
||||
Detail: fmt.Sprintf(
|
||||
"Invalid value for %q parameter: %s.",
|
||||
param.Name, err,
|
||||
),
|
||||
Subject: argExpr.StartRange().Ptr(),
|
||||
Context: e.Range().Ptr(),
|
||||
})
|
||||
|
||||
default:
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Error in function call",
|
||||
Detail: fmt.Sprintf(
|
||||
"Call to function %q failed: %s.",
|
||||
e.Name, err,
|
||||
),
|
||||
Subject: e.StartRange().Ptr(),
|
||||
Context: e.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
return resultVal, diags
|
||||
}
|
||||
|
||||
func (e *FunctionCallExpr) Range() zcl.Range {
|
||||
|
188
zcl/zclsyntax/expression_test.go
Normal file
188
zcl/zclsyntax/expression_test.go
Normal file
@ -0,0 +1,188 @@
|
||||
package zclsyntax
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/apparentlymart/go-cty/cty"
|
||||
"github.com/apparentlymart/go-cty/cty/function"
|
||||
"github.com/apparentlymart/go-cty/cty/function/stdlib"
|
||||
"github.com/apparentlymart/go-zcl/zcl"
|
||||
)
|
||||
|
||||
func TestFunctionCallExprValue(t *testing.T) {
|
||||
funcs := map[string]function.Function{
|
||||
"length": stdlib.StrlenFunc,
|
||||
"jsondecode": stdlib.JSONDecodeFunc,
|
||||
}
|
||||
|
||||
tests := map[string]struct {
|
||||
expr *FunctionCallExpr
|
||||
ctx *zcl.EvalContext
|
||||
want cty.Value
|
||||
diagCount int
|
||||
}{
|
||||
"valid call with no conversions": {
|
||||
&FunctionCallExpr{
|
||||
Name: "length",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.StringVal("hello"),
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.NumberIntVal(5),
|
||||
0,
|
||||
},
|
||||
"valid call with arg conversion": {
|
||||
&FunctionCallExpr{
|
||||
Name: "length",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.BoolVal(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.NumberIntVal(4), // length of string "true"
|
||||
0,
|
||||
},
|
||||
"valid call with unknown arg": {
|
||||
&FunctionCallExpr{
|
||||
Name: "length",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.UnknownVal(cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.UnknownVal(cty.Number),
|
||||
0,
|
||||
},
|
||||
"valid call with unknown arg needing conversion": {
|
||||
&FunctionCallExpr{
|
||||
Name: "length",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.UnknownVal(cty.Bool),
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.UnknownVal(cty.Number),
|
||||
0,
|
||||
},
|
||||
"valid call with dynamic arg": {
|
||||
&FunctionCallExpr{
|
||||
Name: "length",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.DynamicVal,
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.UnknownVal(cty.Number),
|
||||
0,
|
||||
},
|
||||
"invalid arg type": {
|
||||
&FunctionCallExpr{
|
||||
Name: "length",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.ListVal([]cty.Value{cty.StringVal("hello")}),
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.DynamicVal,
|
||||
1,
|
||||
},
|
||||
"function with dynamic return type": {
|
||||
&FunctionCallExpr{
|
||||
Name: "jsondecode",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.StringVal(`"hello"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.StringVal("hello"),
|
||||
0,
|
||||
},
|
||||
"function with dynamic return type unknown arg": {
|
||||
&FunctionCallExpr{
|
||||
Name: "jsondecode",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.UnknownVal(cty.String),
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.DynamicVal, // type depends on arg value
|
||||
0,
|
||||
},
|
||||
"error in function": {
|
||||
&FunctionCallExpr{
|
||||
Name: "jsondecode",
|
||||
Args: []Expression{
|
||||
&LiteralValueExpr{
|
||||
Val: cty.StringVal("invalid-json"),
|
||||
},
|
||||
},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.DynamicVal,
|
||||
1, // JSON parse error
|
||||
},
|
||||
"unknown function": {
|
||||
&FunctionCallExpr{
|
||||
Name: "lenth",
|
||||
Args: []Expression{},
|
||||
},
|
||||
&zcl.EvalContext{
|
||||
Functions: funcs,
|
||||
},
|
||||
cty.DynamicVal,
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := test.expr.Value(test.ctx)
|
||||
|
||||
if len(diags) != test.diagCount {
|
||||
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
|
||||
for _, diag := range diags {
|
||||
t.Logf(" - %s", diag.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if !got.RawEquals(test.want) {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user