zclsyntax: require newlines after block items

Previously we tolerated EOF as an alias for newline, but a file without
a newline at the end is a edge case primarily limited to contrived
examples in unit tests, and requiring it simplifies tasks such as code
generation in zclwrite, since we can always assume that every block item
comes with a built-in line terminator.
This commit is contained in:
Martin Atkins 2017-06-09 08:19:47 -07:00
parent 1de72e146e
commit 5477fecfad
6 changed files with 137 additions and 57 deletions

View File

@ -168,15 +168,25 @@ func (p *parser) finishParsingBodyAttribute(ident Token) (Node, zcl.Diagnostics)
p.recoverAfterBodyItem()
} else {
end := p.Peek()
if end.Type != TokenNewline && end.Type != TokenEOF {
if end.Type != TokenNewline {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing newline after attribute definition",
Detail: "An attribute definition must end with a newline.",
Subject: &end.Range,
Context: zcl.RangeBetween(ident.Range, end.Range).Ptr(),
})
if end.Type == TokenEOF {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing newline after attribute definition",
Detail: "A newline is required after an attribute definition at the end of a file.",
Subject: &end.Range,
Context: zcl.RangeBetween(ident.Range, end.Range).Ptr(),
})
} else {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing newline after attribute definition",
Detail: "An attribute definition must end with a newline.",
Subject: &end.Range,
Context: zcl.RangeBetween(ident.Range, end.Range).Ptr(),
})
}
}
endRange = p.PrevRange()
p.recoverAfterBodyItem()
@ -285,17 +295,27 @@ Token:
cBraceRange := p.PrevRange()
eol := p.Peek()
if eol.Type == TokenNewline || eol.Type == TokenEOF {
if eol.Type == TokenNewline {
p.Read() // eat newline
} else {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing newline after block definition",
Detail: "A block definition must end with a newline.",
Subject: &eol.Range,
Context: zcl.RangeBetween(ident.Range, eol.Range).Ptr(),
})
if eol.Type == TokenEOF {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing newline after block definition",
Detail: "A newline is required after a block definition at the end of a file.",
Subject: &eol.Range,
Context: zcl.RangeBetween(ident.Range, eol.Range).Ptr(),
})
} else {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Missing newline after block definition",
Detail: "A block definition must end with a newline.",
Subject: &eol.Range,
Context: zcl.RangeBetween(ident.Range, eol.Range).Ptr(),
})
}
}
p.recoverAfterBodyItem()
}

View File

@ -33,7 +33,7 @@ func TestParseConfig(t *testing.T) {
},
{
`block {}`,
"block {}\n",
0,
&Body{
Attributes: Attributes{},
@ -72,16 +72,16 @@ func TestParseConfig(t *testing.T) {
},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 9, Byte: 8},
End: zcl.Pos{Line: 2, Column: 1, Byte: 9},
},
EndRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 9, Byte: 8},
End: zcl.Pos{Line: 1, Column: 9, Byte: 8},
Start: zcl.Pos{Line: 2, Column: 1, Byte: 9},
End: zcl.Pos{Line: 2, Column: 1, Byte: 9},
},
},
},
{
`block {}block {}`,
"block {}block {}\n",
1, // missing newline after block definition
&Body{
Attributes: Attributes{},
@ -120,16 +120,16 @@ func TestParseConfig(t *testing.T) {
},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 17, Byte: 16},
End: zcl.Pos{Line: 2, Column: 1, Byte: 17},
},
EndRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 17, Byte: 16},
End: zcl.Pos{Line: 1, Column: 17, Byte: 16},
Start: zcl.Pos{Line: 2, Column: 1, Byte: 17},
End: zcl.Pos{Line: 2, Column: 1, Byte: 17},
},
},
},
{
`block "foo" {}`,
"block \"foo\" {}\n",
0,
&Body{
Attributes: Attributes{},
@ -173,11 +173,11 @@ func TestParseConfig(t *testing.T) {
},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
End: zcl.Pos{Line: 2, Column: 1, Byte: 15},
},
EndRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 15, Byte: 14},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
Start: zcl.Pos{Line: 2, Column: 1, Byte: 15},
End: zcl.Pos{Line: 2, Column: 1, Byte: 15},
},
},
},
@ -269,7 +269,8 @@ block "valid" {}
},
},
{
`block "f\o" {}`,
`block "f\o" {}
`,
1, // "\o" is not a valid escape sequence
&Body{
Attributes: Attributes{},
@ -301,16 +302,17 @@ block "valid" {}
},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
End: zcl.Pos{Line: 2, Column: 1, Byte: 15},
},
EndRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 15, Byte: 14},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
Start: zcl.Pos{Line: 2, Column: 1, Byte: 15},
End: zcl.Pos{Line: 2, Column: 1, Byte: 15},
},
},
},
{
`block "f\n" {}`,
`block "f\n" {}
`,
0,
&Body{
Attributes: Attributes{},
@ -354,11 +356,11 @@ block "valid" {}
},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
End: zcl.Pos{Line: 2, Column: 1, Byte: 15},
},
EndRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 15, Byte: 14},
End: zcl.Pos{Line: 1, Column: 15, Byte: 14},
Start: zcl.Pos{Line: 2, Column: 1, Byte: 15},
End: zcl.Pos{Line: 2, Column: 1, Byte: 15},
},
},
},
@ -405,7 +407,7 @@ block "valid" {}
},
},
{
`a = foo.bar`,
"a = foo.bar\n",
0,
&Body{
Attributes: Attributes{
@ -454,11 +456,11 @@ block "valid" {}
Blocks: Blocks{},
SrcRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 1, Byte: 0},
End: zcl.Pos{Line: 1, Column: 12, Byte: 11},
End: zcl.Pos{Line: 2, Column: 1, Byte: 12},
},
EndRange: zcl.Range{
Start: zcl.Pos{Line: 1, Column: 12, Byte: 11},
End: zcl.Pos{Line: 1, Column: 12, Byte: 11},
Start: zcl.Pos{Line: 2, Column: 1, Byte: 12},
End: zcl.Pos{Line: 2, Column: 1, Byte: 12},
},
},
},

