mirror of https://github.com/hashicorp/terraform
When an error occurs in a function call, the error message text often includes references to particular parameters in the function signature. This commit improves that reporting by also including a summary of the full function signature as part of the diagnostic context in that case, so a reader can see which parameter is which given that function arguments are always assigned positionally and so the parameter names do not appear in the caller's source code.pull/31321/head
parent
8405f46bc5
commit
31aee9650e
@ -0,0 +1,112 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
// Function is a description of the JSON representation of the signature of
|
||||
// a function callable from the Terraform language.
|
||||
type Function struct {
|
||||
// Name is the leaf name of the function, without any namespace prefix.
|
||||
Name string `json:"name"`
|
||||
|
||||
Params []FunctionParam `json:"params"`
|
||||
VariadicParam *FunctionParam `json:"variadic_param,omitempty"`
|
||||
|
||||
// ReturnType is type constraint which is a static approximation of the
|
||||
// possibly-dynamic return type of the function.
|
||||
ReturnType json.RawMessage `json:"return_type"`
|
||||
|
||||
Description string `json:"description,omitempty"`
|
||||
DescriptionKind string `json:"description_kind,omitempty"`
|
||||
}
|
||||
|
||||
// FunctionParam represents a single parameter to a function, as represented
|
||||
// by type Function.
|
||||
type FunctionParam struct {
|
||||
// Name is a name for the function which is used primarily for
|
||||
// documentation purposes, because function arguments are positional
|
||||
// and therefore don't appear directly in configuration source code.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Type is a type constraint which is a static approximation of the
|
||||
// possibly-dynamic type of the parameter. Particular functions may
|
||||
// have additional requirements that a type constraint alone cannot
|
||||
// represent.
|
||||
Type json.RawMessage `json:"type"`
|
||||
|
||||
// Maybe some of the other fields in function.Parameter would be
|
||||
// interesting to describe here too, but we'll wait to see if there
|
||||
// is a use-case first.
|
||||
|
||||
Description string `json:"description,omitempty"`
|
||||
DescriptionKind string `json:"description_kind,omitempty"`
|
||||
}
|
||||
|
||||
// DescribeFunction returns a description of the signature of the given cty
|
||||
// function, as a pointer to this package's serializable type Function.
|
||||
func DescribeFunction(name string, f function.Function) *Function {
|
||||
ret := &Function{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
params := f.Params()
|
||||
ret.Params = make([]FunctionParam, len(params))
|
||||
typeCheckArgs := make([]cty.Type, len(params), len(params)+1)
|
||||
for i, param := range params {
|
||||
ret.Params[i] = describeFunctionParam(¶m)
|
||||
typeCheckArgs[i] = param.Type
|
||||
}
|
||||
if varParam := f.VarParam(); varParam != nil {
|
||||
descParam := describeFunctionParam(varParam)
|
||||
ret.VariadicParam = &descParam
|
||||
typeCheckArgs = append(typeCheckArgs, varParam.Type)
|
||||
}
|
||||
|
||||
retType, err := f.ReturnType(typeCheckArgs)
|
||||
if err != nil {
|
||||
// Getting an error when type-checking with exactly the type constraints
|
||||
// the function called for is weird, so we'll just treat it as if it
|
||||
// has a dynamic return type instead, for our purposes here.
|
||||
// One reason this can happen is for a function which has a variadic
|
||||
// parameter but has logic inside it which considers it invalid to
|
||||
// specify exactly one argument for that parameter (since that's what
|
||||
// we did in typeCheckArgs as an approximation of a valid call above.)
|
||||
retType = cty.DynamicPseudoType
|
||||
}
|
||||
|
||||
if raw, err := retType.MarshalJSON(); err != nil {
|
||||
// Again, we'll treat any errors as if the function is dynamically
|
||||
// typed because it would be weird to get here.
|
||||
ret.ReturnType = json.RawMessage(`"dynamic"`)
|
||||
} else {
|
||||
ret.ReturnType = json.RawMessage(raw)
|
||||
}
|
||||
|
||||
// We don't currently have any sense of descriptions for functions and
|
||||
// their parameters, so we'll just leave those fields unpopulated for now.
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func describeFunctionParam(p *function.Parameter) FunctionParam {
|
||||
ret := FunctionParam{
|
||||
Name: p.Name,
|
||||
}
|
||||
|
||||
if raw, err := p.Type.MarshalJSON(); err != nil {
|
||||
// We'll treat any errors as if the function is dynamically
|
||||
// typed because it would be weird to get here.
|
||||
ret.Type = json.RawMessage(`"dynamic"`)
|
||||
} else {
|
||||
ret.Type = json.RawMessage(raw)
|
||||
}
|
||||
|
||||
// We don't currently have any sense of descriptions for functions and
|
||||
// their parameters, so we'll just leave those fields unpopulated for now.
|
||||
|
||||
return ret
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
"github.com/zclconf/go-cty/cty/function/stdlib"
|
||||
)
|
||||
|
||||
func TestDescribeFunction(t *testing.T) {
|
||||
// NOTE: This test case is referring to some real functions in other
|
||||
// packages. and so if those functions change signature later it will
|
||||
// probably make some cases here fail. If that is the cause of the failure,
|
||||
// it's fine to update the test here to match rather than to revert the
|
||||
// change to the function signature, as long as the change to the
|
||||
// function signature is otherwise within the bounds of our compatibility
|
||||
// promises.
|
||||
|
||||
tests := map[string]struct {
|
||||
Function function.Function
|
||||
Want *Function
|
||||
}{
|
||||
"upper": {
|
||||
Function: stdlib.UpperFunc,
|
||||
Want: &Function{
|
||||
Name: "upper",
|
||||
Params: []FunctionParam{
|
||||
{
|
||||
Name: "str",
|
||||
Type: json.RawMessage(`"string"`),
|
||||
},
|
||||
},
|
||||
ReturnType: json.RawMessage(`"string"`),
|
||||
},
|
||||
},
|
||||
"coalesce": {
|
||||
Function: stdlib.CoalesceFunc,
|
||||
Want: &Function{
|
||||
Name: "coalesce",
|
||||
Params: []FunctionParam{},
|
||||
VariadicParam: &FunctionParam{
|
||||
Name: "vals",
|
||||
Type: json.RawMessage(`"dynamic"`),
|
||||
},
|
||||
ReturnType: json.RawMessage(`"dynamic"`),
|
||||
},
|
||||
},
|
||||
"join": {
|
||||
Function: stdlib.JoinFunc,
|
||||
Want: &Function{
|
||||
Name: "join",
|
||||
Params: []FunctionParam{
|
||||
{
|
||||
Name: "separator",
|
||||
Type: json.RawMessage(`"string"`),
|
||||
},
|
||||
},
|
||||
VariadicParam: &FunctionParam{
|
||||
Name: "lists",
|
||||
Type: json.RawMessage(`["list","string"]`),
|
||||
},
|
||||
ReturnType: json.RawMessage(`"string"`),
|
||||
},
|
||||
},
|
||||
"jsonencode": {
|
||||
Function: stdlib.JSONEncodeFunc,
|
||||
Want: &Function{
|
||||
Name: "jsonencode",
|
||||
Params: []FunctionParam{
|
||||
{
|
||||
Name: "val",
|
||||
Type: json.RawMessage(`"dynamic"`),
|
||||
},
|
||||
},
|
||||
ReturnType: json.RawMessage(`"string"`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := DescribeFunction(name, test.Function)
|
||||
want := test.Want
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("wrong result\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue