diff --git a/decoder/internal/walker/walker.go b/decoder/internal/walker/walker.go index 16fdcad2..15db7075 100644 --- a/decoder/internal/walker/walker.go +++ b/decoder/internal/walker/walker.go @@ -14,7 +14,7 @@ import ( ) type Walker interface { - Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) hcl.Diagnostics + Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) } // Walk walks the given node while providing schema relevant to the node. @@ -24,14 +24,27 @@ type Walker interface { func Walk(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema, w Walker) hcl.Diagnostics { var diags hcl.Diagnostics + blkNestingLvl, ok := schemacontext.BlockNestingLevel(ctx) + if !ok { + ctx = schemacontext.WithBlockNestingLevel(ctx, 0) + blkNestingLvl = 0 + } + switch nodeType := node.(type) { case *hclsyntax.Body: + bodyCtx := ctx foundBlocks := make(map[string]uint64) dynamicBlocks := make(map[string]uint64) - bodySchema, ok := nodeSchema.(*schema.BodySchema) - if ok { - for _, attr := range nodeType.Attributes { - var attrSchema schema.Schema = nil + + bodySchema, bodySchemaOk := nodeSchema.(*schema.BodySchema) + + if !bodySchemaOk { + bodyCtx = schemacontext.WithUnknownSchema(bodyCtx) + } + + for _, attr := range nodeType.Attributes { + var attrSchema schema.Schema = nil + if bodySchemaOk { aSchema, ok := bodySchema.Attributes[attr.Name] if ok { attrSchema = aSchema @@ -46,12 +59,14 @@ func Walk(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema, w if bodySchema.Extensions != nil && bodySchema.Extensions.ForEach && attr.Name == "for_each" { attrSchema = schemahelper.ForEachAttributeSchema() } - - diags = diags.Extend(Walk(ctx, attr, attrSchema, w)) } - for _, block := range nodeType.Blocks { - var blockSchema schema.Schema = nil + diags = diags.Extend(Walk(bodyCtx, attr, attrSchema, w)) + } + + for _, block := range nodeType.Blocks { + var blockSchema schema.Schema = nil + if bodySchemaOk { bs, ok := bodySchema.Blocks[block.Type] if ok { blockSchema = bs @@ -71,31 +86,41 @@ func Walk(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema, w dynamicBlocks[label]++ } } - - diags = diags.Extend(Walk(ctx, block, blockSchema, w)) } + + diags = diags.Extend(Walk(bodyCtx, block, blockSchema, w)) } - ctx = schemacontext.WithFoundBlocks(ctx, foundBlocks) - ctx = schemacontext.WithDynamicBlocks(ctx, dynamicBlocks) - diags = diags.Extend(w.Visit(ctx, node, nodeSchema)) + bodyCtx = schemacontext.WithFoundBlocks(bodyCtx, foundBlocks) + bodyCtx = schemacontext.WithDynamicBlocks(bodyCtx, dynamicBlocks) + + var bodyDiags hcl.Diagnostics + _, bodyDiags = w.Visit(bodyCtx, node, nodeSchema) + diags = diags.Extend(bodyDiags) case *hclsyntax.Attribute: - diags = diags.Extend(w.Visit(ctx, node, nodeSchema)) + var attrDiags hcl.Diagnostics + _, attrDiags = w.Visit(ctx, node, nodeSchema) + diags = diags.Extend(attrDiags) case *hclsyntax.Block: - diags = diags.Extend(w.Visit(ctx, node, nodeSchema)) + var blockCtx context.Context + var blockDiags hcl.Diagnostics + + blockCtx, blockDiags = w.Visit(ctx, node, nodeSchema) + diags = diags.Extend(blockDiags) var blockBodySchema schema.Schema = nil bSchema, ok := nodeSchema.(*schema.BlockSchema) if ok && bSchema.Body != nil { mergedSchema, result := schemahelper.MergeBlockBodySchemas(nodeType.AsHCLBlock(), bSchema) if result == schemahelper.LookupFailed || result == schemahelper.LookupPartiallySuccessful { - ctx = schemacontext.WithUnknownSchema(ctx) + blockCtx = schemacontext.WithUnknownSchema(blockCtx) } blockBodySchema = mergedSchema } - diags = diags.Extend(Walk(ctx, nodeType.Body, blockBodySchema, w)) + blockCtx = schemacontext.WithBlockNestingLevel(blockCtx, blkNestingLvl+1) + diags = diags.Extend(Walk(blockCtx, nodeType.Body, blockBodySchema, w)) // TODO: case hclsyntax.Expression } diff --git a/decoder/internal/walker/walker_test.go b/decoder/internal/walker/walker_test.go new file mode 100644 index 00000000..ace51d20 --- /dev/null +++ b/decoder/internal/walker/walker_test.go @@ -0,0 +1,160 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package walker + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl-lang/schemacontext" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func TestWalk_basic(t *testing.T) { + ctx := context.Background() + src := []byte(` +first { + nested1 {} + nested2 {} +} +second { + foo = "bar" +} +`) + file, diags := hclsyntax.ParseConfig(src, "test.hcl", hcl.InitialPos) + if len(diags) > 0 { + t.Fatalf("unexpected parser diagnostics: %s", diags) + } + + rootSchema := schema.NewBodySchema() + nodeCount := 0 + tw := NewTestWalker(func(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + nodeCount++ + return ctx, nil + }) + diags = Walk(ctx, file.Body.(*hclsyntax.Body), rootSchema, tw) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags) + } + // root body + first(block+body) + nested1(block+body) + nested2(block+body) + second(block+body) + foo + expectedNodeCount := 10 + if nodeCount != expectedNodeCount { + t.Fatalf("unexpected node count: %d, expected: %d", nodeCount, expectedNodeCount) + } +} + +func TestWalk_nestingLevel(t *testing.T) { + ctx := context.Background() + src := []byte(` +first { + nested1 {} + nested2 {} +} +rootattr = "foo" +second { + foo = "bar" +} +`) + file, pDiags := hclsyntax.ParseConfig(src, "test.hcl", hcl.InitialPos) + if len(pDiags) > 0 { + t.Fatalf("unexpected parser diagnostics: %s", pDiags) + } + + rootSchema := schema.NewBodySchema() + + firstBlockFound := false + nestedBlockFound := false + rootAttrFound := false + nestedAttrFound := false + + tw := NewTestWalker(func(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + + nestingLvl, ok := schemacontext.BlockNestingLevel(ctx) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "nesting level not available", + }) + } + + if block, ok := node.(*hclsyntax.Block); ok && block.Type == "first" { + firstBlockFound = true + if nestingLvl != 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%q: unexpected nesting level %d, expected 0", "first", nestingLvl), + }) + } + } + if block, ok := node.(*hclsyntax.Block); ok && block.Type == "nested2" { + nestedBlockFound = true + if nestingLvl != 1 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%q: unexpected nesting level %d, expected 1", "nested2", nestingLvl), + }) + } + } + + if attr, ok := node.(*hclsyntax.Attribute); ok && attr.Name == "rootattr" { + rootAttrFound = true + if nestingLvl != 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%q: unexpected nesting level %d, expected 0", "rootattr", nestingLvl), + }) + } + } + + if attr, ok := node.(*hclsyntax.Attribute); ok && attr.Name == "foo" { + nestedAttrFound = true + if nestingLvl != 1 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("%q: unexpected nesting level %d, expected 1", "foo", nestingLvl), + }) + } + } + + return ctx, diags + }) + + diags := Walk(ctx, file.Body.(*hclsyntax.Body), rootSchema, tw) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags) + } + + if !firstBlockFound { + t.Error("expected first block to be found") + } + if !nestedBlockFound { + t.Error("expected nested block to be found") + } + if !rootAttrFound { + t.Error("expected root attribute to be found") + } + if !nestedAttrFound { + t.Error("expected nested attribute to be found") + } +} + +func NewTestWalker(visitFunc visitFunc) Walker { + return testWalker{ + visitFunc: visitFunc, + } +} + +type visitFunc func(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) + +type testWalker struct { + visitFunc visitFunc +} + +func (tw testWalker) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + return tw.visitFunc(ctx, node, nodeSchema) +} diff --git a/decoder/validate.go b/decoder/validate.go index edb684b9..14a352c5 100644 --- a/decoder/validate.go +++ b/decoder/validate.go @@ -70,10 +70,13 @@ type validationWalker struct { validators []validator.Validator } -func (vw validationWalker) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (vw validationWalker) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags, vDiags hcl.Diagnostics + for _, v := range vw.validators { - diags = append(diags, v.Visit(ctx, node, nodeSchema)...) + ctx, vDiags = v.Visit(ctx, node, nodeSchema) + diags = append(diags, vDiags...) } - return + return ctx, diags } diff --git a/schemacontext/context.go b/schemacontext/context.go index c83d6b27..de6b49dc 100644 --- a/schemacontext/context.go +++ b/schemacontext/context.go @@ -8,6 +8,7 @@ import "context" type unknownSchemaCtxKey struct{} type foundBlocksCtxKey struct{} type dynamicBlocksCtxKey struct{} +type blockNestingLevelCtxKey struct{} // WithUnknownSchema attaches a flag indicating that the schema being passed // is not wholly known. @@ -42,3 +43,12 @@ func WithDynamicBlocks(ctx context.Context, dynamicBlocks map[string]uint64) con func DynamicBlocks(ctx context.Context) map[string]uint64 { return ctx.Value(dynamicBlocksCtxKey{}).(map[string]uint64) } + +func WithBlockNestingLevel(ctx context.Context, nestingLvl uint64) context.Context { + return context.WithValue(ctx, blockNestingLevelCtxKey{}, nestingLvl) +} + +func BlockNestingLevel(ctx context.Context) (uint64, bool) { + lvl, ok := ctx.Value(blockNestingLevelCtxKey{}).(uint64) + return lvl, ok +} diff --git a/validator/attribute_deprecated.go b/validator/attribute_deprecated.go index 0ea7c049..3f4764ff 100644 --- a/validator/attribute_deprecated.go +++ b/validator/attribute_deprecated.go @@ -14,14 +14,16 @@ import ( type DeprecatedAttribute struct{} -func (v DeprecatedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (v DeprecatedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + attr, ok := node.(*hclsyntax.Attribute) if !ok { - return + return ctx, diags } if nodeSchema == nil { - return + return ctx, diags } attrSchema := nodeSchema.(*schema.AttributeSchema) if attrSchema.IsDeprecated { @@ -33,5 +35,5 @@ func (v DeprecatedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nod }) } - return + return ctx, diags } diff --git a/validator/attribute_missing_required.go b/validator/attribute_missing_required.go index f2561dea..10a33d2a 100644 --- a/validator/attribute_missing_required.go +++ b/validator/attribute_missing_required.go @@ -14,19 +14,21 @@ import ( type MissingRequiredAttribute struct{} -func (v MissingRequiredAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (v MissingRequiredAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + body, ok := node.(*hclsyntax.Body) if !ok { - return + return ctx, diags } if nodeSchema == nil { - return + return ctx, diags } bodySchema := nodeSchema.(*schema.BodySchema) if bodySchema.Attributes == nil { - return + return ctx, diags } for name, attr := range bodySchema.Attributes { @@ -43,5 +45,5 @@ func (v MissingRequiredAttribute) Visit(ctx context.Context, node hclsyntax.Node } } - return + return ctx, diags } diff --git a/validator/attribute_unexpected.go b/validator/attribute_unexpected.go index ad7a8d18..0f9b2ea5 100644 --- a/validator/attribute_unexpected.go +++ b/validator/attribute_unexpected.go @@ -15,16 +15,18 @@ import ( type UnexpectedAttribute struct{} -func (v UnexpectedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (v UnexpectedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + if schemacontext.HasUnknownSchema(ctx) { // Avoid checking for unexpected attributes // if we cannot tell which ones are expected. - return + return ctx, diags } attr, ok := node.(*hclsyntax.Attribute) if !ok { - return + return ctx, diags } if nodeSchema == nil { @@ -36,5 +38,5 @@ func (v UnexpectedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nod }) } - return + return ctx, diags } diff --git a/validator/block_deprecated.go b/validator/block_deprecated.go index 68d583b7..205957b4 100644 --- a/validator/block_deprecated.go +++ b/validator/block_deprecated.go @@ -14,14 +14,16 @@ import ( type DeprecatedBlock struct{} -func (v DeprecatedBlock) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (v DeprecatedBlock) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + block, ok := node.(*hclsyntax.Block) if !ok { - return + return ctx, diags } if nodeSchema == nil { - return + return ctx, diags } blockSchema := nodeSchema.(*schema.BlockSchema) if blockSchema.IsDeprecated { @@ -33,5 +35,5 @@ func (v DeprecatedBlock) Visit(ctx context.Context, node hclsyntax.Node, nodeSch }) } - return + return ctx, diags } diff --git a/validator/block_labels_length.go b/validator/block_labels_length.go index f6d00c64..78cf2237 100644 --- a/validator/block_labels_length.go +++ b/validator/block_labels_length.go @@ -14,14 +14,16 @@ import ( type BlockLabelsLength struct{} -func (v BlockLabelsLength) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (v BlockLabelsLength) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + block, ok := node.(*hclsyntax.Block) if !ok { - return + return ctx, diags } if nodeSchema == nil { - return + return ctx, diags } blockSchema := nodeSchema.(*schema.BlockSchema) @@ -47,5 +49,5 @@ func (v BlockLabelsLength) Visit(ctx context.Context, node hclsyntax.Node, nodeS }) } - return + return ctx, diags } diff --git a/validator/block_max_items.go b/validator/block_max_items.go index 7f2f1882..b09cedc0 100644 --- a/validator/block_max_items.go +++ b/validator/block_max_items.go @@ -15,14 +15,16 @@ import ( type MaxBlocks struct{} -func (v MaxBlocks) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (v MaxBlocks) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + _, ok := node.(*hclsyntax.Body) if !ok { - return + return ctx, diags } if nodeSchema == nil { - return + return ctx, diags } foundBlocks := schemacontext.FoundBlocks(ctx) @@ -42,5 +44,5 @@ func (v MaxBlocks) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema sc } } - return + return ctx, diags } diff --git a/validator/block_min_items.go b/validator/block_min_items.go index 5ff3df5d..248c6950 100644 --- a/validator/block_min_items.go +++ b/validator/block_min_items.go @@ -15,14 +15,16 @@ import ( type MinBlocks struct{} -func (v MinBlocks) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (v MinBlocks) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + _, ok := node.(*hclsyntax.Body) if !ok { - return + return ctx, diags } if nodeSchema == nil { - return + return ctx, diags } foundBlocks := schemacontext.FoundBlocks(ctx) @@ -43,7 +45,7 @@ func (v MinBlocks) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema sc } } - return + return ctx, diags } func hasDynamicBlockInBody(bodySchema *schema.BodySchema, dynamicBlocks map[string]uint64, blockName string) bool { diff --git a/validator/block_unexpected.go b/validator/block_unexpected.go index 66cd00f7..a18246a7 100644 --- a/validator/block_unexpected.go +++ b/validator/block_unexpected.go @@ -15,16 +15,18 @@ import ( type UnexpectedBlock struct{} -func (v UnexpectedBlock) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { +func (v UnexpectedBlock) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) { + var diags hcl.Diagnostics + if schemacontext.HasUnknownSchema(ctx) { // Avoid checking for unexpected blocks // if we cannot tell which ones are expected. - return + return ctx, diags } block, ok := node.(*hclsyntax.Block) if !ok { - return + return ctx, diags } if nodeSchema == nil { @@ -35,5 +37,5 @@ func (v UnexpectedBlock) Visit(ctx context.Context, node hclsyntax.Node, nodeSch Subject: block.TypeRange.Ptr(), }) } - return + return ctx, diags } diff --git a/validator/validators.go b/validator/validators.go index b205cd8a..c8d3739f 100644 --- a/validator/validators.go +++ b/validator/validators.go @@ -12,5 +12,5 @@ import ( ) type Validator interface { - Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) hcl.Diagnostics + Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (context.Context, hcl.Diagnostics) }