diff --git a/hcldec/functions.go b/hcldec/functions.go new file mode 100644 index 00000000..9e3e7bc3 --- /dev/null +++ b/hcldec/functions.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcldec + +import ( + "github.com/hashicorp/hcl/v2" +) + +// This is based off of hcldec/variables.go + +// Functions processes the given body with the given spec and returns a +// list of the function traversals that would be required to decode +// the same pairing of body and spec. +// +// This can be used to conditionally populate the functions in the EvalContext +// passed to Decode, for applications where a static scope is insufficient. +// +// If the given body is not compliant with the given schema, the result may +// be incomplete, but that's assumed to be okay because the eventual call +// to Decode will produce error diagnostics anyway. +func Functions(body hcl.Body, spec Spec) []hcl.Traversal { + var funcs []hcl.Traversal + schema := ImpliedSchema(spec) + content, _, _ := body.PartialContent(schema) + + if vs, ok := spec.(specNeedingFunctions); ok { + funcs = append(funcs, vs.functionsNeeded(content)...) + } + + var visitFn visitFunc + visitFn = func(s Spec) { + if vs, ok := s.(specNeedingFunctions); ok { + funcs = append(funcs, vs.functionsNeeded(content)...) + } + s.visitSameBodyChildren(visitFn) + } + spec.visitSameBodyChildren(visitFn) + + return funcs +} diff --git a/hcldec/functions_test.go b/hcldec/functions_test.go new file mode 100644 index 00000000..e2f7789f --- /dev/null +++ b/hcldec/functions_test.go @@ -0,0 +1,217 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcldec + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +// This is inspired by hcldec/variables_test.go + +func TestFunctions(t *testing.T) { + tests := []struct { + config string + spec Spec + want []hcl.Traversal + }{ + { + ``, + &ObjectSpec{}, + nil, + }, + { + "a = foo()\n", + &ObjectSpec{}, + nil, // "a" is not actually used, so "foo" is not required + }, + { + "a = foo()\n", + &AttrSpec{ + Name: "a", + }, + []hcl.Traversal{ + { + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + }, + { + "a = foo()\nb = bar()\n", + &DefaultSpec{ + Primary: &AttrSpec{ + Name: "a", + }, + Default: &AttrSpec{ + Name: "b", + }, + }, + []hcl.Traversal{ + { + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + { + hcl.TraverseRoot{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 2, Column: 5, Byte: 14}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 17}, + }, + }, + }, + }, + }, + { + "a = foo()\n", + &ObjectSpec{ + "a": &AttrSpec{ + Name: "a", + }, + }, + []hcl.Traversal{ + { + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + }, + { + ` +b { + a = foo() +} +`, + &BlockSpec{ + TypeName: "b", + Nested: &AttrSpec{ + Name: "a", + }, + }, + []hcl.Traversal{ + { + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 3, Column: 7, Byte: 11}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 14}, + }, + }, + }, + }, + }, + { + ` +b { + a = foo() + b = bar() +} + `, + &BlockAttrsSpec{ + TypeName: "b", + ElementType: cty.String, + }, + []hcl.Traversal{ + { + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 3, Column: 7, Byte: 11}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 14}, + }, + }, + }, + { + hcl.TraverseRoot{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 4, Column: 7, Byte: 23}, + End: hcl.Pos{Line: 4, Column: 10, Byte: 26}, + }, + }, + }, + }, + }, + { + ` +b { + a = foo() +} +b { + a = bar() +} +c { + a = baz() +} +`, + &BlockListSpec{ + TypeName: "b", + Nested: &AttrSpec{ + Name: "a", + }, + }, + []hcl.Traversal{ + { + hcl.TraverseRoot{ + Name: "foo", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 3, Column: 7, Byte: 11}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 14}, + }, + }, + }, + { + hcl.TraverseRoot{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 6, Column: 7, Byte: 29}, + End: hcl.Pos{Line: 6, Column: 10, Byte: 32}, + }, + }, + }, + }, + }, /**/ + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%02d-%s", i, test.config), func(t *testing.T) { + file, diags := hclsyntax.ParseConfig([]byte(test.config), "", hcl.Pos{Line: 1, Column: 1, Byte: 0}) + if len(diags) != 0 { + t.Errorf("wrong number of diagnostics from ParseConfig %d; want %d", len(diags), 0) + for _, diag := range diags { + t.Logf(" - %s", diag.Error()) + } + } + body := file.Body + + got := Functions(body, test.spec) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) + } + }) + } + +} diff --git a/hcldec/spec.go b/hcldec/spec.go index 2bebc433..01e9a365 100644 --- a/hcldec/spec.go +++ b/hcldec/spec.go @@ -67,6 +67,12 @@ type specNeedingVariables interface { variablesNeeded(content *hcl.BodyContent) []hcl.Traversal } +// specNeedingFunctions is implemented by specs that can use functions +// from the EvalContext, to declare which functions they need. +type specNeedingFunctions interface { + functionsNeeded(content *hcl.BodyContent) []hcl.Traversal +} + // UnknownBody can be optionally implemented by an hcl.Body instance which may // be entirely unknown. type UnknownBody interface { @@ -176,6 +182,19 @@ func (s *AttrSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { return attr.Expr.Variables() } +// specNeedingFunctions implementation +func (s *AttrSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + attr, exists := content.Attributes[s.Name] + if !exists { + return nil + } + + if fexpr, ok := attr.Expr.(hcl.ExpressionWithFunctions); ok { + return fexpr.Functions() + } + return nil +} + // attrSpec implementation func (s *AttrSpec) attrSchemata() []hcl.AttributeSchema { return []hcl.AttributeSchema{ @@ -282,6 +301,14 @@ func (s *ExprSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { return s.Expr.Variables() } +// specNeedingFunctions implementation +func (s *ExprSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + if fexpr, ok := s.Expr.(hcl.ExpressionWithFunctions); ok { + return fexpr.Functions() + } + return nil +} + func (s *ExprSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return s.Expr.Value(ctx) } @@ -345,6 +372,25 @@ func (s *BlockSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal { return Variables(childBlock.Body, s.Nested) } +// specNeedingFunctions implementation +func (s *BlockSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + var childBlock *hcl.Block + for _, candidate := range content.Blocks { + if candidate.Type != s.TypeName { + continue + } + + childBlock = candidate + break + } + + if childBlock == nil { + return nil + } + + return Functions(childBlock.Body, s.Nested) +} + func (s *BlockSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics @@ -457,6 +503,21 @@ func (s *BlockListSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversa return ret } +// specNeedingFunctions implementation +func (s *BlockListSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + var ret []hcl.Traversal + + for _, childBlock := range content.Blocks { + if childBlock.Type != s.TypeName { + continue + } + + ret = append(ret, Functions(childBlock.Body, s.Nested)...) + } + + return ret +} + func (s *BlockListSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics @@ -619,6 +680,21 @@ func (s *BlockTupleSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Travers return ret } +// specNeedingFunctions implementation +func (s *BlockTupleSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + var ret []hcl.Traversal + + for _, childBlock := range content.Blocks { + if childBlock.Type != s.TypeName { + continue + } + + ret = append(ret, Functions(childBlock.Body, s.Nested)...) + } + + return ret +} + func (s *BlockTupleSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics @@ -741,6 +817,21 @@ func (s *BlockSetSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal return ret } +// specNeedingFunctions implementation +func (s *BlockSetSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + var ret []hcl.Traversal + + for _, childBlock := range content.Blocks { + if childBlock.Type != s.TypeName { + continue + } + + ret = append(ret, Functions(childBlock.Body, s.Nested)...) + } + + return ret +} + func (s *BlockSetSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics @@ -902,6 +993,21 @@ func (s *BlockMapSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traversal return ret } +// specNeedingFunctions implementation +func (s *BlockMapSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + var ret []hcl.Traversal + + for _, childBlock := range content.Blocks { + if childBlock.Type != s.TypeName { + continue + } + + ret = append(ret, Functions(childBlock.Body, s.Nested)...) + } + + return ret +} + func (s *BlockMapSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics @@ -1059,6 +1165,21 @@ func (s *BlockObjectSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Traver return ret } +// specNeedingFunctions implementation +func (s *BlockObjectSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + var ret []hcl.Traversal + + for _, childBlock := range content.Blocks { + if childBlock.Type != s.TypeName { + continue + } + + ret = append(ret, Functions(childBlock.Body, s.Nested)...) + } + + return ret +} + func (s *BlockObjectSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics @@ -1235,6 +1356,36 @@ func (s *BlockAttrsSpec) variablesNeeded(content *hcl.BodyContent) []hcl.Travers return vars } +// specNeedingFunctions implementation +func (s *BlockAttrsSpec) functionsNeeded(content *hcl.BodyContent) []hcl.Traversal { + + block, _ := s.findBlock(content) + if block == nil { + return nil + } + + var funcs []hcl.Traversal + + attrs, diags := block.Body.JustAttributes() + if diags.HasErrors() { + return nil + } + + for _, attr := range attrs { + if fexpr, ok := attr.Expr.(hcl.ExpressionWithFunctions); ok { + funcs = append(funcs, fexpr.Functions()...) + } + } + + // We'll return the functions references in source order so that any + // error messages that result are also in source order. + sort.Slice(funcs, func(i, j int) bool { + return funcs[i].SourceRange().Start.Byte < funcs[j].SourceRange().Start.Byte + }) + + return funcs +} + func (s *BlockAttrsSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { var diags hcl.Diagnostics