Skip to content

Commit a369057

Browse files
authored
gohcl: allow gohcl to parse hcl.Range objects for blocks and attributes (#703)
1 parent 638d0fd commit a369057

File tree

4 files changed

+194
-19
lines changed

4 files changed

+194
-19
lines changed

gohcl/decode.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import (
99

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

12-
"github.com/hashicorp/hcl/v2"
1312
"github.com/zclconf/go-cty/cty/convert"
1413
"github.com/zclconf/go-cty/cty/gocty"
14+
15+
"github.com/hashicorp/hcl/v2"
1516
)
1617

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

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

122+
if attrRange, exists := tags.AttributeRange[name]; exists {
123+
val.Field(attrRange).Set(reflect.ValueOf(attr.Range))
124+
}
125+
126+
if attrNameRange, exists := tags.AttributeNameRange[name]; exists {
127+
val.Field(attrNameRange).Set(reflect.ValueOf(attr.NameRange))
128+
}
129+
130+
if attrValueRange, exists := tags.AttributeValueRange[name]; exists {
131+
val.Field(attrValueRange).Set(reflect.ValueOf(attr.Expr.Range()))
132+
}
133+
121134
switch {
122135
case attrType.AssignableTo(field.Type):
123136
fieldV.Set(reflect.ValueOf(attr))
@@ -263,14 +276,26 @@ func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.D
263276
func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
264277
diags := decodeBodyToValue(block.Body, ctx, v)
265278

266-
if len(block.Labels) > 0 {
267-
blockTags := getFieldTags(v.Type())
268-
for li, lv := range block.Labels {
269-
lfieldIdx := blockTags.Labels[li].FieldIndex
270-
v.Field(lfieldIdx).Set(reflect.ValueOf(lv))
279+
blockTags := getFieldTags(v.Type())
280+
for li, lv := range block.Labels {
281+
lfieldIdx := blockTags.Labels[li].FieldIndex
282+
lfieldName := blockTags.Labels[li].Name
283+
284+
v.Field(lfieldIdx).Set(reflect.ValueOf(lv))
285+
286+
if ix, exists := blockTags.LabelRange[lfieldName]; exists {
287+
v.Field(ix).Set(reflect.ValueOf(block.LabelRanges[li]))
271288
}
272289
}
273290

291+
if blockTags.TypeRange != nil {
292+
v.Field(*blockTags.TypeRange).Set(reflect.ValueOf(block.TypeRange))
293+
}
294+
295+
if blockTags.DefRange != nil {
296+
v.Field(*blockTags.DefRange).Set(reflect.ValueOf(block.DefRange))
297+
}
298+
274299
return diags
275300
}
276301

gohcl/decode_test.go

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
"testing"
1111

1212
"github.com/davecgh/go-spew/spew"
13+
"github.com/zclconf/go-cty/cty"
14+
1315
"github.com/hashicorp/hcl/v2"
1416
hclJSON "github.com/hashicorp/hcl/v2/json"
15-
"github.com/zclconf/go-cty/cty"
1617
)
1718

1819
func TestDecodeBody(t *testing.T) {
@@ -681,6 +682,76 @@ func TestDecodeBody(t *testing.T) {
681682
},
682683
0,
683684
},
685+
{
686+
map[string]interface{}{
687+
"foo": map[string]interface{}{
688+
"foo_type": map[string]interface{}{
689+
"foo_name": map[string]interface{}{
690+
"value": "foo",
691+
},
692+
},
693+
},
694+
},
695+
makeInstantiateType(struct {
696+
Foo struct {
697+
Type string `hcl:"type,label"`
698+
TypeLabelRange hcl.Range `hcl:"type,label_range"`
699+
Name string `hcl:"name,label"`
700+
NameLabelRange hcl.Range `hcl:"name,label_range"`
701+
702+
DefRange hcl.Range `hcl:",def_range"`
703+
TypeRange hcl.Range `hcl:",type_range"`
704+
705+
Attribute string `hcl:"value,attr"`
706+
AttributeRange hcl.Range `hcl:"value,attr_range"`
707+
AttributeNameRange hcl.Range `hcl:"value,attr_name_range"`
708+
AttributeValueRange hcl.Range `hcl:"value,attr_value_range"`
709+
} `hcl:"foo,block"`
710+
}{}),
711+
deepEquals(struct {
712+
Foo struct {
713+
Type string `hcl:"type,label"`
714+
TypeLabelRange hcl.Range `hcl:"type,label_range"`
715+
Name string `hcl:"name,label"`
716+
NameLabelRange hcl.Range `hcl:"name,label_range"`
717+
718+
DefRange hcl.Range `hcl:",def_range"`
719+
TypeRange hcl.Range `hcl:",type_range"`
720+
721+
Attribute string `hcl:"value,attr"`
722+
AttributeRange hcl.Range `hcl:"value,attr_range"`
723+
AttributeNameRange hcl.Range `hcl:"value,attr_name_range"`
724+
AttributeValueRange hcl.Range `hcl:"value,attr_value_range"`
725+
} `hcl:"foo,block"`
726+
}{
727+
Foo: struct {
728+
Type string `hcl:"type,label"`
729+
TypeLabelRange hcl.Range `hcl:"type,label_range"`
730+
Name string `hcl:"name,label"`
731+
NameLabelRange hcl.Range `hcl:"name,label_range"`
732+
733+
DefRange hcl.Range `hcl:",def_range"`
734+
TypeRange hcl.Range `hcl:",type_range"`
735+
736+
Attribute string `hcl:"value,attr"`
737+
AttributeRange hcl.Range `hcl:"value,attr_range"`
738+
AttributeNameRange hcl.Range `hcl:"value,attr_name_range"`
739+
AttributeValueRange hcl.Range `hcl:"value,attr_value_range"`
740+
}{
741+
Type: "foo_type",
742+
TypeLabelRange: makeRange("test.json", 1, 9, 19),
743+
Name: "foo_name",
744+
NameLabelRange: makeRange("test.json", 1, 21, 31),
745+
DefRange: makeRange("test.json", 1, 32, 33),
746+
TypeRange: makeRange("test.json", 1, 2, 7),
747+
Attribute: "foo",
748+
AttributeRange: makeRange("test.json", 1, 33, 46),
749+
AttributeNameRange: makeRange("test.json", 1, 33, 40),
750+
AttributeValueRange: makeRange("test.json", 1, 41, 46),
751+
},
752+
}),
753+
0,
754+
},
684755
}
685756

686757
for i, test := range tests {
@@ -811,3 +882,19 @@ func makeInstantiateType(target interface{}) func() interface{} {
811882
return reflect.New(reflect.TypeOf(target)).Interface()
812883
}
813884
}
885+
886+
func makeRange(filename string, line int, start, end int) hcl.Range {
887+
return hcl.Range{
888+
Filename: filename,
889+
Start: hcl.Pos{
890+
Line: line,
891+
Column: start,
892+
Byte: start - 1,
893+
},
894+
End: hcl.Pos{
895+
Line: line,
896+
Column: end,
897+
Byte: end - 1,
898+
},
899+
}
900+
}

gohcl/doc.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@
1010
// A struct field tag scheme is used, similar to other decoding and
1111
// unmarshalling libraries. The tags are formatted as in the following example:
1212
//
13-
// ThingType string `hcl:"thing_type,attr"`
13+
// ThingType string `hcl:"thing_type,attr"`
1414
//
1515
// Within each tag there are two comma-separated tokens. The first is the
1616
// name of the corresponding construct in configuration, while the second
1717
// is a keyword giving the kind of construct expected. The following
1818
// kind keywords are supported:
1919
//
20-
// attr (the default) indicates that the value is to be populated from an attribute
21-
// block indicates that the value is to populated from a block
22-
// label indicates that the value is to populated from a block label
23-
// optional is the same as attr, but the field is optional
24-
// remain indicates that the value is to be populated from the remaining body after populating other fields
20+
// attr (the default) indicates that the value is to be populated from an attribute
21+
// block indicates that the value is to populated from a block
22+
// label indicates that the value is to populated from a block label
23+
// optional is the same as attr, but the field is optional
24+
// remain indicates that the value is to be populated from the remaining body after populating other fields
2525
//
2626
// "attr" fields may either be of type *hcl.Expression, in which case the raw
2727
// expression is assigned, or of any type accepted by gocty, in which case
@@ -40,8 +40,9 @@
4040
//
4141
// "label" fields are considered only in a struct used as the type of a field
4242
// marked as "block", and are used sequentially to capture the labels of
43-
// the blocks being decoded. In this case, the name token is used only as
44-
// an identifier for the label in diagnostic messages.
43+
// the blocks being decoded. In this case, the name token is used (a) as
44+
// an identifier for the label in diagnostic messages and (b) to match the
45+
// which with the equivalent "label_range" field (if it exists).
4546
//
4647
// "optional" fields behave like "attr" fields, but they are optional
4748
// and will not give parsing errors if they are missing.
@@ -52,6 +53,35 @@
5253
// present then any attributes or blocks not matched by another valid tag
5354
// will cause an error diagnostic.
5455
//
56+
// "def_range" can be placed on a single field that must be of type hcl.Range.
57+
// This field is only considered in a struct used as the type of a field marked
58+
// as "block", and is used to capture the range of the block's definition.
59+
//
60+
// "type_range" can be placed on a single field that must be of type hcl.Range.
61+
// This field is only considered in a struct used as the type of a field marked
62+
// as "block", and is used to capture the range of the block's type label.
63+
//
64+
// "label_range" can be placed on multiple fields that must be of type
65+
// hcl.Range. This field is only considered in a struct used as the type of a
66+
// field marked as "block", and is used to capture the range of the block's
67+
// labels. The name token is used to match with the equivalent "label" field
68+
// that this range will specify.
69+
//
70+
// "attr_range" can be placed on multiple fields that must be of type hcl.Range.
71+
// This field will be assigned the complete hcl.Range for the attribute with
72+
// the corresponding name. The name token is used to match with the name of the
73+
// attribute that this range will specify.
74+
//
75+
// "attr_name_range" can be placed on multiple fields that must be of type
76+
// hcl.Range. This field will be assigned the hcl.Range for the name of the
77+
// attribute with the corresponding name. The name token is used to match with
78+
// the name of the attribute that this range will specify.
79+
//
80+
// "attr_value_range" can be placed on multiple fields that must be of type
81+
// hcl.Range. This field will be assigned the hcl.Range for the value of the
82+
// attribute with the corresponding name. The name token is used to match with
83+
// the name of the attribute that this range will specify.
84+
//
5585
// Only a subset of this tagging/typing vocabulary is supported for the
5686
// "Encode" family of functions. See the EncodeIntoBody docs for full details
5787
// on the constraints there.

gohcl/schema.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,31 @@ type fieldTags struct {
118118
Remain *int
119119
Body *int
120120
Optional map[string]bool
121+
122+
AttributeRange map[string]int
123+
AttributeNameRange map[string]int
124+
AttributeValueRange map[string]int
125+
126+
DefRange *int
127+
TypeRange *int
128+
LabelRange map[string]int
121129
}
122130

123131
type labelField struct {
124132
FieldIndex int
133+
RangeIndex int
125134
Name string
126135
}
127136

128137
func getFieldTags(ty reflect.Type) *fieldTags {
129138
ret := &fieldTags{
130-
Attributes: map[string]int{},
131-
Blocks: map[string]int{},
132-
Optional: map[string]bool{},
139+
Attributes: map[string]int{},
140+
Blocks: map[string]int{},
141+
Optional: map[string]bool{},
142+
AttributeRange: map[string]int{},
143+
AttributeNameRange: map[string]int{},
144+
AttributeValueRange: map[string]int{},
145+
LabelRange: map[string]int{},
133146
}
134147

135148
ct := ty.NumField()
@@ -175,6 +188,26 @@ func getFieldTags(ty reflect.Type) *fieldTags {
175188
case "optional":
176189
ret.Attributes[name] = i
177190
ret.Optional[name] = true
191+
case "def_range":
192+
if ret.DefRange != nil {
193+
panic("only one 'def_range' tag is permitted")
194+
}
195+
idx := i // copy, because this loop will continue assigning to i
196+
ret.DefRange = &idx
197+
case "type_range":
198+
if ret.TypeRange != nil {
199+
panic("only one 'type_range' tag is permitted")
200+
}
201+
idx := i // copy, because this loop will continue assigning to i
202+
ret.TypeRange = &idx
203+
case "label_range":
204+
ret.LabelRange[name] = i
205+
case "attr_range":
206+
ret.AttributeRange[name] = i
207+
case "attr_name_range":
208+
ret.AttributeNameRange[name] = i
209+
case "attr_value_range":
210+
ret.AttributeValueRange[name] = i
178211
default:
179212
panic(fmt.Sprintf("invalid hcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
180213
}

0 commit comments

Comments
 (0)