zclsyntax: parsing and evaluation for object constructors

This commit is contained in:
Martin Atkins 2017-06-04 16:14:02 -07:00
parent cac847b163
commit 6f2bd0009c
4 changed files with 324 additions and 0 deletions

View File

@ -397,3 +397,98 @@ func (e *TupleConsExpr) Range() zcl.Range {
func (e *TupleConsExpr) StartRange() zcl.Range {
return e.OpenRange
}
type ObjectConsExpr struct {
Items []ObjectConsItem
SrcRange zcl.Range
OpenRange zcl.Range
}
type ObjectConsItem struct {
KeyExpr Expression
ValueExpr Expression
}
func (e *ObjectConsExpr) walkChildNodes(w internalWalkFunc) {
for i, item := range e.Items {
e.Items[i].KeyExpr = w(item.KeyExpr).(Expression)
e.Items[i].ValueExpr = w(item.ValueExpr).(Expression)
}
}
func (e *ObjectConsExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
var vals map[string]cty.Value
var diags zcl.Diagnostics
// This will get set to true if we fail to produce any of our keys,
// either because they are actually unknown or if the evaluation produces
// errors. In all of these case we must return DynamicPseudoType because
// we're unable to know the full set of keys our object has, and thus
// we can't produce a complete value of the intended type.
//
// We still evaluate all of the item keys and values to make sure that we
// get as complete as possible a set of diagnostics.
known := true
vals = make(map[string]cty.Value, len(e.Items))
for _, item := range e.Items {
key, keyDiags := item.KeyExpr.Value(ctx)
diags = append(diags, keyDiags...)
val, valDiags := item.ValueExpr.Value(ctx)
diags = append(diags, valDiags...)
if keyDiags.HasErrors() {
known = false
continue
}
if key.IsNull() {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Null value as key",
Detail: "Can't use a null value as a key.",
Subject: item.ValueExpr.Range().Ptr(),
})
known = false
continue
}
var err error
key, err = convert.Convert(key, cty.String)
if err != nil {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Incorrect key type",
Detail: fmt.Sprintf("Can't use this value as a key: %s.", err.Error()),
Subject: item.ValueExpr.Range().Ptr(),
})
known = false
continue
}
if !key.IsKnown() {
known = false
continue
}
keyStr := key.AsString()
vals[keyStr] = val
}
if !known {
return cty.DynamicVal, diags
}
return cty.ObjectVal(vals), diags
}
func (e *ObjectConsExpr) Range() zcl.Range {
return e.SrcRange
}
func (e *ObjectConsExpr) StartRange() zcl.Range {
return e.OpenRange
}

View File

