From ed65f98aedff51e7aaf3fb0de9de0d53d3bc9142 Mon Sep 17 00:00:00 2001 From: Liam Cervante Date: Thu, 24 Oct 2024 17:43:18 +0200 Subject: [PATCH] gohcl: allow gohcl to parse hcl.Range objects for blocks and attributes --- gohcl/decode.go | 39 +++++++++++++++---- gohcl/decode_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++- gohcl/doc.go | 46 +++++++++++++++++++---- gohcl/schema.go | 39 +++++++++++++++++-- 4 files changed, 194 insertions(+), 19 deletions(-) diff --git a/gohcl/decode.go b/gohcl/decode.go index 2d1776a3..67c5142e 100644 --- a/gohcl/decode.go +++ b/gohcl/decode.go @@ -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 @@ -110,7 +111,7 @@ 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()) @@ -118,6 +119,18 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) 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)) @@ -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 } diff --git a/gohcl/decode_test.go b/gohcl/decode_test.go index 1ac5d049..a3e0c8f2 100644 --- a/gohcl/decode_test.go +++ b/gohcl/decode_test.go @@ -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) { @@ -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 { @@ -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, + }, + } +} diff --git a/gohcl/doc.go b/gohcl/doc.go index cfec2530..73b7045a 100644 --- a/gohcl/doc.go +++ b/gohcl/doc.go @@ -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 @@ -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. @@ -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. diff --git a/gohcl/schema.go b/gohcl/schema.go index 0cdca271..d0c18881 100644 --- a/gohcl/schema.go +++ b/gohcl/schema.go @@ -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() @@ -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)) }