Skip to content
This repository has been archived by the owner on Oct 3, 2019. It is now read-only.

Commit

Permalink
hcl/hclsyntax: Accept single-line block definitions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
apparentlymart committed Dec 14, 2018
1 parent 2934d2f commit bafa0c5
Show file tree
Hide file tree
Showing 12 changed files with 278 additions and 35 deletions.
148 changes: 130 additions & 18 deletions hcl/hclsyntax/parser.go
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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()

Expand Down
9 changes: 5 additions & 4 deletions hcl/hclsyntax/spec.md
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion hclwrite/examples_test.go
Expand Up @@ -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
//
Expand Down
11 changes: 11 additions & 0 deletions hclwrite/format.go
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions hclwrite/format_test.go
Expand Up @@ -182,6 +182,14 @@ a = 1
b {
a = 1
}
`,
},
{
`
b {a = 1}
`,
`
b { a = 1 }
`,
},
{
Expand Down
30 changes: 18 additions & 12 deletions hclwrite/generate_test.go
Expand Up @@ -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,
Expand All @@ -354,8 +355,9 @@ func TestTokensForValue(t *testing.T) {
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
SpacesBefore: 1,
},
},
},
Expand All @@ -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,
Expand Down Expand Up @@ -403,8 +406,9 @@ func TestTokensForValue(t *testing.T) {
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
SpacesBefore: 1,
},
},
},
Expand All @@ -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,
Expand All @@ -440,8 +445,9 @@ func TestTokensForValue(t *testing.T) {
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
Type: hclsyntax.TokenCBrace,
Bytes: []byte(`}`),
SpacesBefore: 1,
},
},
},
Expand Down
1 change: 1 addition & 0 deletions specsuite/tests/structure/blocks/single_oneline.hcl
@@ -0,0 +1 @@
a { b = "foo" }
8 changes: 8 additions & 0 deletions specsuite/tests/structure/blocks/single_oneline.hcldec
@@ -0,0 +1,8 @@
block {
block_type = "a"
object {
attr "b" {
type = string
}
}
}
6 changes: 6 additions & 0 deletions specsuite/tests/structure/blocks/single_oneline.t
@@ -0,0 +1,6 @@
result_type = object({
b = string
})
result = {
b = "foo"
}
9 changes: 9 additions & 0 deletions specsuite/tests/structure/blocks/single_oneline_invalid.hcl
@@ -0,0 +1,9 @@
a { b = "foo", c = "bar" }
a { b = "foo"
}
a { b = "foo"
c = "bar" }
a { b = "foo"
c = "bar"
}
a { d {} }
14 changes: 14 additions & 0 deletions specsuite/tests/structure/blocks/single_oneline_invalid.hcldec
@@ -0,0 +1,14 @@
block_list {
block_type = "a"
object {
attr "b" {
type = string
}
attr "c" {
type = string
}
block_list "d" {
object {}
}
}
}

1 comment on commit bafa0c5

@mitchellh
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this looks great. I think most commonly I've seen this in mine and others code as: variable "foo" {} (aka no attributes at all). But also for defaults. So this will be really good for compat.

Please sign in to comment.