zcldec: SourceRange function

This allows callers to determine the source location where a particular
value (identified by a spec) came from, for use in application-level
diagnostic messages.
This commit is contained in:
Martin Atkins 2017-06-04 08:13:36 -07:00
parent d3703888b6
commit a9f913f830
4 changed files with 171 additions and 0 deletions

View File

@ -23,3 +23,10 @@ func decode(body zcl.Body, block *zcl.Block, ctx *zcl.EvalContext, spec Spec, pa
return val, leftovers, diags
}
func sourceRange(body zcl.Body, block *zcl.Block, spec Spec) zcl.Range {
schema := ImpliedSchema(spec)
content, _, _ := body.PartialContent(schema)
return spec.sourceRange(content, block)
}

View File

@ -25,3 +25,23 @@ func Decode(body zcl.Body, spec Spec, ctx *zcl.EvalContext) (cty.Value, zcl.Diag
func PartialDecode(body zcl.Body, spec Spec, ctx *zcl.EvalContext) (cty.Value, zcl.Body, zcl.Diagnostics) {
return decode(body, nil, ctx, spec, true)
}
// SourceRange interprets the given body using the given specification and
// then returns the source range of the value that would be used to
// fulfill the spec.
//
// This can be used if application-level validation detects value errors, to
// obtain a reasonable SourceRange to use for generated diagnostics. It works
// best when applied to specific body items (e.g. using AttrSpec, BlockSpec, ...)
// as opposed to entire bodies using ObjectSpec, TupleSpec. The result will
// be less useful the broader the specification, so e.g. a spec that returns
// the entirety of all of the blocks of a given type is likely to be
// _particularly_ arbitrary and useless.
//
// If the given body is not valid per the given spec, the result is best-effort
// and may not actually be something ideal. It's expected that an application
// will already have used Decode or PartialDecode earlier and thus had an
// opportunity to detect and report spec violations.
func SourceRange(body zcl.Body, spec Spec) zcl.Range {
return sourceRange(body, nil, spec)
}

View File

@ -2,6 +2,7 @@ package zcldec
import (
"fmt"
"reflect"
"testing"
"github.com/zclconf/go-cty/cty"
@ -150,3 +151,79 @@ b {}
})
}
}
func TestSourceRange(t *testing.T) {
tests := []struct {
config string
spec Spec
want zcl.Range
}{
{
`a = 1`,
&AttrSpec{
Name: "a",
},
zcl.Range{
Start: zcl.Pos{Line: 1, Column: 5, Byte: 4},
End: zcl.Pos{Line: 1, Column: 6, Byte: 5},
},
},
{
`
b {
a = 1
}`,
&BlockSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
},
},
zcl.Range{
Start: zcl.Pos{Line: 3, Column: 7, Byte: 11},
End: zcl.Pos{Line: 3, Column: 8, Byte: 12},
},
},
{
`
b {
c {
a = 1
}
}`,
&BlockSpec{
TypeName: "b",
Nested: &BlockSpec{
TypeName: "c",
Nested: &AttrSpec{
Name: "a",
},
},
},
zcl.Range{
Start: zcl.Pos{Line: 4, Column: 9, Byte: 19},
End: zcl.Pos{Line: 4, Column: 10, Byte: 20},
},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d-%s", i, test.config), func(t *testing.T) {
file, diags := zclsyntax.ParseConfig([]byte(test.config), "", zcl.Pos{Line: 1, Column: 1, Byte: 0})
if len(diags) != 0 {
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), 0)
for _, diag := range diags {
t.Logf(" - %s", diag.Error())
}
}
body := file.Body
got := SourceRange(body, test.spec)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
}
})
}
}

View File

