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:
parent
2934d2f033
commit
bafa0c5ace
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
//
|
||||
|
@ -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
|
||||
|
@ -182,6 +182,14 @@ a = 1
|
||||
b {
|
||||
a = 1
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
`
|
||||
b {a = 1}
|
||||
`,
|
||||
`
|
||||
b { a = 1 }
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
1
specsuite/tests/structure/blocks/single_oneline.hcl
Normal file
1
specsuite/tests/structure/blocks/single_oneline.hcl
Normal file
@ -0,0 +1 @@
|
||||
a { b = "foo" }
|
8
specsuite/tests/structure/blocks/single_oneline.hcldec
Normal file
8
specsuite/tests/structure/blocks/single_oneline.hcldec
Normal file
@ -0,0 +1,8 @@
|
||||
block {
|
||||
block_type = "a"
|
||||
object {
|
||||
attr "b" {
|
||||
type = string
|
||||
}
|
||||
}
|
||||
}
|
6
specsuite/tests/structure/blocks/single_oneline.t
Normal file
6
specsuite/tests/structure/blocks/single_oneline.t
Normal file
@ -0,0 +1,6 @@
|
||||
result_type = object({
|
||||
b = string
|
||||
})
|
||||
result = {
|
||||
b = "foo"
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
a { b = "foo", c = "bar" }
|
||||
a { b = "foo"
|
||||
}
|
||||
a { b = "foo"
|
||||
c = "bar" }
|
||||
a { b = "foo"
|
||||
c = "bar"
|
||||
}
|
||||
a { d {} }
|
@ -0,0 +1,14 @@
|
||||
block_list {
|
||||
block_type = "a"
|
||||
object {
|
||||
attr "b" {
|
||||
type = string
|
||||
}
|
||||
attr "c" {
|
||||
type = string
|
||||
}
|
||||
block_list "d" {
|
||||
object {}
|
||||
}
|
||||
}
|
||||
}
|
67
specsuite/tests/structure/blocks/single_oneline_invalid.t
Normal file
67
specsuite/tests/structure/blocks/single_oneline_invalid.t
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user