diff --git a/cmd/hcldec/spec.go b/cmd/hcldec/spec.go index b62f05b..c8b90d3 100644 --- a/cmd/hcldec/spec.go +++ b/cmd/hcldec/spec.go @@ -394,6 +394,15 @@ func decodeBlockMapSpec(body hcl.Body, impliedName string) (hcldec.Spec, hcl.Dia return errSpec, diags } + if hcldec.ImpliedType(spec).HasDynamicTypes() { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid block_map spec", + Detail: "A block_map spec may not contain attributes with type 'any'.", + Subject: body.MissingItemRange().Ptr(), + }) + } + return spec, diags } diff --git a/hcldec/public_test.go b/hcldec/public_test.go index 05ed17a..c85afee 100644 --- a/hcldec/public_test.go +++ b/hcldec/public_test.go @@ -426,6 +426,49 @@ b {} }, { ` +b { + a = true +} +b { + a = 1 +} +`, + &BlockListSpec{ + TypeName: "b", + Nested: &AttrSpec{ + Name: "a", + Type: cty.DynamicPseudoType, + }, + }, + nil, + cty.DynamicVal, + 1, // Unconsistent argument types in b blocks + }, + { + ` +b { + a = true +} +b { + a = "not a bool" +} +`, + &BlockListSpec{ + TypeName: "b", + Nested: &AttrSpec{ + Name: "a", + Type: cty.DynamicPseudoType, + }, + }, + nil, + cty.ListVal([]cty.Value{ + cty.StringVal("true"), // type unification generalizes all the values to strings + cty.StringVal("not a bool"), + }), + 0, + }, + { + ` b {} b {} `, @@ -465,6 +508,49 @@ b "bar" "baz" {} }, { ` +b { + a = true +} +b { + a = 1 +} +`, + &BlockSetSpec{ + TypeName: "b", + Nested: &AttrSpec{ + Name: "a", + Type: cty.DynamicPseudoType, + }, + }, + nil, + cty.DynamicVal, + 1, // Unconsistent argument types in b blocks + }, + { + ` +b { + a = true +} +b { + a = "not a bool" +} +`, + &BlockSetSpec{ + TypeName: "b", + Nested: &AttrSpec{ + Name: "a", + Type: cty.DynamicPseudoType, + }, + }, + nil, + cty.SetVal([]cty.Value{ + cty.StringVal("true"), // type unification generalizes all the values to strings + cty.StringVal("not a bool"), + }), + 0, + }, + { + ` b "foo" {} b "bar" {} `, diff --git a/hcldec/spec.go b/hcldec/spec.go index 0bb3f3e..b59b3c0 100644 --- a/hcldec/spec.go +++ b/hcldec/spec.go @@ -478,6 +478,44 @@ func (s *BlockListSpec) decode(content *hcl.BodyContent, blockLabels []blockLabe if len(elems) == 0 { ret = cty.ListValEmpty(s.Nested.impliedType()) } else { + // Since our target is a list, all of the decoded elements must have the + // same type or cty.ListVal will panic below. Different types can arise + // if there is an attribute spec of type cty.DynamicPseudoType in the + // nested spec; all given values must be convertable to a single type + // in order for the result to be considered valid. + etys := make([]cty.Type, len(elems)) + for i, v := range elems { + etys[i] = v.Type() + } + ety, convs := convert.UnifyUnsafe(etys) + if ety == cty.NilType { + // FIXME: This is a pretty terrible error message. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName), + Detail: "Corresponding attributes in all blocks of this type must be the same.", + Subject: &sourceRanges[0], + }) + return cty.DynamicVal, diags + } + for i, v := range elems { + if convs[i] != nil { + newV, err := convs[i](v) + if err != nil { + // FIXME: This is a pretty terrible error message. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName), + Detail: fmt.Sprintf("Block with index %d has inconsistent argument types: %s.", i, err), + Subject: &sourceRanges[i], + }) + // Bail early here so we won't panic below in cty.ListVal + return cty.DynamicVal, diags + } + elems[i] = newV + } + } + ret = cty.ListVal(elems) } @@ -593,6 +631,44 @@ func (s *BlockSetSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel if len(elems) == 0 { ret = cty.SetValEmpty(s.Nested.impliedType()) } else { + // Since our target is a set, all of the decoded elements must have the + // same type or cty.SetVal will panic below. Different types can arise + // if there is an attribute spec of type cty.DynamicPseudoType in the + // nested spec; all given values must be convertable to a single type + // in order for the result to be considered valid. + etys := make([]cty.Type, len(elems)) + for i, v := range elems { + etys[i] = v.Type() + } + ety, convs := convert.UnifyUnsafe(etys) + if ety == cty.NilType { + // FIXME: This is a pretty terrible error message. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName), + Detail: "Corresponding attributes in all blocks of this type must be the same.", + Subject: &sourceRanges[0], + }) + return cty.DynamicVal, diags + } + for i, v := range elems { + if convs[i] != nil { + newV, err := convs[i](v) + if err != nil { + // FIXME: This is a pretty terrible error message. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Unconsistent argument types in %s blocks", s.TypeName), + Detail: fmt.Sprintf("Block with index %d has inconsistent argument types: %s.", i, err), + Subject: &sourceRanges[i], + }) + // Bail early here so we won't panic below in cty.ListVal + return cty.DynamicVal, diags + } + elems[i] = newV + } + } + ret = cty.SetVal(elems) } @@ -675,6 +751,9 @@ func (s *BlockMapSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel if s.Nested == nil { panic("BlockSetSpec with no Nested Spec") } + if ImpliedType(s).HasDynamicTypes() { + panic("cty.DynamicPseudoType attributes may not be used inside a BlockMapSpec") + } elems := map[string]interface{}{} for _, childBlock := range content.Blocks {