From 181c6bef400f73407556a01cf852371bb24899c3 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 8 Aug 2023 17:45:16 +0200 Subject: [PATCH 1/3] decoder: Decouple each validator into its own type --- .../validator/attribute_deprecated.go | 37 +++ .../validator/attribute_missing_required.go | 47 ++++ .../validator/attribute_unexpected.go | 33 +++ .../internal/validator/block_deprecated.go | 37 +++ .../internal/validator/block_labels_length.go | 51 ++++ decoder/internal/validator/block_max_items.go | 52 ++++ decoder/internal/validator/block_min_items.go | 52 ++++ .../internal/validator/block_unexpected.go | 32 +++ decoder/internal/validator/validators.go | 16 ++ decoder/internal/walker/walker.go | 73 ++++++ decoder/validate.go | 228 ++---------------- decoder/validate_test.go | 55 ++--- 12 files changed, 476 insertions(+), 237 deletions(-) create mode 100644 decoder/internal/validator/attribute_deprecated.go create mode 100644 decoder/internal/validator/attribute_missing_required.go create mode 100644 decoder/internal/validator/attribute_unexpected.go create mode 100644 decoder/internal/validator/block_deprecated.go create mode 100644 decoder/internal/validator/block_labels_length.go create mode 100644 decoder/internal/validator/block_max_items.go create mode 100644 decoder/internal/validator/block_min_items.go create mode 100644 decoder/internal/validator/block_unexpected.go create mode 100644 decoder/internal/validator/validators.go create mode 100644 decoder/internal/walker/walker.go diff --git a/decoder/internal/validator/attribute_deprecated.go b/decoder/internal/validator/attribute_deprecated.go new file mode 100644 index 00000000..0ea7c049 --- /dev/null +++ b/decoder/internal/validator/attribute_deprecated.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type DeprecatedAttribute struct{} + +func (v DeprecatedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + attr, ok := node.(*hclsyntax.Attribute) + if !ok { + return + } + + if nodeSchema == nil { + return + } + attrSchema := nodeSchema.(*schema.AttributeSchema) + if attrSchema.IsDeprecated { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: fmt.Sprintf("%q is deprecated", attr.Name), + Detail: fmt.Sprintf("Reason: %q", attrSchema.Description.Value), + Subject: attr.SrcRange.Ptr(), + }) + } + + return +} diff --git a/decoder/internal/validator/attribute_missing_required.go b/decoder/internal/validator/attribute_missing_required.go new file mode 100644 index 00000000..f2561dea --- /dev/null +++ b/decoder/internal/validator/attribute_missing_required.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type MissingRequiredAttribute struct{} + +func (v MissingRequiredAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + body, ok := node.(*hclsyntax.Body) + if !ok { + return + } + + if nodeSchema == nil { + return + } + + bodySchema := nodeSchema.(*schema.BodySchema) + if bodySchema.Attributes == nil { + return + } + + for name, attr := range bodySchema.Attributes { + if attr.IsRequired { + _, ok := body.Attributes[name] + if !ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Required attribute %q not specified", name), + Detail: fmt.Sprintf("An attribute named %q is required here", name), + Subject: body.SrcRange.Ptr(), + }) + } + } + } + + return +} diff --git a/decoder/internal/validator/attribute_unexpected.go b/decoder/internal/validator/attribute_unexpected.go new file mode 100644 index 00000000..b4d30aed --- /dev/null +++ b/decoder/internal/validator/attribute_unexpected.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type UnexpectedAttribute struct{} + +func (v UnexpectedAttribute) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + attr, ok := node.(*hclsyntax.Attribute) + if !ok { + return + } + + if nodeSchema == nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unexpected attribute", + Detail: fmt.Sprintf("An attribute named %q is not expected here", attr.Name), + Subject: attr.SrcRange.Ptr(), + }) + } + + return +} diff --git a/decoder/internal/validator/block_deprecated.go b/decoder/internal/validator/block_deprecated.go new file mode 100644 index 00000000..68d583b7 --- /dev/null +++ b/decoder/internal/validator/block_deprecated.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type DeprecatedBlock struct{} + +func (v DeprecatedBlock) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + block, ok := node.(*hclsyntax.Block) + if !ok { + return + } + + if nodeSchema == nil { + return + } + blockSchema := nodeSchema.(*schema.BlockSchema) + if blockSchema.IsDeprecated { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: fmt.Sprintf("%q is deprecated", block.Type), + Detail: fmt.Sprintf("Reason: %q", blockSchema.Description.Value), + Subject: block.TypeRange.Ptr(), + }) + } + + return +} diff --git a/decoder/internal/validator/block_labels_length.go b/decoder/internal/validator/block_labels_length.go new file mode 100644 index 00000000..f6d00c64 --- /dev/null +++ b/decoder/internal/validator/block_labels_length.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type BlockLabelsLength struct{} + +func (v BlockLabelsLength) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + block, ok := node.(*hclsyntax.Block) + if !ok { + return + } + + if nodeSchema == nil { + return + } + + blockSchema := nodeSchema.(*schema.BlockSchema) + + validLabelNum := len(blockSchema.Labels) + for i := range block.Labels { + if i >= validLabelNum { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Too many labels specified for %q", block.Type), + Detail: fmt.Sprintf("Only %d label(s) are expected for %q blocks", validLabelNum, block.Type), + Subject: block.LabelRanges[i].Ptr(), + }) + } + } + + if validLabelNum > len(block.Labels) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Not enough labels specified for %q", block.Type), + Detail: fmt.Sprintf("All %q blocks must have %d label(s)", block.Type, validLabelNum), + Subject: block.TypeRange.Ptr(), + }) + } + + return +} diff --git a/decoder/internal/validator/block_max_items.go b/decoder/internal/validator/block_max_items.go new file mode 100644 index 00000000..b31f39d5 --- /dev/null +++ b/decoder/internal/validator/block_max_items.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type MaxBlocks struct{} + +func (v MaxBlocks) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + body, ok := node.(*hclsyntax.Body) + if !ok { + return + } + + if nodeSchema == nil { + return + } + + foundBlocks := make(map[string]uint64) + for _, block := range body.Blocks { + if _, ok := foundBlocks[block.Type]; !ok { + foundBlocks[block.Type] = 0 + } + + foundBlocks[block.Type]++ + } + + bodySchema := nodeSchema.(*schema.BodySchema) + for name, blockSchema := range bodySchema.Blocks { + if blockSchema.MaxItems != 0 { + foundBlocks, ok := foundBlocks[name] + if ok && foundBlocks > blockSchema.MaxItems { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Too many blocks specified for %q", name), + Detail: fmt.Sprintf("Only %d block(s) are expected for %q", blockSchema.MaxItems, name), + Subject: node.Range().Ptr(), + }) + } + } + } + + return +} diff --git a/decoder/internal/validator/block_min_items.go b/decoder/internal/validator/block_min_items.go new file mode 100644 index 00000000..fa0d1739 --- /dev/null +++ b/decoder/internal/validator/block_min_items.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type MinBlocks struct{} + +func (v MinBlocks) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + body, ok := node.(*hclsyntax.Body) + if !ok { + return + } + + if nodeSchema == nil { + return + } + + foundBlocks := make(map[string]uint64) + for _, block := range body.Blocks { + if _, ok := foundBlocks[block.Type]; !ok { + foundBlocks[block.Type] = 0 + } + + foundBlocks[block.Type]++ + } + + bodySchema := nodeSchema.(*schema.BodySchema) + for name, blockSchema := range bodySchema.Blocks { + if blockSchema.MinItems != 0 { + foundBlocks, ok := foundBlocks[name] + if !ok || foundBlocks < blockSchema.MinItems { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Too few blocks specified for %q", name), + Detail: fmt.Sprintf("At least %d block(s) are expected for %q", blockSchema.MinItems, name), + Subject: node.Range().Ptr(), + }) + } + } + } + + return +} diff --git a/decoder/internal/validator/block_unexpected.go b/decoder/internal/validator/block_unexpected.go new file mode 100644 index 00000000..3a7390d1 --- /dev/null +++ b/decoder/internal/validator/block_unexpected.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type UnexpectedBlock struct{} + +func (v UnexpectedBlock) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + block, ok := node.(*hclsyntax.Block) + if !ok { + return + } + + if nodeSchema == nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unexpected block", + Detail: fmt.Sprintf("Blocks of type %q are not expected here", block.Type), + Subject: block.TypeRange.Ptr(), + }) + } + return +} diff --git a/decoder/internal/validator/validators.go b/decoder/internal/validator/validators.go new file mode 100644 index 00000000..b205cd8a --- /dev/null +++ b/decoder/internal/validator/validators.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +import ( + "context" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type Validator interface { + Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) hcl.Diagnostics +} diff --git a/decoder/internal/walker/walker.go b/decoder/internal/walker/walker.go new file mode 100644 index 00000000..887d1a18 --- /dev/null +++ b/decoder/internal/walker/walker.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package walker + +import ( + "context" + + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type Walker interface { + Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) hcl.Diagnostics +} + +// Walk walks the given node while providing schema relevant to the node. +// +// This is similar to upstream hclsyntax.Walk() which does not make it possible +// to keep track of schema. +func Walk(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema, w Walker) hcl.Diagnostics { + var diags hcl.Diagnostics + + switch nodeType := node.(type) { + case *hclsyntax.Body: + diags = diags.Extend(w.Visit(ctx, node, nodeSchema)) + + bodySchema, ok := nodeSchema.(*schema.BodySchema) + if ok { + for _, attr := range nodeType.Attributes { + var attrSchema schema.Schema = nil + aSchema, ok := bodySchema.Attributes[attr.Name] + if ok { + attrSchema = aSchema + } else if bodySchema.AnyAttribute != nil { + attrSchema = bodySchema.AnyAttribute + } + + diags = diags.Extend(Walk(ctx, attr, attrSchema, w)) + } + + for _, block := range nodeType.Blocks { + var blockSchema schema.Schema = nil + bs, ok := bodySchema.Blocks[block.Type] + if ok { + blockSchema = bs + + // TODO: merge block body schemas + } + + diags = diags.Extend(Walk(ctx, block, blockSchema, w)) + } + } + + case *hclsyntax.Attribute: + diags = diags.Extend(w.Visit(ctx, node, nodeSchema)) + case *hclsyntax.Block: + diags = diags.Extend(w.Visit(ctx, node, nodeSchema)) + + var blockBodySchema schema.Schema = nil + bSchema, ok := nodeSchema.(*schema.BlockSchema) + if ok && bSchema.Body != nil { + blockBodySchema = bSchema.Body + } + + diags = diags.Extend(Walk(ctx, nodeType.Body, blockBodySchema, w)) + + // TODO: case hclsyntax.Expression + } + + return diags +} diff --git a/decoder/validate.go b/decoder/validate.go index f286d5d3..3172e74e 100644 --- a/decoder/validate.go +++ b/decoder/validate.go @@ -5,8 +5,9 @@ package decoder import ( "context" - "fmt" + "github.com/hashicorp/hcl-lang/decoder/internal/validator" + "github.com/hashicorp/hcl-lang/decoder/internal/walker" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -14,11 +15,22 @@ import ( // Validate returns a set of Diagnostics for all known files func (d *PathDecoder) Validate(ctx context.Context) (map[string]hcl.Diagnostics, error) { - diags := make(map[string]hcl.Diagnostics, 0) + diags := make(map[string]hcl.Diagnostics) if d.pathCtx.Schema == nil { return diags, &NoSchemaError{} } + builtinValidators := []validator.Validator{ + validator.BlockLabelsLength{}, + validator.DeprecatedAttribute{}, + validator.DeprecatedBlock{}, + validator.MaxBlocks{}, + validator.MinBlocks{}, + validator.MissingRequiredAttribute{}, + validator.UnexpectedAttribute{}, + validator.UnexpectedBlock{}, + } + // Validate module files per schema for filename, f := range d.pathCtx.Files { body, ok := f.Body.(*hclsyntax.Body) @@ -27,7 +39,9 @@ func (d *PathDecoder) Validate(ctx context.Context) (map[string]hcl.Diagnostics, continue } - diags[filename] = d.validateBody(ctx, body, d.pathCtx.Schema) + diags[filename] = walker.Walk(ctx, body, d.pathCtx.Schema, validationWalker{ + validators: builtinValidators, + }) } // Run validation functions @@ -38,210 +52,14 @@ func (d *PathDecoder) Validate(ctx context.Context) (map[string]hcl.Diagnostics, return diags, nil } -// validateBody returns a set of Diagnostics for a given HCL body -// -// Validations available: -// -// - unexpected attribute -// -// - missing required attribute -// -// - deprecated attribute -// -// - unexpected block -// -// - deprecated block -// -// - min blocks -// -// - max blocks -func (d *PathDecoder) validateBody(ctx context.Context, body *hclsyntax.Body, bodySchema *schema.BodySchema) hcl.Diagnostics { - diags := hcl.Diagnostics{} - - // Iterate over all Attributes in the body - for name, attribute := range body.Attributes { - attributeSchema, ok := bodySchema.Attributes[name] - if !ok { - if bodySchema.AnyAttribute == nil { - // ---------- diag ERR unknown attribute - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unexpected attribute", - Detail: fmt.Sprintf("An attribute named %q is not expected here", name), - Subject: attribute.SrcRange.Ptr(), - }) - // don't check futher because this isn't a valid attribute - continue - } - attributeSchema = bodySchema.AnyAttribute - } - - // ---------- diag WARN deprecated attribute - if attributeSchema.IsDeprecated { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: fmt.Sprintf("%q is deprecated", name), - Detail: fmt.Sprintf("Reason: %q", attributeSchema.Description.Value), - Subject: attribute.SrcRange.Ptr(), - }) - } - } - - // Iterate over all schema Attributes and check if specified in the configuration - for name, attribute := range bodySchema.Attributes { - if attribute.IsRequired { - _, ok := body.Attributes[name] - if !ok { - // ---------- diag ERR unknown attribute - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Required attribute %q not specified", name), - Detail: fmt.Sprintf("An attribute named %q is required here", name), - // TODO This is the closest I could think of - // maybe block instead ? - Subject: body.SrcRange.Ptr(), - }) - } - } - } - - // keep track of blocks actually used so we can compare to schema later - specifiedBlocks := make(map[string]int) - - // Iterate over all Blocks in the body - for _, block := range body.Blocks { - blockSchema, ok := bodySchema.Blocks[block.Type] - if !ok { - // ---------- diag ERR unknown block - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unexpected block", - Detail: fmt.Sprintf("Blocks of type %q are not expected here", block.Type), - Subject: block.TypeRange.Ptr(), - }) - // don't check futher because this isn't a valid block - continue - } - - // ---------- diag WARN deprecated block - if blockSchema.IsDeprecated { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: fmt.Sprintf("%q is deprecated", block.Type), - // todo check if description is there - Detail: fmt.Sprintf("Reason: %q", blockSchema.Description.Value), - Subject: &block.TypeRange, - }) - } - - // ---------- daig ERR extraneous block labels - validLabelNum := len(blockSchema.Labels) - for i := range block.Labels { - if i >= validLabelNum { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Too many labels specified for %q", block.Type), - Detail: fmt.Sprintf("Only %d label(s) are expected for %q blocks", validLabelNum, block.Type), - Subject: block.LabelRanges[i].Ptr(), - }) - } - } - - // ---------- diag ERR missing labels - if validLabelNum > len(block.Labels) { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Not enough labels specified for %q", block.Type), - Detail: fmt.Sprintf("All %q blocks must have %d label(s)", block.Type, validLabelNum), - Subject: block.TypeRange.Ptr(), - }) - } - - if block.Body != nil { - mergedSchema, err := mergeBlockBodySchemas(block.AsHCLBlock(), blockSchema) - if err != nil { - // TODO! err - } - - // Recurse for nested blocks - diags = diags.Extend(d.validateBody(ctx, block.Body, mergedSchema)) - } - - // build list of blocks specified - specifiedBlocks[block.Type]++ - } - - // Iterate over bodySchema Blocks and check if they are specified in configuration - for name, block := range bodySchema.Blocks { - // check if the bodySchema Block is specified in the configuration - numBlocks, ok := specifiedBlocks[name] - if ok { - // block is in schema and user specified it in configuration - // check if schema says there should be maximum number of items for this block - if block.MaxItems > 0 { - // ---------- diag ERR too many blocks - if numBlocks > int(block.MaxItems) { - subjectRange := &body.Blocks[block.Type].TypeRange - maxItems := block.MaxItems - diags = append(diags, tooManyBlocksDiag(diags, name, maxItems, subjectRange)) - } - } - - // check if schema says there should be minimum number of items for this block - if block.MinItems > 0 { - // ---------- diag ERR too little blocks - if numBlocks < int(block.MinItems) { - subjectRange := &body.Blocks[block.Type].TypeRange - minItems := block.MinItems - diags = append(diags, tooFewItemsDiag(diags, name, minItems, subjectRange)) - } - } - } else { - // block is in schema, but user did not specify it in configuration - // check if schema says there should be maximum number of items for this block - numBlocks = 0 - if block.MaxItems > 0 { - // ---------- diag ERR too many blocks - if numBlocks > int(block.MaxItems) { - // use current body range as there isn't a block to reference because - // the user didn't write anything here - subjectRange := &body.SrcRange - maxItems := block.MaxItems - diags = append(diags, tooManyBlocksDiag(diags, name, maxItems, subjectRange)) - } - } - - // check if schema says there should be minimum number of items for this block - if block.MinItems > 0 { - // ---------- diag ERR too little blocks - if numBlocks < int(block.MinItems) { - // use current body range as there isn't a block to reference because - // the user didn't write anything here - subjectRange := &body.SrcRange - minItems := block.MinItems - diags = append(diags, tooFewItemsDiag(diags, name, minItems, subjectRange)) - } - } - } - } - - return diags +type validationWalker struct { + validators []validator.Validator } -func tooFewItemsDiag(diags hcl.Diagnostics, name string, minItems uint64, subjectRange *hcl.Range) *hcl.Diagnostic { - return &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Too few blocks specified for %q", name), - Detail: fmt.Sprintf("At least %d block(s) are expected for %q", minItems, name), - Subject: subjectRange, +func (vw validationWalker) Visit(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema) (diags hcl.Diagnostics) { + for _, v := range vw.validators { + diags = append(diags, v.Visit(ctx, node, nodeSchema)...) } -} -func tooManyBlocksDiag(diags hcl.Diagnostics, name string, maxItems uint64, subjectRange *hcl.Range) *hcl.Diagnostic { - return &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Too many blocks specified for %q", name), - Detail: fmt.Sprintf("Only %d block(s) are expected for %q", maxItems, name), - Subject: subjectRange, - } + return } diff --git a/decoder/validate_test.go b/decoder/validate_test.go index 293bf94b..3b98d293 100644 --- a/decoder/validate_test.go +++ b/decoder/validate_test.go @@ -28,9 +28,7 @@ func TestValidate_schema(t *testing.T) { "empty schema", schema.NewBodySchema(), ``, - map[string]hcl.Diagnostics{ - "test.tf": {}, - }, + map[string]hcl.Diagnostics{}, }, { "valid schema", @@ -43,9 +41,7 @@ func TestValidate_schema(t *testing.T) { }, }, `test = 1`, - map[string]hcl.Diagnostics{ - "test.tf": {}, - }, + map[string]hcl.Diagnostics{}, }, // attributes { @@ -365,8 +361,8 @@ wakka = 2 Detail: "Only 1 block(s) are expected for \"bar\"", Subject: &hcl.Range{ Filename: "test.tf", - Start: hcl.Pos{Line: 2, Column: 5, Byte: 10}, - End: hcl.Pos{Line: 2, Column: 8, Byte: 13}, + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 6, Column: 5, Byte: 56}, }, }, }, @@ -408,8 +404,8 @@ wakka = 2 Detail: "At least 2 block(s) are expected for \"one\"", Subject: &hcl.Range{ Filename: "test.tf", - Start: hcl.Pos{Line: 2, Column: 5, Byte: 10}, - End: hcl.Pos{Line: 2, Column: 8, Byte: 13}, + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 5, Column: 5, Byte: 45}, }, }, }, @@ -484,9 +480,7 @@ wakka = 2 one {} test = 1 }`, - map[string]hcl.Diagnostics{ - "test.tf": {}, - }, + map[string]hcl.Diagnostics{}, }, { "min and max set on two different blocks with correct number", @@ -518,9 +512,7 @@ wakka = 2 two {} test = 1 }`, - map[string]hcl.Diagnostics{ - "test.tf": {}, - }, + map[string]hcl.Diagnostics{}, }, { "min and max set on two different blocks with incorrect number", @@ -560,8 +552,8 @@ wakka = 2 Detail: "At least 2 block(s) are expected for \"one\"", Subject: &hcl.Range{ Filename: "test.tf", - Start: hcl.Pos{Line: 2, Column: 5, Byte: 10}, - End: hcl.Pos{Line: 2, Column: 8, Byte: 13}, + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 6, Column: 5, Byte: 56}, }, }, &hcl.Diagnostic{ @@ -570,8 +562,8 @@ wakka = 2 Detail: "Only 1 block(s) are expected for \"two\"", Subject: &hcl.Range{ Filename: "test.tf", - Start: hcl.Pos{Line: 2, Column: 5, Byte: 10}, - End: hcl.Pos{Line: 2, Column: 8, Byte: 13}, + Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + End: hcl.Pos{Line: 6, Column: 5, Byte: 56}, }, }, }, @@ -602,9 +594,7 @@ wakka = 2 `foo { test = 1 }`, - map[string]hcl.Diagnostics{ - "test.tf": {}, - }, + map[string]hcl.Diagnostics{}, }, { "any attribute", @@ -623,9 +613,7 @@ wakka = 2 `foo { test = 1 }`, - map[string]hcl.Diagnostics{ - "test.tf": {}, - }, + map[string]hcl.Diagnostics{}, }, { "deprecated any attribute", @@ -678,15 +666,18 @@ wakka = 2 t.Fatal(err) } - sortedDiags := diags["test.tf"] - sort.Slice(sortedDiags, func(i, j int) bool { - return sortedDiags[i].Subject.Start.Byte < sortedDiags[j].Subject.Start.Byte || - sortedDiags[i].Summary < sortedDiags[j].Summary - }) + sortDiagnostics(diags["test.tf"]) - if diff := cmp.Diff(tc.expectedDiagnostics["test.tf"], sortedDiags); diff != "" { + if diff := cmp.Diff(tc.expectedDiagnostics["test.tf"], diags["test.tf"]); diff != "" { t.Fatalf("unexpected diagnostics: %s", diff) } }) } } + +func sortDiagnostics(diags hcl.Diagnostics) { + sort.Slice(diags, func(i, j int) bool { + return diags[i].Subject.Start.Byte < diags[j].Subject.Start.Byte || + diags[i].Summary < diags[j].Summary + }) +} From f0f3206e047e33e6bb3dea1a7af5ac0f7dd95e58 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 9 Aug 2023 10:02:54 +0200 Subject: [PATCH 2/3] decoder: Decouple schema & AST helpers into its own packages --- decoder/candidates.go | 3 +- decoder/decoder.go | 165 ------------------ decoder/extension_schemas.go | 57 ------ decoder/hover.go | 5 +- decoder/internal/ast/ast.go | 24 +++ decoder/internal/ast/decode_body.go | 71 ++++++++ decoder/internal/schemahelper/block_schema.go | 94 ++++++++++ .../schemahelper}/dependent_body.go | 5 +- .../schemahelper}/dependent_body_test.go | 2 +- .../internal/schemahelper/dynamic_block.go | 67 +++++++ decoder/links.go | 3 +- decoder/reference_origins.go | 6 +- decoder/reference_targets.go | 24 +-- decoder/semantic_tokens.go | 3 +- decoder/symbols.go | 6 +- 15 files changed, 290 insertions(+), 245 deletions(-) create mode 100644 decoder/internal/ast/ast.go create mode 100644 decoder/internal/ast/decode_body.go create mode 100644 decoder/internal/schemahelper/block_schema.go rename decoder/{ => internal/schemahelper}/dependent_body.go (95%) rename decoder/{ => internal/schemahelper}/dependent_body_test.go (99%) create mode 100644 decoder/internal/schemahelper/dynamic_block.go diff --git a/decoder/candidates.go b/decoder/candidates.go index 89bde6a3..aa54ba33 100644 --- a/decoder/candidates.go +++ b/decoder/candidates.go @@ -7,6 +7,7 @@ import ( "context" "fmt" + "github.com/hashicorp/hcl-lang/decoder/internal/schemahelper" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" @@ -142,7 +143,7 @@ func (d *PathDecoder) candidatesAtPos(ctx context.Context, body *hclsyntax.Body, } if block.Body != nil && block.Body.Range().ContainsPos(pos) { - mergedSchema, err := mergeBlockBodySchemas(block.AsHCLBlock(), blockSchema) + mergedSchema, err := schemahelper.MergeBlockBodySchemas(block.AsHCLBlock(), blockSchema) if err != nil { return lang.ZeroCandidates(), err } diff --git a/decoder/decoder.go b/decoder/decoder.go index 2e625678..48b699f4 100644 --- a/decoder/decoder.go +++ b/decoder/decoder.go @@ -6,9 +6,7 @@ package decoder import ( "fmt" - "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" ) type Decoder struct { @@ -32,169 +30,6 @@ func posEqual(pos, other hcl.Pos) bool { pos.Byte == other.Byte } -func mergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*schema.BodySchema, error) { - mergedSchema := &schema.BodySchema{} - if blockSchema.Body != nil { - mergedSchema = blockSchema.Body.Copy() - } - if mergedSchema.Attributes == nil { - mergedSchema.Attributes = make(map[string]*schema.AttributeSchema, 0) - } - if mergedSchema.Blocks == nil { - mergedSchema.Blocks = make(map[string]*schema.BlockSchema, 0) - } - if mergedSchema.TargetableAs == nil { - mergedSchema.TargetableAs = make([]*schema.Targetable, 0) - } - if mergedSchema.ImpliedOrigins == nil { - mergedSchema.ImpliedOrigins = make([]schema.ImpliedOrigin, 0) - } - - depSchema, _, ok := NewBlockSchema(blockSchema).DependentBodySchema(block) - if ok { - for name, attr := range depSchema.Attributes { - if _, exists := mergedSchema.Attributes[name]; !exists { - mergedSchema.Attributes[name] = attr - } else { - // Skip duplicate attribute - continue - } - } - for bType, block := range depSchema.Blocks { - if _, exists := mergedSchema.Blocks[bType]; !exists { - // propagate DynamicBlocks extension to any nested blocks - if mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks { - if block.Body.Extensions == nil { - block.Body.Extensions = &schema.BodyExtensions{} - } - block.Body.Extensions.DynamicBlocks = true - } - - mergedSchema.Blocks[bType] = block - } else { - // Skip duplicate block type - continue - } - } - - if mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks && len(depSchema.Blocks) > 0 { - mergedSchema.Blocks["dynamic"] = buildDynamicBlockSchema(depSchema) - } - - mergedSchema.TargetableAs = append(mergedSchema.TargetableAs, depSchema.TargetableAs...) - mergedSchema.ImpliedOrigins = append(mergedSchema.ImpliedOrigins, depSchema.ImpliedOrigins...) - - // TODO: avoid resetting? - mergedSchema.Targets = depSchema.Targets.Copy() - - // TODO: avoid resetting? - mergedSchema.DocsLink = depSchema.DocsLink.Copy() - - // use extensions of DependentBody if not nil - // (to avoid resetting to nil) - if depSchema.Extensions != nil { - mergedSchema.Extensions = depSchema.Extensions.Copy() - } - } else if !ok && mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks && len(mergedSchema.Blocks) > 0 { - // dynamic blocks are only relevant for dependent schemas, - // but we may end up here because the schema is a result - // of merged static + dependent schema from previous iteration - - // propagate DynamicBlocks extension to any nested blocks - if mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks { - for bType, block := range mergedSchema.Blocks { - if block.Body.Extensions == nil { - block.Body.Extensions = &schema.BodyExtensions{} - } - block.Body.Extensions.DynamicBlocks = true - mergedSchema.Blocks[bType] = block - } - } - - mergedSchema.Blocks["dynamic"] = buildDynamicBlockSchema(mergedSchema) - } - - return mergedSchema, nil -} - -// blockContent represents HCL or JSON block content -type blockContent struct { - *hcl.Block - - // Range represents range of the block in HCL syntax - // or closest available representative range in JSON - Range hcl.Range -} - -// bodyContent represents an HCL or JSON body content -type bodyContent struct { - Attributes hcl.Attributes - Blocks []*blockContent - RangePtr *hcl.Range -} - -// decodeBody produces content of either HCL or JSON body -// -// JSON body requires schema for decoding, empty bodyContent -// is returned if nil schema is provided -func decodeBody(body hcl.Body, bodySchema *schema.BodySchema) bodyContent { - content := bodyContent{ - Attributes: make(hcl.Attributes, 0), - Blocks: make([]*blockContent, 0), - } - - // More common HCL syntax is processed directly (without schema) - // which also better represents the reality in symbol lookups - // i.e. expressions written as opposed to schema requirements - if hclBody, ok := body.(*hclsyntax.Body); ok { - for name, attr := range hclBody.Attributes { - content.Attributes[name] = attr.AsHCLAttribute() - } - - for _, block := range hclBody.Blocks { - content.Blocks = append(content.Blocks, &blockContent{ - Block: block.AsHCLBlock(), - Range: block.Range(), - }) - } - - content.RangePtr = hclBody.Range().Ptr() - - return content - } - - // JSON syntax cannot be decoded without schema as attributes - // and blocks are otherwise ambiguous - if bodySchema != nil { - hclSchema := bodySchema.ToHCLSchema() - bContent, remainingBody, _ := body.PartialContent(hclSchema) - - content.Attributes = bContent.Attributes - if bodySchema.AnyAttribute != nil { - // Remaining unknown fields may also be blocks in JSON, - // but we blindly treat them as attributes here - // as we cannot do any better without upstream HCL changes. - remainingAttrs, _ := remainingBody.JustAttributes() - for name, attr := range remainingAttrs { - content.Attributes[name] = attr - } - } - - for _, block := range bContent.Blocks { - // hcl.Block interface (as the only way of accessing block in JSON) - // does not come with Range for the block, so we calculate it here - rng := hcl.RangeBetween(block.DefRange, block.Body.MissingItemRange()) - - content.Blocks = append(content.Blocks, &blockContent{ - Block: block, - Range: rng, - }) - } - } - - return content -} - func stringPos(pos hcl.Pos) string { return fmt.Sprintf("%d,%d", pos.Line, pos.Column) } diff --git a/decoder/extension_schemas.go b/decoder/extension_schemas.go index f497f2da..69ff8fc7 100644 --- a/decoder/extension_schemas.go +++ b/decoder/extension_schemas.go @@ -32,63 +32,6 @@ func forEachAttributeSchema() *schema.AttributeSchema { } } -func buildDynamicBlockSchema(inputSchema *schema.BodySchema) *schema.BlockSchema { - dependentBody := make(map[schema.SchemaKey]*schema.BodySchema) - for blockName, block := range inputSchema.Blocks { - dependentBody[schema.NewSchemaKey(schema.DependencyKeys{ - Labels: []schema.LabelDependent{ - {Index: 0, Value: blockName}, - }, - })] = &schema.BodySchema{ - Blocks: map[string]*schema.BlockSchema{ - "content": { - Description: lang.PlainText("The body of each generated block"), - MaxItems: 1, - Body: block.Body.Copy(), - }, - }, - } - } - - return &schema.BlockSchema{ - Description: lang.Markdown("A dynamic block to produce blocks dynamically by iterating over a given complex value"), - Labels: []*schema.LabelSchema{ - { - Name: "name", - Completable: true, - IsDepKey: true, - }, - }, - Body: &schema.BodySchema{ - Attributes: map[string]*schema.AttributeSchema{ - "for_each": { - Constraint: schema.OneOf{ - schema.AnyExpression{OfType: cty.Map(cty.DynamicPseudoType)}, - schema.AnyExpression{OfType: cty.Set(cty.String)}, - }, - IsRequired: true, - Description: lang.Markdown("A meta-argument that accepts a map or a set of strings, and creates an instance for each item in that map or set."), - }, - "iterator": { - Constraint: schema.LiteralType{Type: cty.String}, - IsOptional: true, - Description: lang.Markdown("The name of a temporary variable that represents the current " + - "element of the complex value. Defaults to the label of the dynamic block."), - }, - "labels": { - Constraint: schema.AnyExpression{ - OfType: cty.List(cty.String), - }, - IsOptional: true, - Description: lang.Markdown("A list of strings that specifies the block labels, " + - "in order, to use for each generated block."), - }, - }, - }, - DependentBody: dependentBody, - } -} - func countIndexReferenceTarget(attr *hcl.Attribute, bodyRange hcl.Range) reference.Target { return reference.Target{ LocalAddr: lang.Address{ diff --git a/decoder/hover.go b/decoder/hover.go index 0a5936e2..118e441d 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -9,6 +9,7 @@ import ( "sort" "strings" + "github.com/hashicorp/hcl-lang/decoder/internal/schemahelper" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" @@ -143,7 +144,7 @@ func (d *PathDecoder) hoverAtPos(ctx context.Context, body *hclsyntax.Body, body } if block.Body != nil && block.Body.Range().ContainsPos(pos) { - mergedSchema, err := mergeBlockBodySchemas(block.AsHCLBlock(), blockSchema) + mergedSchema, err := schemahelper.MergeBlockBodySchemas(block.AsHCLBlock(), blockSchema) if err != nil { return nil, err } @@ -166,7 +167,7 @@ func (d *PathDecoder) hoverContentForLabel(i int, block *hclsyntax.Block, bSchem labelSchema := bSchema.Labels[i] if labelSchema.IsDepKey { - bs, _, ok := NewBlockSchema(bSchema).DependentBodySchema(block.AsHCLBlock()) + bs, _, ok := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(block.AsHCLBlock()) if ok { content := fmt.Sprintf("`%s`", value) if bs.Detail != "" { diff --git a/decoder/internal/ast/ast.go b/decoder/internal/ast/ast.go new file mode 100644 index 00000000..3282b1b9 --- /dev/null +++ b/decoder/internal/ast/ast.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ast + +import ( + "github.com/hashicorp/hcl/v2" +) + +// blockContent represents HCL or JSON block content +type BlockContent struct { + *hcl.Block + + // Range represents range of the block in HCL syntax + // or closest available representative range in JSON + Range hcl.Range +} + +// bodyContent represents an HCL or JSON body content +type BodyContent struct { + Attributes hcl.Attributes + Blocks []*BlockContent + RangePtr *hcl.Range +} diff --git a/decoder/internal/ast/decode_body.go b/decoder/internal/ast/decode_body.go new file mode 100644 index 00000000..1232f3f3 --- /dev/null +++ b/decoder/internal/ast/decode_body.go @@ -0,0 +1,71 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ast + +import ( + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +// DecodeBody produces content of either HCL or JSON body +// JSON body requires schema for decoding, empty bodyContent +// is returned if nil schema is provided +func DecodeBody(body hcl.Body, bodySchema *schema.BodySchema) BodyContent { + content := BodyContent{ + Attributes: make(hcl.Attributes, 0), + Blocks: make([]*BlockContent, 0), + } + + // More common HCL syntax is processed directly (without schema) + // which also better represents the reality in symbol lookups + // i.e. expressions written as opposed to schema requirements + if hclBody, ok := body.(*hclsyntax.Body); ok { + for name, attr := range hclBody.Attributes { + content.Attributes[name] = attr.AsHCLAttribute() + } + + for _, block := range hclBody.Blocks { + content.Blocks = append(content.Blocks, &BlockContent{ + Block: block.AsHCLBlock(), + Range: block.Range(), + }) + } + + content.RangePtr = hclBody.Range().Ptr() + + return content + } + + // JSON syntax cannot be decoded without schema as attributes + // and blocks are otherwise ambiguous + if bodySchema != nil { + hclSchema := bodySchema.ToHCLSchema() + bContent, remainingBody, _ := body.PartialContent(hclSchema) + + content.Attributes = bContent.Attributes + if bodySchema.AnyAttribute != nil { + // Remaining unknown fields may also be blocks in JSON, + // but we blindly treat them as attributes here + // as we cannot do any better without upstream HCL changes. + remainingAttrs, _ := remainingBody.JustAttributes() + for name, attr := range remainingAttrs { + content.Attributes[name] = attr + } + } + + for _, block := range bContent.Blocks { + // hcl.Block interface (as the only way of accessing block in JSON) + // does not come with Range for the block, so we calculate it here + rng := hcl.RangeBetween(block.DefRange, block.Body.MissingItemRange()) + + content.Blocks = append(content.Blocks, &BlockContent{ + Block: block, + Range: rng, + }) + } + } + + return content +} diff --git a/decoder/internal/schemahelper/block_schema.go b/decoder/internal/schemahelper/block_schema.go new file mode 100644 index 00000000..873beb87 --- /dev/null +++ b/decoder/internal/schemahelper/block_schema.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schemahelper + +import ( + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" +) + +func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*schema.BodySchema, error) { + mergedSchema := &schema.BodySchema{} + if blockSchema.Body != nil { + mergedSchema = blockSchema.Body.Copy() + } + if mergedSchema.Attributes == nil { + mergedSchema.Attributes = make(map[string]*schema.AttributeSchema, 0) + } + if mergedSchema.Blocks == nil { + mergedSchema.Blocks = make(map[string]*schema.BlockSchema, 0) + } + if mergedSchema.TargetableAs == nil { + mergedSchema.TargetableAs = make([]*schema.Targetable, 0) + } + if mergedSchema.ImpliedOrigins == nil { + mergedSchema.ImpliedOrigins = make([]schema.ImpliedOrigin, 0) + } + + depSchema, _, ok := NewBlockSchema(blockSchema).DependentBodySchema(block) + if ok { + for name, attr := range depSchema.Attributes { + if _, exists := mergedSchema.Attributes[name]; !exists { + mergedSchema.Attributes[name] = attr + } else { + // Skip duplicate attribute + continue + } + } + for bType, block := range depSchema.Blocks { + if _, exists := mergedSchema.Blocks[bType]; !exists { + // propagate DynamicBlocks extension to any nested blocks + if mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks { + if block.Body.Extensions == nil { + block.Body.Extensions = &schema.BodyExtensions{} + } + block.Body.Extensions.DynamicBlocks = true + } + + mergedSchema.Blocks[bType] = block + } else { + // Skip duplicate block type + continue + } + } + + if mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks && len(depSchema.Blocks) > 0 { + mergedSchema.Blocks["dynamic"] = buildDynamicBlockSchema(depSchema) + } + + mergedSchema.TargetableAs = append(mergedSchema.TargetableAs, depSchema.TargetableAs...) + mergedSchema.ImpliedOrigins = append(mergedSchema.ImpliedOrigins, depSchema.ImpliedOrigins...) + + // TODO: avoid resetting? + mergedSchema.Targets = depSchema.Targets.Copy() + + // TODO: avoid resetting? + mergedSchema.DocsLink = depSchema.DocsLink.Copy() + + // use extensions of DependentBody if not nil + // (to avoid resetting to nil) + if depSchema.Extensions != nil { + mergedSchema.Extensions = depSchema.Extensions.Copy() + } + } else if !ok && mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks && len(mergedSchema.Blocks) > 0 { + // dynamic blocks are only relevant for dependent schemas, + // but we may end up here because the schema is a result + // of merged static + dependent schema from previous iteration + + // propagate DynamicBlocks extension to any nested blocks + if mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks { + for bType, block := range mergedSchema.Blocks { + if block.Body.Extensions == nil { + block.Body.Extensions = &schema.BodyExtensions{} + } + block.Body.Extensions.DynamicBlocks = true + mergedSchema.Blocks[bType] = block + } + } + + mergedSchema.Blocks["dynamic"] = buildDynamicBlockSchema(mergedSchema) + } + + return mergedSchema, nil +} diff --git a/decoder/dependent_body.go b/decoder/internal/schemahelper/dependent_body.go similarity index 95% rename from decoder/dependent_body.go rename to decoder/internal/schemahelper/dependent_body.go index 095c7505..3451f8b7 100644 --- a/decoder/dependent_body.go +++ b/decoder/internal/schemahelper/dependent_body.go @@ -1,9 +1,10 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package decoder +package schemahelper import ( + "github.com/hashicorp/hcl-lang/decoder/internal/ast" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" @@ -79,7 +80,7 @@ func dependencyKeysFromBlock(block *hcl.Block, blockSchema blockSchema) schema.D return dk } - content := decodeBody(block.Body, blockSchema.Body) + content := ast.DecodeBody(block.Body, blockSchema.Body) for name, attrSchema := range blockSchema.Body.Attributes { if attrSchema.IsDepKey { diff --git a/decoder/dependent_body_test.go b/decoder/internal/schemahelper/dependent_body_test.go similarity index 99% rename from decoder/dependent_body_test.go rename to decoder/internal/schemahelper/dependent_body_test.go index c4b88c49..6667a48a 100644 --- a/decoder/dependent_body_test.go +++ b/decoder/internal/schemahelper/dependent_body_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package decoder +package schemahelper import ( "fmt" diff --git a/decoder/internal/schemahelper/dynamic_block.go b/decoder/internal/schemahelper/dynamic_block.go new file mode 100644 index 00000000..9648c77f --- /dev/null +++ b/decoder/internal/schemahelper/dynamic_block.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schemahelper + +import ( + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/zclconf/go-cty/cty" +) + +func buildDynamicBlockSchema(inputSchema *schema.BodySchema) *schema.BlockSchema { + dependentBody := make(map[schema.SchemaKey]*schema.BodySchema) + for blockName, block := range inputSchema.Blocks { + dependentBody[schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: blockName}, + }, + })] = &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "content": { + Description: lang.PlainText("The body of each generated block"), + MaxItems: 1, + Body: block.Body.Copy(), + }, + }, + } + } + + return &schema.BlockSchema{ + Description: lang.Markdown("A dynamic block to produce blocks dynamically by iterating over a given complex value"), + Labels: []*schema.LabelSchema{ + { + Name: "name", + Completable: true, + IsDepKey: true, + }, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "for_each": { + Constraint: schema.OneOf{ + schema.AnyExpression{OfType: cty.Map(cty.DynamicPseudoType)}, + schema.AnyExpression{OfType: cty.Set(cty.String)}, + }, + IsRequired: true, + Description: lang.Markdown("A meta-argument that accepts a map or a set of strings, and creates an instance for each item in that map or set."), + }, + "iterator": { + Constraint: schema.LiteralType{Type: cty.String}, + IsOptional: true, + Description: lang.Markdown("The name of a temporary variable that represents the current " + + "element of the complex value. Defaults to the label of the dynamic block."), + }, + "labels": { + Constraint: schema.AnyExpression{ + OfType: cty.List(cty.String), + }, + IsOptional: true, + Description: lang.Markdown("A list of strings that specifies the block labels, " + + "in order, to use for each generated block."), + }, + }, + }, + DependentBody: dependentBody, + } +} diff --git a/decoder/links.go b/decoder/links.go index d2e49067..c031a69d 100644 --- a/decoder/links.go +++ b/decoder/links.go @@ -6,6 +6,7 @@ package decoder import ( "net/url" + "github.com/hashicorp/hcl-lang/decoder/internal/schemahelper" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" @@ -45,7 +46,7 @@ func (d *PathDecoder) linksInBody(body *hclsyntax.Body, bodySchema *schema.BodyS // Currently only block bodies have links associated if block.Body != nil { - depSchema, dk, ok := NewBlockSchema(blockSchema).DependentBodySchema(block.AsHCLBlock()) + depSchema, dk, ok := schemahelper.NewBlockSchema(blockSchema).DependentBodySchema(block.AsHCLBlock()) if ok && depSchema.DocsLink != nil { link := depSchema.DocsLink u, err := d.docsURL(link.URL, "documentLink") diff --git a/decoder/reference_origins.go b/decoder/reference_origins.go index cc2f06ce..920c2521 100644 --- a/decoder/reference_origins.go +++ b/decoder/reference_origins.go @@ -7,6 +7,8 @@ import ( "context" "sort" + "github.com/hashicorp/hcl-lang/decoder/internal/ast" + "github.com/hashicorp/hcl-lang/decoder/internal/schemahelper" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" @@ -124,7 +126,7 @@ func (d *PathDecoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.B ctx := context.Background() impliedOrigins = append(impliedOrigins, bodySchema.ImpliedOrigins...) - content := decodeBody(body, bodySchema) + content := ast.DecodeBody(body, bodySchema) for _, attr := range content.Attributes { var aSchema *schema.AttributeSchema @@ -190,7 +192,7 @@ func (d *PathDecoder) referenceOriginsInBody(body hcl.Body, bodySchema *schema.B // skip unknown blocks continue } - mergedSchema, err := mergeBlockBodySchemas(block.Block, bSchema) + mergedSchema, err := schemahelper.MergeBlockBodySchemas(block.Block, bSchema) if err != nil { continue } diff --git a/decoder/reference_targets.go b/decoder/reference_targets.go index 4ff1bec0..7d7cb740 100644 --- a/decoder/reference_targets.go +++ b/decoder/reference_targets.go @@ -8,6 +8,8 @@ import ( "context" "sort" + "github.com/hashicorp/hcl-lang/decoder/internal/ast" + "github.com/hashicorp/hcl-lang/decoder/internal/schemahelper" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" @@ -101,14 +103,14 @@ func (d *PathDecoder) CollectReferenceTargets() (reference.Targets, error) { return refs, nil } -func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock *blockContent, bodySchema *schema.BodySchema) reference.Targets { +func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock *ast.BlockContent, bodySchema *schema.BodySchema) reference.Targets { refs := make(reference.Targets, 0) if bodySchema == nil { return reference.Targets{} } - content := decodeBody(body, bodySchema) + content := ast.DecodeBody(body, bodySchema) for _, attr := range content.Attributes { if bodySchema.Extensions != nil { @@ -140,7 +142,7 @@ func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock * continue } - mergedSchema, err := mergeBlockBodySchemas(blk.Block, bSchema) + mergedSchema, err := schemahelper.MergeBlockBodySchemas(blk.Block, bSchema) if err != nil { mergedSchema = bSchema.Body } @@ -203,11 +205,11 @@ func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock * } } - depSchema, _, ok := NewBlockSchema(bSchema).DependentBodySchema(blk.Block) + depSchema, _, ok := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(blk.Block) if ok { fullSchema := depSchema if bSchema.Address.BodyAsData { - mergedSchema, err := mergeBlockBodySchemas(blk.Block, bSchema) + mergedSchema, err := schemahelper.MergeBlockBodySchemas(blk.Block, bSchema) if err != nil { continue } @@ -249,7 +251,7 @@ func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock * return refs } -func decodeTargetableBody(body hcl.Body, parentBlock *blockContent, tt *schema.Targetable) reference.Target { +func decodeTargetableBody(body hcl.Body, parentBlock *ast.BlockContent, tt *schema.Targetable) reference.Target { target := reference.Target{ Addr: tt.Address.Copy(), ScopeId: tt.ScopeId, @@ -709,7 +711,7 @@ func bodySchemaAsAttrTypes(bodySchema *schema.BodySchema) map[string]cty.Type { func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, bAddrSchema *schema.BlockAddrSchema, body hcl.Body, bodySchema *schema.BodySchema, selfRefBodyRangePtr *hcl.Range, selfRefAddr lang.Address) reference.Targets { refs := make(reference.Targets, 0) - content := decodeBody(body, bodySchema) + content := ast.DecodeBody(body, bodySchema) // We don't get body range for JSON here // TODO? calculate or implement upstream if bAddrSchema.DependentBodySelfRef && content.RangePtr != nil && selfRefBodyRangePtr == nil { @@ -1025,7 +1027,7 @@ func (d *PathDecoder) collectInferredReferenceTargetsForBody(addr lang.Address, type blockCollection struct { Schema *schema.BlockSchema - Blocks []*blockContent + Blocks []*ast.BlockContent } type blockTypes map[string]*blockCollection @@ -1043,7 +1045,7 @@ func (bt blockTypes) OfSchemaType(t schema.BlockType) blockTypes { func blocksTypesWithSchema(body hcl.Body, bodySchema *schema.BodySchema) blockTypes { blockTypes := make(blockTypes, 0) - content := decodeBody(body, bodySchema) + content := ast.DecodeBody(body, bodySchema) for _, block := range content.Blocks { bSchema, ok := bodySchema.Blocks[block.Type] @@ -1056,7 +1058,7 @@ func blocksTypesWithSchema(body hcl.Body, bodySchema *schema.BodySchema) blockTy if !ok { blockTypes[block.Type] = &blockCollection{ Schema: bSchema, - Blocks: make([]*blockContent, 0), + Blocks: make([]*ast.BlockContent, 0), } } @@ -1144,7 +1146,7 @@ func resolveBlockAddress(block *hcl.Block, blockSchema *schema.BlockSchema) (lan } stepName = block.Labels[step.Index] case schema.AttrValueStep: - content := decodeBody(block.Body, blockSchema.Body) + content := ast.DecodeBody(block.Body, blockSchema.Body) attr, ok := content.Attributes[step.Name] if !ok && step.IsOptional { diff --git a/decoder/semantic_tokens.go b/decoder/semantic_tokens.go index e9a901a3..7ffc0c0e 100644 --- a/decoder/semantic_tokens.go +++ b/decoder/semantic_tokens.go @@ -7,6 +7,7 @@ import ( "context" "sort" + "github.com/hashicorp/hcl-lang/decoder/internal/schemahelper" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" @@ -126,7 +127,7 @@ func (d *PathDecoder) tokensForBody(ctx context.Context, body *hclsyntax.Body, b } if block.Body != nil { - mergedSchema, err := mergeBlockBodySchemas(block.AsHCLBlock(), blockSchema) + mergedSchema, err := schemahelper.MergeBlockBodySchemas(block.AsHCLBlock(), blockSchema) if err != nil { continue } diff --git a/decoder/symbols.go b/decoder/symbols.go index 0632c83a..2bc64247 100644 --- a/decoder/symbols.go +++ b/decoder/symbols.go @@ -9,6 +9,8 @@ import ( "sort" "strings" + "github.com/hashicorp/hcl-lang/decoder/internal/ast" + "github.com/hashicorp/hcl-lang/decoder/internal/schemahelper" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" @@ -96,7 +98,7 @@ func (d *PathDecoder) symbolsForBody(body hcl.Body, bodySchema *schema.BodySchem return symbols } - content := decodeBody(body, bodySchema) + content := ast.DecodeBody(body, bodySchema) for name, attr := range content.Attributes { symbols = append(symbols, &AttributeSymbol{ @@ -114,7 +116,7 @@ func (d *PathDecoder) symbolsForBody(body hcl.Body, bodySchema *schema.BodySchem bs, ok := bodySchema.Blocks[block.Type] if ok { bSchema = bs.Body - mergedSchema, err := mergeBlockBodySchemas(block.Block, bs) + mergedSchema, err := schemahelper.MergeBlockBodySchemas(block.Block, bs) if err == nil { bSchema = mergedSchema } From e60c85bc66a9cd1d8a046373850bd07743f7a9d0 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 9 Aug 2023 10:03:24 +0200 Subject: [PATCH 3/3] decoder/internal/walker: Merge schemas before walking block bodies --- decoder/internal/walker/walker.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/decoder/internal/walker/walker.go b/decoder/internal/walker/walker.go index 887d1a18..89027a4b 100644 --- a/decoder/internal/walker/walker.go +++ b/decoder/internal/walker/walker.go @@ -6,6 +6,7 @@ package walker import ( "context" + "github.com/hashicorp/hcl-lang/decoder/internal/schemahelper" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -45,8 +46,6 @@ func Walk(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema, w bs, ok := bodySchema.Blocks[block.Type] if ok { blockSchema = bs - - // TODO: merge block body schemas } diags = diags.Extend(Walk(ctx, block, blockSchema, w)) @@ -61,7 +60,11 @@ func Walk(ctx context.Context, node hclsyntax.Node, nodeSchema schema.Schema, w var blockBodySchema schema.Schema = nil bSchema, ok := nodeSchema.(*schema.BlockSchema) if ok && bSchema.Body != nil { - blockBodySchema = bSchema.Body + mergedSchema, err := schemahelper.MergeBlockBodySchemas(nodeType.AsHCLBlock(), bSchema) + if err != nil { + // TODO! err + } + blockBodySchema = mergedSchema } diags = diags.Extend(Walk(ctx, nodeType.Body, blockBodySchema, w))