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: WithRange[T] for concise decoding with source locations #516

Closed
Closed
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
11 changes: 8 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/hashicorp/hcl/v2

go 1.12
go 1.18

require (
github.com/agext/levenshtein v1.2.1
Expand All @@ -12,12 +12,17 @@ require (
github.com/kr/pretty v0.1.0
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.0.0
github.com/spf13/pflag v1.0.2
github.com/stretchr/testify v1.2.2 // indirect
github.com/zclconf/go-cty v1.8.0
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
)

require (
github.com/kr/text v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2 // indirect
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 // indirect
golang.org/x/text v0.3.5 // indirect
)
10 changes: 10 additions & 0 deletions gohcl/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,19 @@ func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value)
// are returned then the given value may have been partially-populated but
// may still be accessed by a careful caller for static analysis and editor
// integration use-cases.
//
// DecodeExpression can also decode into non-nil *WithRange[T] values, as long
// as T is a type that DecodeExpression would normally accept directly. In that
// case, the resulting object has Value set to the decode result and
// Range set to the source range of the given expression.
func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
srcVal, diags := expr.Value(ctx)

if wrr := analyzeWithRange(val); wrr != nil {
val = wrr.value.Addr().Interface()
wrr.rng.Set(reflect.ValueOf(expr.Range()))
}

convTy, err := gocty.ImpliedType(val)
if err != nil {
panic(fmt.Sprintf("unsuitable DecodeExpression target: %s", err))
Expand Down
26 changes: 26 additions & 0 deletions gohcl/with_range.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gohcl

import (
"reflect"
)

// Our WithRange[T] support is currently conditional on whether we're running
// in a Go 1.18 or later toolchain, and thus we can use generics.
//
// This file contains some items we need regardless of whether we have that
// turned on, just so that our version-agnostic callers can still work even
// when it's disabled.
//
// See with_range_118.go for the parts that are active only in Go 1.18 or later,
// and with_range_compat.go for a stub we'll use in earlier Go versions.

// withRangeReflect is a reflection-oriented description of a value of any
// specific WithRange[T] type, which a caller can therefore interpret without
// using any Go 1.18-only language features.
type withRangeReflect struct {
containerPtr reflect.Value
container reflect.Value

value reflect.Value
rng reflect.Value
}
63 changes: 63 additions & 0 deletions gohcl/with_range_118.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//go:build go1.18
// +build go1.18

package gohcl

import (
"reflect"

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

type WithRange[T any] struct {
Value T
Range hcl.Range
}

// withRange is an internal interface implemented only by WithRange[T]
// types, which we'll use to recognize uses of it even though we can't
// predict ahead of time all of the possible type arguments.
type withRange interface {
// withRangeReflect returns a reflect-package-oriented interpretation of
// the reciever and its fields.
withRangeReflect() withRangeReflect
}

func (wr *WithRange[T]) withRangeReflect() withRangeReflect {
containerPtrV := reflect.ValueOf(wr)
var containerV reflect.Value

// If we don't yet have a container (wr is nil) then we'll instantiate
// one during our work here and describe _that_ to the caller so that
// they can write it into the appropriate location in the surrounding
// type once it's all populated.
if containerPtrV.IsNil() {
newContainerPtrV := reflect.New(containerPtrV.Elem().Type())
containerV = newContainerPtrV.Elem()
} else {
containerV = containerPtrV.Elem()
}

return withRangeReflect{
containerPtr: containerPtrV,
container: containerV,

value: containerV.FieldByName("Value"),
rng: containerV.FieldByName("Range"),
}
}

// analyzeWithRange is an internal adapter to allow Go-version-agnostic callers
// to compile regardless of whether we are using Go 1.18 features or not.
//
// On Go 1.18 or later, will return a non-nil withRangeReflect pointer if the
// given value has a WithRange type, or nil if it doesn't.
func analyzeWithRange(v interface{}) *withRangeReflect {
wrI, ok := v.(withRange)
if !ok {
return nil
}

ret := wrI.withRangeReflect()
return &ret
}
14 changes: 14 additions & 0 deletions gohcl/with_range_compat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !go1.18
// +build !go1.18

package gohcl

// analyzeWithRange is an internal adapter to allow Go-version-agnostic callers
// to compile regardless of whether we are using Go 1.18 features or not.
//
// On versions of Go prior to 1.18, this just immediately returns nil to
// indicate that no value can possibly have a WithRange type on prior versions;
// that type isn't declared at all, then.
func analyzeWithRange(v interface{}) *withRangeReflect {
return nil
}
51 changes: 51 additions & 0 deletions gohcl/with_range_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//go:build go1.18
// +build go1.18

package gohcl

import (
"testing"

"github.com/google/go-cmp/cmp"

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

func TestDecodeWithRange(t *testing.T) {
type Config struct {
Name WithRange[string] `hcl:"name"`
Number int `hcl:"number"`
}

configSrc := `
name = "Gerald"
number = 12
`

f, diags := hclsyntax.ParseConfig([]byte(configSrc), "test.hcl", hcl.InitialPos)
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags)
}

var config Config
diags = DecodeBody(f.Body, nil, &config)
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags)
}

want := Config{
Name: WithRange[string]{
Value: "Gerald",
Range: hcl.Range{
Filename: "test.hcl",
Start: hcl.Pos{ Line: 2, Column: 12, Byte: 12 },
End: hcl.Pos{ Line: 2, Column: 20, Byte: 20 },
},
},
Number: 12,
}
if diff := cmp.Diff(want, config); diff != "" {
t.Errorf("incorrect result\n%s", diff)
}
}