zclsyntax: parsing and evaluation for object constructors
This commit is contained in:
parent
cac847b163
commit
6f2bd0009c
@ -397,3 +397,98 @@ func (e *TupleConsExpr) Range() zcl.Range {
|
|||||||
func (e *TupleConsExpr) StartRange() zcl.Range {
|
func (e *TupleConsExpr) StartRange() zcl.Range {
|
||||||
return e.OpenRange
|
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
|
||||||
|
}
|
||||||
|
@ -179,6 +179,99 @@ upper(
|
|||||||
cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.True}),
|
cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.True}),
|
||||||
0,
|
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 {
|
for _, test := range tests {
|
||||||
|
@ -23,6 +23,10 @@ func (e *LiteralValueExpr) Variables() []zcl.Traversal {
|
|||||||
return Variables(e)
|
return Variables(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *ObjectConsExpr) Variables() []zcl.Traversal {
|
||||||
|
return Variables(e)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *ScopeTraversalExpr) Variables() []zcl.Traversal {
|
func (e *ScopeTraversalExpr) Variables() []zcl.Traversal {
|
||||||
return Variables(e)
|
return Variables(e)
|
||||||
}
|
}
|
||||||
|
@ -590,6 +590,9 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) {
|
|||||||
case TokenOBrack:
|
case TokenOBrack:
|
||||||
return p.parseTupleCons()
|
return p.parseTupleCons()
|
||||||
|
|
||||||
|
case TokenOBrace:
|
||||||
|
return p.parseObjectCons()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
var diags zcl.Diagnostics
|
var diags zcl.Diagnostics
|
||||||
if !p.recovery {
|
if !p.recovery {
|
||||||
@ -752,6 +755,135 @@ func (p *parser) parseTupleCons() (Expression, zcl.Diagnostics) {
|
|||||||
}, diags
|
}, 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) {
|
func (p *parser) ParseTemplate(end TokenType) (Expression, zcl.Diagnostics) {
|
||||||
var parts []Expression
|
var parts []Expression
|
||||||
var diags zcl.Diagnostics
|
var diags zcl.Diagnostics
|
||||||
|
Loading…
x
Reference in New Issue
Block a user