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

gohcl: allow gohcl to parse hcl.Range objects for blocks and attributes #703

Merged
merged 2 commits into from
Nov 19, 2024
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
39 changes: 32 additions & 7 deletions gohcl/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import (

"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/gocty"

"github.com/hashicorp/hcl/v2"
)

// DecodeBody extracts the configuration within the given body into the given
Expand Down Expand Up @@ -110,14 +111,26 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value)
}

// As a special case, if the target is of type hcl.Expression then
// we'll assign an actual expression that evalues to a cty null,
// we'll assign an actual expression that evaluates to a cty null,
// so the caller can deal with it within the cty realm rather
// than within the Go realm.
synthExpr := hcl.StaticExpr(cty.NullVal(cty.DynamicPseudoType), body.MissingItemRange())
fieldV.Set(reflect.ValueOf(synthExpr))
continue
}

if attrRange, exists := tags.AttributeRange[name]; exists {
val.Field(attrRange).Set(reflect.ValueOf(attr.Range))
}

if attrNameRange, exists := tags.AttributeNameRange[name]; exists {
val.Field(attrNameRange).Set(reflect.ValueOf(attr.NameRange))
}

if attrValueRange, exists := tags.AttributeValueRange[name]; exists {
val.Field(attrValueRange).Set(reflect.ValueOf(attr.Expr.Range()))
}

switch {
case attrType.AssignableTo(field.Type):
fieldV.Set(reflect.ValueOf(attr))
Expand Down Expand Up @@ -263,14 +276,26 @@ func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.D
func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
diags := decodeBodyToValue(block.Body, ctx, v)

if len(block.Labels) > 0 {
blockTags := getFieldTags(v.Type())
for li, lv := range block.Labels {
lfieldIdx := blockTags.Labels[li].FieldIndex
v.Field(lfieldIdx).Set(reflect.ValueOf(lv))
blockTags := getFieldTags(v.Type())
for li, lv := range block.Labels {
lfieldIdx := blockTags.Labels[li].FieldIndex
lfieldName := blockTags.Labels[li].Name

v.Field(lfieldIdx).Set(reflect.ValueOf(lv))

if ix, exists := blockTags.LabelRange[lfieldName]; exists {
v.Field(ix).Set(reflect.ValueOf(block.LabelRanges[li]))
}
}

if blockTags.TypeRange != nil {
v.Field(*blockTags.TypeRange).Set(reflect.ValueOf(block.TypeRange))
}

if blockTags.DefRange != nil {
v.Field(*blockTags.DefRange).Set(reflect.ValueOf(block.DefRange))
}

return diags
}

Expand Down
89 changes: 88 additions & 1 deletion gohcl/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
"testing"

"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/hcl/v2"
hclJSON "github.com/hashicorp/hcl/v2/json"
"github.com/zclconf/go-cty/cty"
)

func TestDecodeBody(t *testing.T) {
Expand Down Expand Up @@ -681,6 +682,76 @@ func TestDecodeBody(t *testing.T) {
},
0,
},
{
map[string]interface{}{
"foo": map[string]interface{}{
"foo_type": map[string]interface{}{
"foo_name": map[string]interface{}{
"value": "foo",
},
},
},
},
makeInstantiateType(struct {
Foo struct {
Type string `hcl:"type,label"`
TypeLabelRange hcl.Range `hcl:"type,label_range"`
Name string `hcl:"name,label"`
NameLabelRange hcl.Range `hcl:"name,label_range"`

DefRange hcl.Range `hcl:",def_range"`
TypeRange hcl.Range `hcl:",type_range"`

Attribute string `hcl:"value,attr"`
AttributeRange hcl.Range `hcl:"value,attr_range"`
AttributeNameRange hcl.Range `hcl:"value,attr_name_range"`
AttributeValueRange hcl.Range `hcl:"value,attr_value_range"`
} `hcl:"foo,block"`
}{}),
deepEquals(struct {
Foo struct {
Type string `hcl:"type,label"`
TypeLabelRange hcl.Range `hcl:"type,label_range"`
Name string `hcl:"name,label"`
NameLabelRange hcl.Range `hcl:"name,label_range"`

DefRange hcl.Range `hcl:",def_range"`
TypeRange hcl.Range `hcl:",type_range"`

Attribute string `hcl:"value,attr"`
AttributeRange hcl.Range `hcl:"value,attr_range"`
AttributeNameRange hcl.Range `hcl:"value,attr_name_range"`
AttributeValueRange hcl.Range `hcl:"value,attr_value_range"`
} `hcl:"foo,block"`
}{
Foo: struct {
Type string `hcl:"type,label"`
TypeLabelRange hcl.Range `hcl:"type,label_range"`
Name string `hcl:"name,label"`
NameLabelRange hcl.Range `hcl:"name,label_range"`

DefRange hcl.Range `hcl:",def_range"`
TypeRange hcl.Range `hcl:",type_range"`

Attribute string `hcl:"value,attr"`
AttributeRange hcl.Range `hcl:"value,attr_range"`
AttributeNameRange hcl.Range `hcl:"value,attr_name_range"`
AttributeValueRange hcl.Range `hcl:"value,attr_value_range"`
}{
Type: "foo_type",
TypeLabelRange: makeRange("test.json", 1, 9, 19),
Name: "foo_name",
NameLabelRange: makeRange("test.json", 1, 21, 31),
DefRange: makeRange("test.json", 1, 32, 33),
TypeRange: makeRange("test.json", 1, 2, 7),
Attribute: "foo",
AttributeRange: makeRange("test.json", 1, 33, 46),
AttributeNameRange: makeRange("test.json", 1, 33, 40),
AttributeValueRange: makeRange("test.json", 1, 41, 46),
},
}),
0,
},
}

