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
|
package zclsyntax
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/apparentlymart/go-cty/cty"
|
"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"
|
"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) {
|
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 {
|
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…
x
Reference in New Issue
Block a user