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:
parent
d3703888b6
commit
a9f913f830
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user