hcl/hclsyntax: Accept single-line block definitions

This relaxes our previous spec to include a special form from HCL 1:

    foo { bar = baz }

Although we normally require each argument to be on a line of its own, as
a special case we allow a block to be defined with a single nested
argument all on one line.

Only one nested argument definition is allowed, and a nested block
definition like "foo { bar {} }" is also disallowed in order to force the
more-readable split of bar {} onto a line of its own.

This is a pragmatic addition for broader compatibility with HCL 1-oriented
input. This single-line usage is not considered idiomatic HCL 2 and may
in future be undone by the formatter, though for now it is left as-is
aside from the spacing around the braces.

This also changes the behavior of the source code formatter to include
spaces on both sides of braces. This mimicks the formatting behavior of
HCL 1 for this situation, and (subjectively) reads better even for other
one-line braced expressions like object constructors and object for
expressions.
This commit is contained in:
Martin Atkins 2018-12-14 13:45:03 -08:00
parent 2934d2f033
commit bafa0c5ace
12 changed files with 278 additions and 35 deletions

View File

@ -131,7 +131,7 @@ func (p *parser) ParseBodyItem() (Node, hcl.Diagnostics) {
switch next.Type {
case TokenEqual:
return p.finishParsingBodyAttribute(ident)
return p.finishParsingBodyAttribute(ident, false)
case TokenOQuote, TokenOBrace, TokenIdent:
return p.finishParsingBodyBlock(ident)
default:
@ -149,7 +149,72 @@ func (p *parser) ParseBodyItem() (Node, hcl.Diagnostics) {
return nil, nil
}
func (p *parser) finishParsingBodyAttribute(ident Token) (Node, hcl.Diagnostics) {
// parseSingleAttrBody is a weird variant of ParseBody that deals with the
// body of a nested block containing only one attribute value all on a single
// line, like foo { bar = baz } . It expects to find a single attribute item
// immediately followed by the end token type with no intervening newlines.
func (p *parser) parseSingleAttrBody(end TokenType) (*Body, hcl.Diagnostics) {
ident := p.Read()
if ident.Type != TokenIdent {
p.recoverAfterBodyItem()
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Argument or block definition required",
Detail: "An argument or block definition is required here.",
Subject: &ident.Range,
},
}
}
var attr *Attribute
var diags hcl.Diagnostics
next := p.Peek()
switch next.Type {
case TokenEqual:
node, attrDiags := p.finishParsingBodyAttribute(ident, true)
diags = append(diags, attrDiags...)
attr = node.(*Attribute)
case TokenOQuote, TokenOBrace, TokenIdent:
p.recoverAfterBodyItem()
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Argument definition required",
Detail: fmt.Sprintf("A single-line block definition can contain only a single argument. If you meant to define argument %q, use an equals sign to assign it a value. To define a nested block, place it on a line of its own within its parent block.", ident.Bytes),
Subject: hcl.RangeBetween(ident.Range, next.Range).Ptr(),
},
}
default:
p.recoverAfterBodyItem()
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Argument or block definition required",
Detail: "An argument or block definition is required here. To set an argument, use the equals sign \"=\" to introduce the argument value.",
Subject: &ident.Range,
},
}
}
return &Body{
Attributes: Attributes{
string(ident.Bytes): attr,
},
SrcRange: attr.SrcRange,
EndRange: hcl.Range{
Filename: attr.SrcRange.Filename,
Start: attr.SrcRange.End,
End: attr.SrcRange.End,
},
}, diags
}
func (p *parser) finishParsingBodyAttribute(ident Token, singleLine bool) (Node, hcl.Diagnostics) {
eqTok := p.Read() // eat equals token
if eqTok.Type != TokenEqual {
// should never happen if caller behaves
@ -166,22 +231,25 @@ func (p *parser) finishParsingBodyAttribute(ident Token) (Node, hcl.Diagnostics)
endRange = p.PrevRange()
p.recoverAfterBodyItem()
} else {
end := p.Peek()
if end.Type != TokenNewline && end.Type != TokenEOF {
if !p.recovery {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing newline after argument",
Detail: "An argument definition must end with a newline.",
Subject: &end.Range,
Context: hcl.RangeBetween(ident.Range, end.Range).Ptr(),
})
endRange = p.PrevRange()
if !singleLine {
end := p.Peek()
if end.Type != TokenNewline && end.Type != TokenEOF {
if !p.recovery {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing newline after argument",
Detail: "An argument definition must end with a newline.",
Subject: &end.Range,
Context: hcl.RangeBetween(ident.Range, end.Range).Ptr(),
})
}
endRange = p.PrevRange()
p.recoverAfterBodyItem()
} else {
endRange = p.PrevRange()
p.Read() // eat newline
}
endRange = p.PrevRange()
p.recoverAfterBodyItem()
} else {
endRange = p.PrevRange()
p.Read() // eat newline
}
}
@ -288,7 +356,51 @@ Token:
// Once we fall out here, the peeker is pointed just after our opening
// brace, so we can begin our nested body parsing.
body, bodyDiags := p.ParseBody(TokenCBrace)
var body *Body
var bodyDiags hcl.Diagnostics
switch p.Peek().Type {
case TokenNewline, TokenEOF, TokenCBrace:
body, bodyDiags = p.ParseBody(TokenCBrace)
default:
// Special one-line, single-attribute block parsing mode.
body, bodyDiags = p.parseSingleAttrBody(TokenCBrace)
switch p.Peek().Type {
case TokenCBrace:
p.Read() // the happy path - just consume the closing brace
case TokenComma:
// User seems to be trying to use the object-constructor
// comma-separated style, which isn't permitted for blocks.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid single-argument block definition",
Detail: "Single-line block syntax can include only one argument definition. To define multiple arguments, use the multi-line block syntax with one argument definition per line.",
Subject: p.Peek().Range.Ptr(),
})
p.recover(TokenCBrace)
case TokenNewline:
// We don't allow weird mixtures of single and multi-line syntax.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid single-argument block definition",
Detail: "An argument definition on the same line as its containing block creates a single-line block definition, which must also be closed on the same line. Place the block's closing brace immediately after the argument definition.",
Subject: p.Peek().Range.Ptr(),
})
p.recover(TokenCBrace)
default:
// Some other weird thing is going on. Since we can't guess a likely
// user intent for this one, we'll skip it if we're already in
// recovery mode.
if !p.recovery {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid single-argument block definition",
Detail: "A single-line block definition must end with a closing brace immediately after its single argument definition.",
Subject: p.Peek().Range.Ptr(),
})
}
p.recover(TokenCBrace)
}
}
diags = append(diags, bodyDiags...)
cBraceRange := p.PrevRange()

