Skip to content

Commit 81b680d

Browse files
hclsyntax: Special error messages for EOF in certain contexts
For parts of the input that are delimited by start and end tokens, our typical pattern is to keep scanning for items until we reach the end token, or to generate an error if we encounter a token that isn't valid. In some common examples of that we'll now treat TokenEOF as special and report a different message about the element being unclosed, because it seems common in practice for folks to leave off closing delimiters and then be confused about HCL reporting a parse error at the end of the file. Instead, we'll now report the error from the perspective of the opening token(s) and describe that construct as "unclosed", because the EOF range is generally less useful than any range that actually contains some relevant characters. This is not totally comprehensive for all cases, but covers some situations that I've seen folks ask about, and some others that were similar enough to those that it was easy to modify them in the same ways. This does actually change one of the error ranges constrained by the specification test suite, but in practice we're not actually using that test suite to represent the "specification" for HCL, so it's better to change the hypothetical specification to call for a better error reporting behavior than to retain the old behavior just because we happened to encoded in the (unfinished) test suite.
1 parent e84201c commit 81b680d

4 files changed

Lines changed: 418 additions & 48 deletions

File tree

hclsyntax/parser.go

Lines changed: 116 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,37 @@ Token:
7676
default:
7777
bad := p.Read()
7878
if !p.recovery {
79-
if bad.Type == TokenOQuote {
79+
switch bad.Type {
80+
case TokenOQuote:
8081
diags = append(diags, &hcl.Diagnostic{
8182
Severity: hcl.DiagError,
8283
Summary: "Invalid argument name",
8384
Detail: "Argument names must not be quoted.",
8485
Subject: &bad.Range,
8586
})
86-
} else {
87+
case TokenEOF:
88+
switch end {
89+
case TokenCBrace:
90+
// If we're looking for a closing brace then we're parsing a block
91+
diags = append(diags, &hcl.Diagnostic{
92+
Severity: hcl.DiagError,
93+
Summary: "Unclosed configuration block",
94+
Detail: "There is no closing brace for this block before the end of the file. This may be caused by incorrect brace nesting elsewhere in this file.",
95+
Subject: &startRange,
96+
})
97+
default:
98+
// The only other "end" should itself be TokenEOF (for
99+
// the top-level body) and so we shouldn't get here,
100+
// but we'll return a generic error message anyway to
101+
// be resilient.
102+
diags = append(diags, &hcl.Diagnostic{
103+
Severity: hcl.DiagError,
104+
Summary: "Unclosed configuration body",
105+
Detail: "Found end of file before the end of this configuration body.",
106+
Subject: &startRange,
107+
})
108+
}
109+
default:
87110
diags = append(diags, &hcl.Diagnostic{
88111
Severity: hcl.DiagError,
89112
Summary: "Argument or block definition required",
@@ -388,12 +411,23 @@ Token:
388411
// user intent for this one, we'll skip it if we're already in
389412
// recovery mode.
390413
if !p.recovery {
391-
diags = append(diags, &hcl.Diagnostic{
392-
Severity: hcl.DiagError,
393-
Summary: "Invalid single-argument block definition",
394-
Detail: "A single-line block definition must end with a closing brace immediately after its single argument definition.",
395-
Subject: p.Peek().Range.Ptr(),
396-
})
414+
switch p.Peek().Type {
415+
case TokenEOF:
416+
diags = append(diags, &hcl.Diagnostic{
417+
Severity: hcl.DiagError,
418+
Summary: "Unclosed configuration block",
419+
Detail: "There is no closing brace for this block before the end of the file. This may be caused by incorrect brace nesting elsewhere in this file.",
420+
Subject: oBrace.Range.Ptr(),
421+
Context: hcl.RangeBetween(ident.Range, oBrace.Range).Ptr(),
422+
})
423+
default:
424+
diags = append(diags, &hcl.Diagnostic{
425+
Severity: hcl.DiagError,
426+
Summary: "Invalid single-argument block definition",
427+
Detail: "A single-line block definition must end with a closing brace immediately after its single argument definition.",
428+
Subject: p.Peek().Range.Ptr(),
429+
})
430+
}
397431
}
398432
p.recover(TokenCBrace)
399433
}
@@ -1059,12 +1093,22 @@ func (p *parser) parseExpressionTerm() (Expression, hcl.Diagnostics) {
10591093
default:
10601094
var diags hcl.Diagnostics
10611095
if !p.recovery {
1062-
diags = append(diags, &hcl.Diagnostic{
1063-
Severity: hcl.DiagError,
1064-
Summary: "Invalid expression",
1065-
Detail: "Expected the start of an expression, but found an invalid expression token.",
1066-
Subject: &start.Range,
1067-
})
1096+
switch start.Type {
1097+
case TokenEOF:
1098+
diags = append(diags, &hcl.Diagnostic{
1099+
Severity: hcl.DiagError,
1100+
Summary: "Missing expression",
1101+
Detail: "Expected the start of an expression, but found the end of the file.",
1102+
Subject: &start.Range,
1103+
})
1104+
default:
1105+
diags = append(diags, &hcl.Diagnostic{
1106+
Severity: hcl.DiagError,
1107+
Summary: "Invalid expression",
1108+
Detail: "Expected the start of an expression, but found an invalid expression token.",
1109+
Subject: &start.Range,
1110+
})
1111+
}
10681112
}
10691113
p.setRecovery()
10701114

@@ -1163,13 +1207,23 @@ Token:
11631207
}
11641208

11651209
if sep.Type != TokenComma {
1166-
diags = append(diags, &hcl.Diagnostic{
1167-
Severity: hcl.DiagError,
1168-
Summary: "Missing argument separator",
1169-
Detail: "A comma is required to separate each function argument from the next.",
1170-
Subject: &sep.Range,
1171-
Context: hcl.RangeBetween(name.Range, sep.Range).Ptr(),
1172-
})
1210+
switch sep.Type {
1211+
case TokenEOF:
1212+
diags = append(diags, &hcl.Diagnostic{
1213+
Severity: hcl.DiagError,
1214+
Summary: "Unterminated function call",
1215+
Detail: "There is no closing parenthesis for this function call before the end of the file. This may be caused by incorrect parethesis nesting elsewhere in this file.",
1216+
Subject: hcl.RangeBetween(name.Range, openTok.Range).Ptr(),
1217+
})
1218+
default:
1219+
diags = append(diags, &hcl.Diagnostic{
1220+
Severity: hcl.DiagError,
1221+
Summary: "Missing argument separator",
1222+
Detail: "A comma is required to separate each function argument from the next.",
1223+
Subject: &sep.Range,
1224+
Context: hcl.RangeBetween(name.Range, sep.Range).Ptr(),
1225+
})
1226+
}
11731227
closeTok = p.recover(TokenCParen)
11741228
break Token
11751229
}
@@ -1242,13 +1296,23 @@ func (p *parser) parseTupleCons() (Expression, hcl.Diagnostics) {
12421296

12431297
if next.Type != TokenComma {
12441298
if !p.recovery {
1245-
diags = append(diags, &hcl.Diagnostic{
1246-
Severity: hcl.DiagError,
1247-
Summary: "Missing item separator",
1248-
Detail: "Expected a comma to mark the beginning of the next item.",
1249-
Subject: &next.Range,
1250-
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
1251-
})
1299+
switch next.Type {
1300+
case TokenEOF:
1301+
diags = append(diags, &hcl.Diagnostic{
1302+
Severity: hcl.DiagError,
1303+
Summary: "Unterminated tuple constructor expression",
1304+
Detail: "There is no corresponding closing bracket before the end of the file. This may be caused by incorrect bracket nesting elsewhere in this file.",
1305+
Subject: open.Range.Ptr(),
1306+
})
1307+
default:
1308+
diags = append(diags, &hcl.Diagnostic{
1309+
Severity: hcl.DiagError,
1310+
Summary: "Missing item separator",
1311+
Detail: "Expected a comma to mark the beginning of the next item.",
1312+
Subject: &next.Range,
1313+
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
1314+
})
1315+
}
12521316
}
12531317
close = p.recover(TokenCBrack)
12541318
break
@@ -1359,6 +1423,13 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) {
13591423
Subject: &next.Range,
13601424
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
13611425
})
1426+
case TokenEOF:
1427+
diags = append(diags, &hcl.Diagnostic{
1428+
Severity: hcl.DiagError,
1429+
Summary: "Unterminated object constructor expression",
1430+
Detail: "There is no corresponding closing brace before the end of the file. This may be caused by incorrect brace nesting elsewhere in this file.",
1431+
Subject: open.Range.Ptr(),
1432+
})
13621433
default:
13631434
diags = append(diags, &hcl.Diagnostic{
13641435
Severity: hcl.DiagError,
@@ -1399,13 +1470,23 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) {
13991470

14001471
if next.Type != TokenComma && next.Type != TokenNewline {
14011472
if !p.recovery {
1402-
diags = append(diags, &hcl.Diagnostic{
1403-
Severity: hcl.DiagError,
1404-
Summary: "Missing attribute separator",
1405-
Detail: "Expected a newline or comma to mark the beginning of the next attribute.",
1406-
Subject: &next.Range,
1407-
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
1408-
})
1473+
switch next.Type {
1474+
case TokenEOF:
1475+
diags = append(diags, &hcl.Diagnostic{
1476+
Severity: hcl.DiagError,
1477+
Summary: "Unterminated object constructor expression",
1478+
Detail: "There is no corresponding closing brace before the end of the file. This may be caused by incorrect brace nesting elsewhere in this file.",
1479+
Subject: open.Range.Ptr(),
1480+
})
1481+
default:
1482+
diags = append(diags, &hcl.Diagnostic{
1483+
Severity: hcl.DiagError,
1484+
Summary: "Missing attribute separator",
1485+
Detail: "Expected a newline or comma to mark the beginning of the next attribute.",
1486+
Subject: &next.Range,
1487+
Context: hcl.RangeBetween(open.Range, next.Range).Ptr(),
1488+
})
1489+
}
14091490
}
14101491
close = p.recover(TokenCBrace)
14111492
break

hclsyntax/parser_template.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,22 +414,42 @@ Token:
414414
if close.Type != TokenTemplateSeqEnd {
415415
if !p.recovery {
416416
switch close.Type {
417-
case TokenColon:
417+
case TokenEOF:
418418
diags = append(diags, &hcl.Diagnostic{
419419
Severity: hcl.DiagError,
420-
Summary: "Extra characters after interpolation expression",
421-
Detail: "Template interpolation doesn't expect a colon at this location. Did you intend this to be a literal sequence to be processed as part of another language? If so, you can escape it by starting with \"$${\" instead of just \"${\".",
422-
Subject: &close.Range,
423-
Context: hcl.RangeBetween(startRange, close.Range).Ptr(),
420+
Summary: "Unclosed template interpolation sequence",
421+
Detail: "There is no closing brace for this interpolation sequence before the end of the file. This might be caused by incorrect nesting inside the given expression.",
422+
Subject: &startRange,
424423
})
425-
default:
424+
case TokenColon:
426425
diags = append(diags, &hcl.Diagnostic{
427426
Severity: hcl.DiagError,
428427
Summary: "Extra characters after interpolation expression",
429-
Detail: "Expected a closing brace to end the interpolation expression, but found extra characters.\n\nThis can happen when you include interpolation syntax for another language, such as shell scripting, but forget to escape the interpolation start token. If this is an embedded sequence for another language, escape it by starting with \"$${\" instead of just \"${\".",
428+
Detail: "Template interpolation doesn't expect a colon at this location. Did you intend this to be a literal sequence to be processed as part of another language? If so, you can escape it by starting with \"$${\" instead of just \"${\".",
430429
Subject: &close.Range,
431430
Context: hcl.RangeBetween(startRange, close.Range).Ptr(),
432431
})
432+
default:
433+
if (close.Type == TokenCQuote || close.Type == TokenOQuote) && end == TokenCQuote {
434+
// We'll get here if we're processing a _quoted_
435+
// template and we find an errant quote inside an
436+
// interpolation sequence, which suggests that
437+
// the interpolation sequence is missing its terminator.
438+
diags = append(diags, &hcl.Diagnostic{
439+
Severity: hcl.DiagError,
440+
Summary: "Unclosed template interpolation sequence",
441+
Detail: "There is no closing brace for this interpolation sequence before the end of the quoted template. This might be caused by incorrect nesting inside the given expression.",
442+
Subject: &startRange,
443+
})
444+
} else {
445+
diags = append(diags, &hcl.Diagnostic{
446+
Severity: hcl.DiagError,
447+
Summary: "Extra characters after interpolation expression",
448+
Detail: "Expected a closing brace to end the interpolation expression, but found extra characters.\n\nThis can happen when you include interpolation syntax for another language, such as shell scripting, but forget to escape the interpolation start token. If this is an embedded sequence for another language, escape it by starting with \"$${\" instead of just \"${\".",
449+
Subject: &close.Range,
450+
Context: hcl.RangeBetween(startRange, close.Range).Ptr(),
451+
})
452+
}
433453
}
434454
}
435455
p.recover(TokenTemplateSeqEnd)

0 commit comments

Comments
 (0)