View File

@ -26,14 +26,14 @@ func TestDecode(t *testing.T) {
0,
},
{
`a = 1`,
"a = 1\n",
&ObjectSpec{},
nil,
cty.EmptyObjectVal,
1, // attribute named "a" is not expected here
},
{
`a = 1`,
"a = 1\n",
&ObjectSpec{
"a": &AttrSpec{
Name: "a",
@ -47,7 +47,7 @@ func TestDecode(t *testing.T) {
0,
},
{
`a = 1`,
"a = 1\n",
&AttrSpec{
Name: "a",
Type: cty.Number,
@ -57,7 +57,7 @@ func TestDecode(t *testing.T) {
0,
},
{
`a = "1"`,
"a = \"1\"\n",
&AttrSpec{
Name: "a",
Type: cty.Number,
@ -67,7 +67,7 @@ func TestDecode(t *testing.T) {
0,
},
{
`a = true`,
"a = true\n",
&AttrSpec{
Name: "a",
Type: cty.Number,
@ -112,7 +112,7 @@ b {
0,
},
{
`a {}`,
"a {}\n",
&BlockSpec{
TypeName: "b",
Nested: ObjectSpec{},
@ -179,7 +179,7 @@ func TestSourceRange(t *testing.T) {
want zcl.Range
}{
{
`a = 1`,
"a = 1\n",
&AttrSpec{
Name: "a",
},
@ -192,7 +192,8 @@ func TestSourceRange(t *testing.T) {
`
b {
a = 1
}`,
}
`,
&BlockSpec{
TypeName: "b",
Nested: &AttrSpec{
@ -210,7 +211,8 @@ b {
c {
a = 1
}
}`,
}
`,
&BlockSpec{
TypeName: "b",
Nested: &BlockSpec{

View File

@ -21,12 +21,12 @@ func TestVariables(t *testing.T) {
nil,
},
{
`a = foo`,
"a = foo\n",
&ObjectSpec{},
nil, // "a" is not actually used, so "foo" is not required
},
{
`a = foo`,
"a = foo\n",
&AttrSpec{
Name: "a",
},
@ -43,7 +43,7 @@ func TestVariables(t *testing.T) {
},
},
{
`a = foo`,
"a = foo\n",
&ObjectSpec{
"a": &AttrSpec{
Name: "a",
@ -65,7 +65,8 @@ func TestVariables(t *testing.T) {
`
b {
a = foo
}`,
}
`,
&BlockSpec{
TypeName: "b",
Nested: &AttrSpec{

View File

@ -24,7 +24,7 @@ func TestParse(t *testing.T) {
},
},
{
"a = 1",
"a = 1\n",
&Body{
Items: []Node{
&Attribute{
@ -56,6 +56,15 @@ func TestParse(t *testing.T) {
},
},
},
&TokenSeq{
Tokens{
{
Type: zclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
},
},
},
NameTokens: &TokenSeq{Tokens{
{
@ -111,12 +120,21 @@ func TestParse(t *testing.T) {
},
},
},
&TokenSeq{
Tokens{
{
Type: zclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
},
},
},
},
},
},
{
"# aye aye aye\na = 1",
"# aye aye aye\na = 1\n",
&Body{
Items: []Node{
&Attribute{
@ -157,6 +175,15 @@ func TestParse(t *testing.T) {
},
},
},
&TokenSeq{
Tokens{
{
Type: zclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
},
},
},
LeadCommentTokens: &TokenSeq{Tokens{
{
@ -228,6 +255,15 @@ func TestParse(t *testing.T) {
},
},
},
&TokenSeq{
Tokens{
{
Type: zclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
},
},
},
},
},
@ -350,7 +386,7 @@ func TestParse(t *testing.T) {
},
},
{
"# bee bee bee\n\nb = 1", // two newlines separate the comment from the attribute
"# bee bee bee\n\nb = 1\n", // two newlines separate the comment from the attribute
&Body{
Items: []Node{
&Attribute{
@ -382,6 +418,15 @@ func TestParse(t *testing.T) {
},
},
},
&TokenSeq{
Tokens{
{
Type: zclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
},
},
},
NameTokens: &TokenSeq{Tokens{
{
@ -451,6 +496,15 @@ func TestParse(t *testing.T) {
},
},
},
&TokenSeq{
Tokens{
{
Type: zclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
},
},
},
},
},

View File

@ -10,7 +10,8 @@ import (
func TestRoundTrip(t *testing.T) {
tests := []string{
``,
`foo = 1`,
`foo = 1
`,
`
foobar = 1
baz = 1