for i, test := range tests {
Expand Down Expand Up @@ -811,3 +882,19 @@ func makeInstantiateType(target interface{}) func() interface{} {
return reflect.New(reflect.TypeOf(target)).Interface()
}
}

func makeRange(filename string, line int, start, end int) hcl.Range {
return hcl.Range{
Filename: filename,
Start: hcl.Pos{
Line: line,
Column: start,
Byte: start - 1,
},
End: hcl.Pos{
Line: line,
Column: end,
Byte: end - 1,
},
}
}
46 changes: 38 additions & 8 deletions gohcl/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@
// A struct field tag scheme is used, similar to other decoding and
// unmarshalling libraries. The tags are formatted as in the following example:
//
// ThingType string `hcl:"thing_type,attr"`
// ThingType string `hcl:"thing_type,attr"`
//
// Within each tag there are two comma-separated tokens. The first is the
// name of the corresponding construct in configuration, while the second
// is a keyword giving the kind of construct expected. The following
// kind keywords are supported:
//
// attr (the default) indicates that the value is to be populated from an attribute
// block indicates that the value is to populated from a block
// label indicates that the value is to populated from a block label
// optional is the same as attr, but the field is optional
// remain indicates that the value is to be populated from the remaining body after populating other fields
// attr (the default) indicates that the value is to be populated from an attribute
// block indicates that the value is to populated from a block
// label indicates that the value is to populated from a block label
// optional is the same as attr, but the field is optional
// remain indicates that the value is to be populated from the remaining body after populating other fields
//
// "attr" fields may either be of type *hcl.Expression, in which case the raw
// expression is assigned, or of any type accepted by gocty, in which case
Expand All @@ -40,8 +40,9 @@
//
// "label" fields are considered only in a struct used as the type of a field
// marked as "block", and are used sequentially to capture the labels of
// the blocks being decoded. In this case, the name token is used only as
// an identifier for the label in diagnostic messages.
// the blocks being decoded. In this case, the name token is used (a) as
// an identifier for the label in diagnostic messages and (b) to match the
// which with the equivalent "label_range" field (if it exists).
//
// "optional" fields behave like "attr" fields, but they are optional
// and will not give parsing errors if they are missing.
Expand All @@ -52,6 +53,35 @@
// present then any attributes or blocks not matched by another valid tag
// will cause an error diagnostic.
//
// "def_range" can be placed on a single field that must be of type hcl.Range.
// This field is only considered in a struct used as the type of a field marked
// as "block", and is used to capture the range of the block's definition.
//
// "type_range" can be placed on a single field that must be of type hcl.Range.
// This field is only considered in a struct used as the type of a field marked
// as "block", and is used to capture the range of the block's type label.
//
// "label_range" can be placed on multiple fields that must be of type
// hcl.Range. This field is only considered in a struct used as the type of a
// field marked as "block", and is used to capture the range of the block's
// labels. The name token is used to match with the equivalent "label" field
// that this range will specify.
//
// "attr_range" can be placed on multiple fields that must be of type hcl.Range.
// This field will be assigned the complete hcl.Range for the attribute with
// the corresponding name. The name token is used to match with the name of the
// attribute that this range will specify.
//
// "attr_name_range" can be placed on multiple fields that must be of type
// hcl.Range. This field will be assigned the hcl.Range for the name of the
// attribute with the corresponding name. The name token is used to match with
// the name of the attribute that this range will specify.
//
// "attr_value_range" can be placed on multiple fields that must be of type
// hcl.Range. This field will be assigned the hcl.Range for the value of the
// attribute with the corresponding name. The name token is used to match with
// the name of the attribute that this range will specify.
//
// Only a subset of this tagging/typing vocabulary is supported for the
// "Encode" family of functions. See the EncodeIntoBody docs for full details
// on the constraints there.
Expand Down
39 changes: 36 additions & 3 deletions gohcl/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,31 @@ type fieldTags struct {
Remain *int
Body *int
Optional map[string]bool

AttributeRange map[string]int
AttributeNameRange map[string]int
AttributeValueRange map[string]int

DefRange *int
TypeRange *int
LabelRange map[string]int
}

type labelField struct {
FieldIndex int
RangeIndex int
Name string
}

func getFieldTags(ty reflect.Type) *fieldTags {
ret := &fieldTags{
Attributes: map[string]int{},
Blocks: map[string]int{},
Optional: map[string]bool{},
Attributes: map[string]int{},
Blocks: map[string]int{},
Optional: map[string]bool{},
AttributeRange: map[string]int{},
AttributeNameRange: map[string]int{},
AttributeValueRange: map[string]int{},
LabelRange: map[string]int{},
}

ct := ty.NumField()
Expand Down Expand Up @@ -175,6 +188,26 @@ func getFieldTags(ty reflect.Type) *fieldTags {
case "optional":
ret.Attributes[name] = i
ret.Optional[name] = true
case "def_range":
if ret.DefRange != nil {
panic("only one 'def_range' tag is permitted")
}
idx := i // copy, because this loop will continue assigning to i
ret.DefRange = &idx
case "type_range":
if ret.TypeRange != nil {
panic("only one 'type_range' tag is permitted")
}
idx := i // copy, because this loop will continue assigning to i
ret.TypeRange = &idx
case "label_range":
ret.LabelRange[name] = i
case "attr_range":
ret.AttributeRange[name] = i
case "attr_name_range":
ret.AttributeNameRange[name] = i
case "attr_value_range":
ret.AttributeValueRange[name] = i
default:
panic(fmt.Sprintf("invalid hcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
}
Expand Down