diff --git a/decoder/hover.go b/decoder/hover.go index b47d26b2..8076c500 100644 --- a/decoder/hover.go +++ b/decoder/hover.go @@ -163,8 +163,8 @@ func (d *PathDecoder) hoverContentForLabel(i int, block *hclsyntax.Block, bSchem labelSchema := bSchema.Labels[i] if labelSchema.IsDepKey { - bs, _, ok := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(block.AsHCLBlock()) - if ok { + bs, _, result := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(block.AsHCLBlock()) + if result == schemahelper.LookupSuccessful || result == schemahelper.LookupPartiallySuccessful { content := fmt.Sprintf("`%s`", value) if bs.Detail != "" { content += " " + bs.Detail diff --git a/decoder/internal/schemahelper/block_schema.go b/decoder/internal/schemahelper/block_schema.go index 66681775..3323c5fe 100644 --- a/decoder/internal/schemahelper/block_schema.go +++ b/decoder/internal/schemahelper/block_schema.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/hcl/v2" ) -func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*schema.BodySchema, bool) { +func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (*schema.BodySchema, LookupResult) { mergedSchema := &schema.BodySchema{} if blockSchema.Body != nil { mergedSchema = blockSchema.Body.Copy() @@ -26,8 +26,8 @@ func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (* mergedSchema.ImpliedOrigins = make([]schema.ImpliedOrigin, 0) } - depSchema, depKeys, ok := NewBlockSchema(blockSchema).DependentBodySchema(block) - if ok { + depSchema, _, result := NewBlockSchema(blockSchema).DependentBodySchema(block) + if result == LookupSuccessful || result == LookupPartiallySuccessful { for name, attr := range depSchema.Attributes { if _, exists := mergedSchema.Attributes[name]; !exists { mergedSchema.Attributes[name] = attr @@ -71,7 +71,7 @@ func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (* if depSchema.Extensions != nil { mergedSchema.Extensions = depSchema.Extensions.Copy() } - } else if !ok && mergedSchema.Extensions != nil && mergedSchema.Extensions.DynamicBlocks && len(mergedSchema.Blocks) > 0 { + } else if (result == LookupFailed || result == NoDependentKeys) && 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 @@ -90,10 +90,5 @@ func MergeBlockBodySchemas(block *hcl.Block, blockSchema *schema.BlockSchema) (* mergedSchema.Blocks["dynamic"] = buildDynamicBlockSchema(mergedSchema) } - expectedDepBody := len(depKeys.Labels) > 0 || len(depKeys.Attributes) > 0 - - // report success either if there wasn't any dependent body merging to do - // or if the merging was successful - - return mergedSchema, !expectedDepBody || ok + return mergedSchema, result } diff --git a/decoder/internal/schemahelper/dependent_body.go b/decoder/internal/schemahelper/dependent_body.go index 371e6ffc..8462898d 100644 --- a/decoder/internal/schemahelper/dependent_body.go +++ b/decoder/internal/schemahelper/dependent_body.go @@ -12,6 +12,15 @@ import ( "github.com/zclconf/go-cty/cty" ) +type LookupResult int + +const ( + LookupFailed LookupResult = iota + LookupSuccessful + LookupPartiallySuccessful + NoDependentKeys +) + type blockSchema struct { *schema.BlockSchema seenNestedDepKeys bool @@ -23,16 +32,23 @@ func NewBlockSchema(bs *schema.BlockSchema) blockSchema { // DependentBodySchema finds relevant BodySchema based on dependency keys // such as a label or an attribute (or combination of both). -func (bs blockSchema) DependentBodySchema(block *hcl.Block) (*schema.BodySchema, schema.DependencyKeys, bool) { +func (bs blockSchema) DependentBodySchema(block *hcl.Block) (*schema.BodySchema, schema.DependencyKeys, LookupResult) { + result := LookupFailed + dks := dependencyKeysFromBlock(block, bs) b, err := dks.MarshalJSON() if err != nil { - return nil, schema.DependencyKeys{}, false + return nil, schema.DependencyKeys{}, result + } + + if len(dks.Labels) == 0 && len(dks.Attributes) == 0 { + return bs.Body, schema.DependencyKeys{}, NoDependentKeys } key := schema.SchemaKey(string(b)) depBodySchema, ok := bs.DependentBody[key] if ok { + result = LookupSuccessful hasDepKeys := false for _, attr := range depBodySchema.Attributes { if attr.IsDepKey { @@ -44,13 +60,17 @@ func (bs blockSchema) DependentBodySchema(block *hcl.Block) (*schema.BodySchema, mergedBlockSchema := NewBlockSchema(bs.Copy()) mergedBlockSchema.seenNestedDepKeys = true mergedBlockSchema.Body = depBodySchema - if depBodySchema, dks, ok := mergedBlockSchema.DependentBodySchema(block); ok { - return depBodySchema, dks, ok + if depBodySchema, dks, nestedOk := mergedBlockSchema.DependentBodySchema(block); nestedOk == LookupSuccessful { + return depBodySchema, dks, LookupSuccessful + } else { + // Ensure we report lookup failure overall if we couldn't + // lookup nested dependent body + result = LookupPartiallySuccessful } } } - return depBodySchema, dks, ok + return depBodySchema, dks, result } func dependencyKeysFromBlock(block *hcl.Block, blockSchema blockSchema) schema.DependencyKeys { diff --git a/decoder/internal/schemahelper/dependent_body_test.go b/decoder/internal/schemahelper/dependent_body_test.go index f104e686..5e8c3947 100644 --- a/decoder/internal/schemahelper/dependent_body_test.go +++ b/decoder/internal/schemahelper/dependent_body_test.go @@ -52,8 +52,8 @@ func TestBodySchema_DependentBodySchema_label_basic(t *testing.T) { }, } - bodySchema, _, ok := NewBlockSchema(bSchema).DependentBodySchema(block) - if !ok { + bodySchema, _, result := NewBlockSchema(bSchema).DependentBodySchema(block) + if result != LookupSuccessful { t.Fatal("expected to find body schema for 'theircloud' label") } expectedSchema := &schema.BodySchema{ @@ -104,8 +104,8 @@ func TestBodySchema_DependentBodySchema_mismatchingLabels(t *testing.T) { }, } - _, _, ok := NewBlockSchema(bSchema).DependentBodySchema(block) - if ok { + _, _, result := NewBlockSchema(bSchema).DependentBodySchema(block) + if result != LookupFailed { t.Fatal("expected to not find body schema for mismatching label schema") } } @@ -170,17 +170,17 @@ func TestBodySchema_DependentBodySchema_dependentAttr(t *testing.T) { }, } - bodySchema, _, ok := NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{ + bodySchema, _, result := NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{ Labels: []string{"remote_state"}, }) - if !ok { + if result != LookupSuccessful { t.Fatal("expected to find body schema for nested dependent schema") } if diff := cmp.Diff(firstDepBody, bodySchema, ctydebug.CmpOptions); diff != "" { t.Fatalf("mismatching body schema: %s", diff) } - bodySchema, _, ok = NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{ + bodySchema, _, result = NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{ Labels: []string{"remote_state"}, Body: &hclsyntax.Body{ Attributes: hclsyntax.Attributes{ @@ -193,7 +193,7 @@ func TestBodySchema_DependentBodySchema_dependentAttr(t *testing.T) { }, }, }) - if !ok { + if result != LookupSuccessful { t.Fatal("expected to find body schema for nested dependent schema") } if diff := cmp.Diff(secondDepBody, bodySchema, ctydebug.CmpOptions); diff != "" { @@ -256,7 +256,7 @@ func TestBodySchema_DependentBodySchema_missingDependentAttr(t *testing.T) { }, } - bodySchema, _, ok := NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{ + bodySchema, _, result := NewBlockSchema(bSchema).DependentBodySchema(&hcl.Block{ Labels: []string{"remote_state"}, Body: &hclsyntax.Body{ Attributes: hclsyntax.Attributes{ @@ -267,8 +267,8 @@ func TestBodySchema_DependentBodySchema_missingDependentAttr(t *testing.T) { }, }, }) - if !ok { - t.Fatal("expected to find first body schema for missing keys") + if result != LookupPartiallySuccessful { + t.Fatalf("expected to find first body schema for missing keys; reported: %q", result) } if diff := cmp.Diff(firstDepBody, bodySchema, ctydebug.CmpOptions); diff != "" { t.Fatalf("mismatching body schema: %s", diff) @@ -418,8 +418,8 @@ func TestBodySchema_DependentBodySchema_attributes(t *testing.T) { Attributes: tc.attributes, }, } - bodySchema, _, ok := tc.schema.DependentBodySchema(block) - if !ok { + bodySchema, _, result := tc.schema.DependentBodySchema(block) + if result != LookupSuccessful { t.Fatalf("expected to find body schema for given block with %d attributes", len(tc.attributes)) } @@ -430,13 +430,86 @@ func TestBodySchema_DependentBodySchema_attributes(t *testing.T) { } } +func TestBodySchema_DependentBodySchema_partialMergeFailure(t *testing.T) { + testSchema := NewBlockSchema(&schema.BlockSchema{ + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + }, + }, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "count": { + Constraint: schema.AnyExpression{OfType: cty.Number}, + }, + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + { + Index: 0, + Value: "terraform_remote_state", + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "first": { + Constraint: schema.LiteralType{Type: cty.String}, + }, + "backend": { + Constraint: schema.AnyExpression{OfType: cty.String}, + IsDepKey: true, + }, + }, + }, + schema.NewSchemaKey(schema.DependencyKeys{ + Attributes: []schema.AttributeDependent{ + { + Name: "backend", + Expr: schema.ExpressionValue{ + Static: cty.StringVal("remote"), + }, + }, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "second": {Constraint: schema.LiteralType{Type: cty.String}}, + }, + }, + }, + }) + + block := &hcl.Block{ + Labels: []string{"terraform_remote_state"}, + Body: &hclsyntax.Body{ + Attributes: map[string]*hclsyntax.Attribute{ + "backend": { + Name: "backend", + Expr: &hclsyntax.ScopeTraversalExpr{ + Traversal: hcl.Traversal{ + hcl.TraverseRoot{Name: "referencestep"}, + }, + }, + }, + }, + }, + } + + _, result := MergeBlockBodySchemas(block, testSchema.BlockSchema) + if result != LookupPartiallySuccessful { + t.Fatal("expected partially failed dependent body lookup to fail") + } +} + func TestBodySchema_DependentBodySchema_label_notFound(t *testing.T) { block := &hcl.Block{ Labels: []string{"test", "mycloud"}, Body: hcl.EmptyBody(), } - _, _, ok := testSchemaWithLabels.DependentBodySchema(block) - if ok { + _, _, result := testSchemaWithLabels.DependentBodySchema(block) + if result != LookupFailed { t.Fatal("expected not to find body schema for 'mycloud' 2nd label") } } @@ -446,8 +519,8 @@ func TestBodySchema_DependentBodySchema_label_storedUnsorted(t *testing.T) { Labels: []string{"complexcloud", "pumpkin"}, Body: hcl.EmptyBody(), } - bodySchema, _, ok := testSchemaWithLabels.DependentBodySchema(block) - if !ok { + bodySchema, _, result := testSchemaWithLabels.DependentBodySchema(block) + if result != LookupSuccessful { t.Fatal("expected to find body schema stored with unsorted keys") } expectedSchema := &schema.BodySchema{ @@ -465,8 +538,8 @@ func TestBodySchema_DependentBodySchema_label_lookupUnsorted(t *testing.T) { Labels: []string{"apple", "crazycloud"}, Body: hcl.EmptyBody(), } - _, _, ok := testSchemaWithLabels.DependentBodySchema(block) - if ok { + _, _, result := testSchemaWithLabels.DependentBodySchema(block) + if result != LookupFailed { t.Fatal("expected to not find body schema based on wrongly sorted labels") } } diff --git a/decoder/internal/walker/walker.go b/decoder/internal/walker/walker.go index a90bbb42..16fdcad2 100644 --- a/decoder/internal/walker/walker.go +++ b/decoder/internal/walker/walker.go @@ -88,8 +88,8 @@ 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 { - mergedSchema, ok := schemahelper.MergeBlockBodySchemas(nodeType.AsHCLBlock(), bSchema) - if !ok { + mergedSchema, result := schemahelper.MergeBlockBodySchemas(nodeType.AsHCLBlock(), bSchema) + if result == schemahelper.LookupFailed || result == schemahelper.LookupPartiallySuccessful { ctx = schemacontext.WithUnknownSchema(ctx) } blockBodySchema = mergedSchema diff --git a/decoder/links.go b/decoder/links.go index c031a69d..1af9b752 100644 --- a/decoder/links.go +++ b/decoder/links.go @@ -46,8 +46,8 @@ func (d *PathDecoder) linksInBody(body *hclsyntax.Body, bodySchema *schema.BodyS // Currently only block bodies have links associated if block.Body != nil { - depSchema, dk, ok := schemahelper.NewBlockSchema(blockSchema).DependentBodySchema(block.AsHCLBlock()) - if ok && depSchema.DocsLink != nil { + depSchema, dk, result := schemahelper.NewBlockSchema(blockSchema).DependentBodySchema(block.AsHCLBlock()) + if (result == schemahelper.LookupSuccessful || result == schemahelper.LookupPartiallySuccessful || result == schemahelper.NoDependentKeys) && depSchema.DocsLink != nil { link := depSchema.DocsLink u, err := d.docsURL(link.URL, "documentLink") if err != nil { diff --git a/decoder/reference_targets.go b/decoder/reference_targets.go index 3c2853d8..0a1a5f6f 100644 --- a/decoder/reference_targets.go +++ b/decoder/reference_targets.go @@ -201,8 +201,8 @@ func (d *PathDecoder) decodeReferenceTargetsForBody(body hcl.Body, parentBlock * } } - depSchema, _, ok := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(blk.Block) - if ok { + depSchema, _, result := schemahelper.NewBlockSchema(bSchema).DependentBodySchema(blk.Block) + if result == schemahelper.LookupSuccessful { fullSchema := depSchema if bSchema.Address.BodyAsData { mergedSchema, _ := schemahelper.MergeBlockBodySchemas(blk.Block, bSchema)