View File

@ -158,10 +158,11 @@ These constructs correspond to the similarly-named concepts in the
language-agnostic HCL information model.
```ebnf
ConfigFile = Body;
Body = (Attribute | Block)*;
Attribute = Identifier "=" Expression Newline;
Block = Identifier (StringLit|Identifier)* "{" Newline Body "}" Newline;
ConfigFile = Body;
Body = (Attribute | Block | OneLineBlock)*;
Attribute = Identifier "=" Expression Newline;
Block = Identifier (StringLit|Identifier)* "{" Newline Body "}" Newline;
OneLineBlock = Identifier (StringLit|Identifier)* "{" (Identifier "=" Expression)? "}" Newline;
```
### Configuration Files

View File

@ -48,7 +48,7 @@ func Example_generateFromScratch() {
// Output:
// string = "foo"
//
// object = {bar = 5, baz = true, foo = "foo"}
// object = { bar = 5, baz = true, foo = "foo" }
// bool = false
// path = env.PATH
//

View File

@ -283,6 +283,17 @@ func spaceAfterToken(subject, before, after *Token) bool {
return true
}
case subject.Type == hclsyntax.TokenOBrace || (after != nil && after.Type == hclsyntax.TokenCBrace):
// Unlike other bracket types, braces have spaces on both sides of them,
// both in single-line nested blocks foo { bar = baz } and in object
// constructor expressions foo = { bar = baz }.
if subject.Type == hclsyntax.TokenOBrace && after.Type == hclsyntax.TokenCBrace {
// An open brace followed by a close brace is an exception, however.
// e.g. foo {} rather than foo { }
return false
}
return true
case tokenBracketChange(subject) > 0:
// No spaces after open brackets
return false

View File

@ -182,6 +182,14 @@ a = 1
b {
a = 1
}
`,
},
{
`
b {a = 1}
`,
`
b { a = 1 }
`,
},
{

View File

@ -340,8 +340,9 @@ func TestTokensForValue(t *testing.T) {
Bytes: []byte(`{`),
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
Type: hclsyntax.TokenIdent,
Bytes: []byte(`foo`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenEqual,
@ -354,8 +355,9 @@ func TestTokensForValue(t *testing.T) {
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
SpacesBefore: 1,
},
},
},
@ -370,8 +372,9 @@ func TestTokensForValue(t *testing.T) {
Bytes: []byte(`{`),
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte(`bar`),
Type: hclsyntax.TokenIdent,
Bytes: []byte(`bar`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenEqual,
@ -403,8 +406,9 @@ func TestTokensForValue(t *testing.T) {
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
SpacesBefore: 1,
},
},
},
@ -418,8 +422,9 @@ func TestTokensForValue(t *testing.T) {
Bytes: []byte(`{`),
},
{
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
Type: hclsyntax.TokenOQuote,
Bytes: []byte(`"`),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenQuotedLit,
@ -440,8 +445,9 @@ func TestTokensForValue(t *testing.T) {
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
SpacesBefore: 1,
},
},
},

View File

@ -0,0 +1 @@
a { b = "foo" }

View File

@ -0,0 +1,8 @@
block {
block_type = "a"
object {
attr "b" {
type = string
}
}
}

View File

@ -0,0 +1,6 @@
result_type = object({
b = string
})
result = {
b = "foo"
}

View File

@ -0,0 +1,9 @@
a { b = "foo", c = "bar" }
a { b = "foo"
}
a { b = "foo"
c = "bar" }
a { b = "foo"
c = "bar"
}
a { d {} }

View File

@ -0,0 +1,14 @@
block_list {
block_type = "a"
object {
attr "b" {
type = string
}
attr "c" {
type = string
}
block_list "d" {
object {}
}
}
}

View File

@ -0,0 +1,67 @@
diagnostics {
error {
# Message like "Only one argument is allowed in a single-line block definition"
from {
line = 1
column = 14
byte = 13
}
to {
line = 1
column = 15
byte = 14
}
}
error {
# Message like "The closing brace for a single-line block definition must be on the same line"
from {
line = 2
column = 14
byte = 40
}
to {
line = 3
column = 1
byte = 41
}
}
error {
# Message like "The closing brace for a single-line block definition must be on the same line"
from {
line = 4
column = 14
byte = 56
}
to {
line = 5
column = 1
byte = 57
}
}
error {
# Message like "The closing brace for a single-line block definition must be on the same line"
from {
line = 6
column = 14
byte = 84
}
to {
line = 7
column = 1
byte = 85
}
}
error {
# Message like "A single-line block definition cannot contain another block definition"
from {
line = 9
column = 5
byte = 103
}
to {
line = 9
column = 8
byte = 106
}
}
}