diff --git a/hclsyntax/parser.go b/hclsyntax/parser.go index ff72713f..287e8293 100644 --- a/hclsyntax/parser.go +++ b/hclsyntax/parser.go @@ -76,14 +76,37 @@ Token: default: bad := p.Read() if !p.recovery { - if bad.Type == TokenOQuote { + switch bad.Type { + case TokenOQuote: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid argument name", Detail: "Argument names must not be quoted.", Subject: &bad.Range, }) - } else { + case TokenEOF: + switch end { + case TokenCBrace: + // If we're looking for a closing brace then we're parsing a block + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unclosed configuration block", + 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.", + Subject: &startRange, + }) + default: + // The only other "end" should itself be TokenEOF (for + // the top-level body) and so we shouldn't get here, + // but we'll return a generic error message anyway to + // be resilient. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unclosed configuration body", + Detail: "Found end of file before the end of this configuration body.", + Subject: &startRange, + }) + } + default: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Argument or block definition required", @@ -388,12 +411,23 @@ Token: // 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(), - }) + switch p.Peek().Type { + case TokenEOF: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unclosed configuration block", + 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.", + Subject: oBrace.Range.Ptr(), + Context: hcl.RangeBetween(ident.Range, oBrace.Range).Ptr(), + }) + default: + 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) } @@ -1059,12 +1093,22 @@ func (p *parser) parseExpressionTerm() (Expression, hcl.Diagnostics) { default: var diags hcl.Diagnostics if !p.recovery { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid expression", - Detail: "Expected the start of an expression, but found an invalid expression token.", - Subject: &start.Range, - }) + switch start.Type { + case TokenEOF: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing expression", + Detail: "Expected the start of an expression, but found the end of the file.", + Subject: &start.Range, + }) + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid expression", + Detail: "Expected the start of an expression, but found an invalid expression token.", + Subject: &start.Range, + }) + } } p.setRecovery() @@ -1163,13 +1207,23 @@ Token: } if sep.Type != TokenComma { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing argument separator", - Detail: "A comma is required to separate each function argument from the next.", - Subject: &sep.Range, - Context: hcl.RangeBetween(name.Range, sep.Range).Ptr(), - }) + switch sep.Type { + case TokenEOF: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unterminated function call", + 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.", + Subject: hcl.RangeBetween(name.Range, openTok.Range).Ptr(), + }) + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing argument separator", + Detail: "A comma is required to separate each function argument from the next.", + Subject: &sep.Range, + Context: hcl.RangeBetween(name.Range, sep.Range).Ptr(), + }) + } closeTok = p.recover(TokenCParen) break Token } @@ -1242,13 +1296,23 @@ func (p *parser) parseTupleCons() (Expression, hcl.Diagnostics) { if next.Type != TokenComma { if !p.recovery { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing item separator", - Detail: "Expected a comma to mark the beginning of the next item.", - Subject: &next.Range, - Context: hcl.RangeBetween(open.Range, next.Range).Ptr(), - }) + switch next.Type { + case TokenEOF: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unterminated tuple constructor expression", + 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.", + Subject: open.Range.Ptr(), + }) + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing item separator", + Detail: "Expected a comma to mark the beginning of the next item.", + Subject: &next.Range, + Context: hcl.RangeBetween(open.Range, next.Range).Ptr(), + }) + } } close = p.recover(TokenCBrack) break @@ -1359,6 +1423,13 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) { Subject: &next.Range, Context: hcl.RangeBetween(open.Range, next.Range).Ptr(), }) + case TokenEOF: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unterminated object constructor expression", + 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.", + Subject: open.Range.Ptr(), + }) default: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -1399,13 +1470,23 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) { if next.Type != TokenComma && next.Type != TokenNewline { if !p.recovery { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing attribute separator", - Detail: "Expected a newline or comma to mark the beginning of the next attribute.", - Subject: &next.Range, - Context: hcl.RangeBetween(open.Range, next.Range).Ptr(), - }) + switch next.Type { + case TokenEOF: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unterminated object constructor expression", + 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.", + Subject: open.Range.Ptr(), + }) + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing attribute separator", + Detail: "Expected a newline or comma to mark the beginning of the next attribute.", + Subject: &next.Range, + Context: hcl.RangeBetween(open.Range, next.Range).Ptr(), + }) + } } close = p.recover(TokenCBrace) break diff --git a/hclsyntax/parser_template.go b/hclsyntax/parser_template.go index 02181fc7..ae880585 100644 --- a/hclsyntax/parser_template.go +++ b/hclsyntax/parser_template.go @@ -414,22 +414,42 @@ Token: if close.Type != TokenTemplateSeqEnd { if !p.recovery { switch close.Type { - case TokenColon: + case TokenEOF: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Extra characters after interpolation expression", - 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 \"${\".", - Subject: &close.Range, - Context: hcl.RangeBetween(startRange, close.Range).Ptr(), + Summary: "Unclosed template interpolation sequence", + 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.", + Subject: &startRange, }) - default: + case TokenColon: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Extra characters after interpolation expression", - 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 \"${\".", + 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 \"${\".", Subject: &close.Range, Context: hcl.RangeBetween(startRange, close.Range).Ptr(), }) + default: + if (close.Type == TokenCQuote || close.Type == TokenOQuote) && end == TokenCQuote { + // We'll get here if we're processing a _quoted_ + // template and we find an errant quote inside an + // interpolation sequence, which suggests that + // the interpolation sequence is missing its terminator. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unclosed template interpolation sequence", + 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.", + Subject: &startRange, + }) + } else { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Extra characters after interpolation expression", + 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 \"${\".", + Subject: &close.Range, + Context: hcl.RangeBetween(startRange, close.Range).Ptr(), + }) + } } } p.recover(TokenTemplateSeqEnd) diff --git a/hclsyntax/parser_test.go b/hclsyntax/parser_test.go index 8b083da8..e347d399 100644 --- a/hclsyntax/parser_test.go +++ b/hclsyntax/parser_test.go @@ -2565,3 +2565,272 @@ block "valid" {} }) } } + +func TestParseConfigDiagnostics(t *testing.T) { + // This test function is a variant of TestParseConfig which tests for + // specific error messages for certain kinds of invalid input where we + // intend to produce a particular helpful error message. + + tests := map[string]struct { + input string + want hcl.Diagnostics + }{ + "unclosed multi-line block (no contents)": { + "blah {\n", + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unclosed configuration block", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + End: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + }, + }, + }, + "unclosed multi-line block (after one argument)": { + "blah {\n a = 1\n", + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unclosed configuration block", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + End: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + }, + }, + }, + "unclosed single-line block (no contents)": { + "blah {", + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unclosed configuration block", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + End: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + }, + }, + }, + "unclosed single-line block (after its argument)": { + "blah { a = 1", + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unclosed configuration block", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + End: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + Context: &hcl.Range{ // In this case we can also report a context because we detect this error in a more convenient place in the parser + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + }, + }, + }, + }, + "unclosed object constructor (before element separator)": { + `foo = { a = 1`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unterminated object constructor expression", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + "unclosed object constructor (before equals)": { + `foo = { a `, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unterminated object constructor expression", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + "unclosed tuple constructor (before element separator)": { + `foo = [ a`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unterminated tuple constructor expression", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + "unclosed function call": { + `foo = boop("a"`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unterminated function call", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + }, + }, + }, + "unclosed grouping parentheses": { + `foo = (1`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unbalanced parentheses", + Detail: "Expected a closing parenthesis to terminate the expression.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + Context: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 7, Byte: 6}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + }, + "unclosed template interpolation at EOF": { + `foo = "${a`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unclosed template interpolation sequence", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + "unclosed quoted template interpolation at closing quote": { + `foo = "${a"`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unclosed template interpolation sequence", + 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.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }, + "unclosed quoted template at literal part": { + `foo = "${a}`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Unterminated template string", + Detail: "No closing marker was found for the string.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + Context: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + }, + }, + }, + + // Some of our "unclosed" situations happen at a less convenient time + // when we only know we're waiting for an expression, so those get + // an error message with much less context. + "unclosed object constructor (before any expression)": { + `foo = {`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Missing expression", + Detail: "Expected the start of an expression, but found the end of the file.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + "unclosed tuple constructor (before any expression)": { + `foo = [`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Missing expression", + Detail: "Expected the start of an expression, but found the end of the file.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }, + "unclosed function call (before any argument)": { + `foo = foo(`, + hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Missing expression", + Detail: "Expected the start of an expression, but found the end of the file.", + Subject: &hcl.Range{ + Filename: "test.hcl", + Start: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + End: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Logf("\n%s", test.input) + _, diags := ParseConfig([]byte(test.input), "test.hcl", hcl.InitialPos) + + if diff := deep.Equal(diags, test.want); diff != nil { + for _, problem := range diff { + t.Errorf(problem) + } + } + }) + } +} diff --git a/specsuite/tests/structure/blocks/single_unclosed.t b/specsuite/tests/structure/blocks/single_unclosed.t index 2792e2c9..66c8c753 100644 --- a/specsuite/tests/structure/blocks/single_unclosed.t +++ b/specsuite/tests/structure/blocks/single_unclosed.t @@ -1,14 +1,14 @@ diagnostics { error { from { - line = 2 - column = 1 - byte = 4 + line = 1 + column = 3 + byte = 2 } to { - line = 2 - column = 1 - byte = 4 + line = 1 + column = 4 + byte = 3 } } }