Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

walker: Pass through context to enable more granular validation #341

Merged
merged 5 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 43 additions & 18 deletions decoder/internal/walker/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand Down
160 changes: 160 additions & 0 deletions decoder/internal/walker/walker_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
9 changes: 6 additions & 3 deletions decoder/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions schemacontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
10 changes: 6 additions & 4 deletions validator/attribute_deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -33,5 +35,5 @@ func (v DeprecatedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nod
})
}

return
return ctx, diags
}
12 changes: 7 additions & 5 deletions validator/attribute_missing_required.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -43,5 +45,5 @@ func (v MissingRequiredAttribute) Visit(ctx context.Context, node hclsyntax.Node
}
}

return
return ctx, diags
}
10 changes: 6 additions & 4 deletions validator/attribute_unexpected.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -36,5 +38,5 @@ func (v UnexpectedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nod
})
}

return
return ctx, diags
}
Loading