@ -24,6 +24,12 @@ type Spec interface {
// get decoded with the same body and block as the receiver. This should
// not descend into the nested specs used when decoding blocks.
visitSameBodyChildren(cb visitFunc)
// Determine the source range of the value that would be returned for the
// spec in the given content, in the context of the given block
// (which might be null). If the corresponding item is missing, return
// a place where it might be inserted.
sourceRange(content *zcl.BodyContent, block *zcl.Block) zcl.Range
}
type visitFunc func(spec Spec)
@ -67,6 +73,17 @@ func (s ObjectSpec) decode(content *zcl.BodyContent, block *zcl.Block, ctx *zcl.
return cty.ObjectVal(vals), diags
}
func (s ObjectSpec) sourceRange(content *zcl.BodyContent, block *zcl.Block) zcl.Range {
if block != nil {
return block.DefRange
}
// This is not great, but the best we can do. In practice, it's rather
// strange to ask for the source range of an entire top-level body, since
// that's already readily available to the caller.
return content.MissingItemRange
}
// A TupleSpec is a Spec that produces a cty.Value of a tuple type whose
// elements correspond to the elements of the spec slice.
type TupleSpec []Spec
@ -90,6 +107,17 @@ func (s TupleSpec) decode(content *zcl.BodyContent, block *zcl.Block, ctx *zcl.E
return cty.TupleVal(vals), diags
}
func (s TupleSpec) sourceRange(content *zcl.BodyContent, block *zcl.Block) zcl.Range {
if block != nil {
return block.DefRange
}
// This is not great, but the best we can do. In practice, it's rather
// strange to ask for the source range of an entire top-level body, since
// that's already readily available to the caller.
return content.MissingItemRange
}
// An AttrSpec is a Spec that evaluates a particular attribute expression in
// the body and returns its resulting value converted to the requested type,
// or produces a diagnostic if the type is incorrect.
@ -123,6 +151,15 @@ func (s *AttrSpec) attrSchemata() []zcl.AttributeSchema {
}
}
func (s *AttrSpec) sourceRange(content *zcl.BodyContent, block *zcl.Block) zcl.Range {
attr, exists := content.Attributes[s.Name]
if !exists {
return content.MissingItemRange
}
return attr.Expr.Range()
}
func (s *AttrSpec) decode(content *zcl.BodyContent, block *zcl.Block, ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
attr, exists := content.Attributes[s.Name]
if !exists {
@ -149,6 +186,14 @@ func (s *LiteralSpec) decode(content *zcl.BodyContent, block *zcl.Block, ctx *zc
return s.Value, nil
}
func (s *LiteralSpec) sourceRange(content *zcl.BodyContent, block *zcl.Block) zcl.Range {
// No sensible range to return for a literal, so the caller had better
// ensure it doesn't cause any diagnostics.
return zcl.Range{
Filename: "<unknown>",
}
}
// An ExprSpec is a Spec that evaluates the given expression, ignoring the
// given body.
type ExprSpec struct {
@ -168,6 +213,10 @@ func (s *ExprSpec) decode(content *zcl.BodyContent, block *zcl.Block, ctx *zcl.E
return s.Expr.Value(ctx)
}
func (s *ExprSpec) sourceRange(content *zcl.BodyContent, block *zcl.Block) zcl.Range {
return s.Expr.Range()
}
// A BlockSpec is a Spec that produces a cty.Value by decoding the contents
// of a single nested block of a given type, using a nested spec.
//
@ -240,6 +289,24 @@ func (s *BlockSpec) decode(content *zcl.BodyContent, block *zcl.Block, ctx *zcl.
return val, diags
}
func (s *BlockSpec) sourceRange(content *zcl.BodyContent, block *zcl.Block) zcl.Range {
var childBlock *zcl.Block
for _, candidate := range content.Blocks {
if candidate.Type != s.TypeName {
continue
}
childBlock = candidate
break
}
if childBlock == nil {
return content.MissingItemRange
}
return sourceRange(childBlock.Body, childBlock, s.Nested)
}
// A BlockListSpec is a Spec that produces a cty list of the results of
// decoding all of the nested blocks of a given type, using a nested spec.
type BlockListSpec struct {