@ -179,6 +179,99 @@ upper(
cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.True}),
0,
},
{
`{}`,
nil,
cty.EmptyObjectVal,
0,
},
{
`{"hello": "world"}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
0,
},
{
`{"hello" = "world"}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
0,
},
{
`{hello = "world"}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
0,
},
{
`{hello: "world"}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
0,
},
{
`{"hello" = "world", "goodbye" = "cruel world"}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
"goodbye": cty.StringVal("cruel world"),
}),
0,
},
{
`{
"hello" = "world"
}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
0,
},
{
`{
"hello" = "world"
"goodbye" = "cruel world"
}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
"goodbye": cty.StringVal("cruel world"),
}),
0,
},
{
`{
"hello" = "world",
"goodbye" = "cruel world"
}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
"goodbye": cty.StringVal("cruel world"),
}),
0,
},
{
`{
"hello" = "world",
"goodbye" = "cruel world",
}`,
nil,
cty.ObjectVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
"goodbye": cty.StringVal("cruel world"),
}),
0,
},
}
for _, test := range tests {

View File

@ -23,6 +23,10 @@ func (e *LiteralValueExpr) Variables() []zcl.Traversal {
return Variables(e)
}
func (e *ObjectConsExpr) Variables() []zcl.Traversal {
return Variables(e)
}
func (e *ScopeTraversalExpr) Variables() []zcl.Traversal {
return Variables(e)
}

View File

@ -590,6 +590,9 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) {
case TokenOBrack:
return p.parseTupleCons()
case TokenOBrace:
return p.parseObjectCons()
default:
var diags zcl.Diagnostics
if !p.recovery {
@ -752,6 +755,135 @@ func (p *parser) parseTupleCons() (Expression, zcl.Diagnostics) {
}, diags
}
func (p *parser) parseObjectCons() (Expression, zcl.Diagnostics) {
open := p.Read()
if open.Type != TokenOBrace {
// Should never happen if callers are behaving
panic("parseObjectCons called without peeker pointing to open brace")
}
var close Token
var diags zcl.Diagnostics
var items []ObjectConsItem
p.PushIncludeNewlines(true)
defer p.PopIncludeNewlines()
for {
next := p.Peek()
if next.Type == TokenNewline {
p.Read() // eat newline
continue
}
if next.Type == TokenCBrace {
close = p.Read() // eat closer
break
}
// As a special case, we allow the key to be a literal identifier.
// This means that a variable reference or function call can't appear
// directly as key expression, and must instead be wrapped in some
// disambiguation punctuation, like (var.a) = "b" or "${var.a}" = "b".
var key Expression
var keyDiags zcl.Diagnostics
if p.Peek().Type == TokenIdent {
nameTok := p.Read()
key = &LiteralValueExpr{
Val: cty.StringVal(string(nameTok.Bytes)),
SrcRange: nameTok.Range,
}
} else {
key, keyDiags = p.ParseExpression()
}
diags = append(diags, keyDiags...)
if p.recovery && keyDiags.HasErrors() {
// If expression parsing failed then we are probably in a strange
// place in the token stream, so we'll bail out and try to reset
// to after our closing brace to allow parsing to continue.
close = p.recover(TokenCBrace)
break
}
next = p.Peek()
if next.Type != TokenEqual && next.Type != TokenColon {
if !p.recovery {
if next.Type == TokenNewline || next.Type == TokenComma {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing item value",
Detail: "Expected an item value, introduced by an equals sign (\"=\").",
Subject: &next.Range,
Context: zcl.RangeBetween(open.Range, next.Range).Ptr(),
})
} else {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing key/value separator",
Detail: "Expected an equals sign (\"=\") to mark the beginning of the item value.",
Subject: &next.Range,
Context: zcl.RangeBetween(open.Range, next.Range).Ptr(),
})
}
}
close = p.recover(TokenCBrace)
break
}
p.Read() // eat equals sign or colon
value, valueDiags := p.ParseExpression()
diags = append(diags, valueDiags...)
if p.recovery && valueDiags.HasErrors() {
// If expression parsing failed then we are probably in a strange
// place in the token stream, so we'll bail out and try to reset
// to after our closing brace to allow parsing to continue.
close = p.recover(TokenCBrace)
break
}
items = append(items, ObjectConsItem{
KeyExpr: key,
ValueExpr: value,
})
next = p.Peek()
if next.Type == TokenCBrace {
close = p.Read() // eat closer
break
}
if next.Type != TokenComma && next.Type != TokenNewline {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing item separator",
Detail: "Expected a newline or comma to mark the beginning of the next item.",
Subject: &next.Range,
Context: zcl.RangeBetween(open.Range, next.Range).Ptr(),
})
}
close = p.recover(TokenCBrace)
break
}
p.Read() // eat comma or newline
}
return &ObjectConsExpr{
Items: items,
SrcRange: zcl.RangeBetween(open.Range, close.Range),
OpenRange: open.Range,
}, diags
}
func (p *parser) ParseTemplate(end TokenType) (Expression, zcl.Diagnostics) {
var parts []Expression
var diags zcl.Diagnostics