]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blobdiff - vendor/github.com/hashicorp/hcl2/hcl/hclsyntax/parser.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / hcl2 / hcl / hclsyntax / parser.go
index 002858f46fdb9b3d25f5225946490ebbd1b7cee3..253ad5031a2550406c89b1be7105a47578577b16 100644 (file)
@@ -9,7 +9,6 @@ import (
        "github.com/apparentlymart/go-textseg/textseg"
        "github.com/hashicorp/hcl2/hcl"
        "github.com/zclconf/go-cty/cty"
-       "github.com/zclconf/go-cty/cty/convert"
 )
 
 type parser struct {
@@ -55,7 +54,7 @@ Token:
                                                Severity: hcl.DiagError,
                                                Summary:  "Attribute redefined",
                                                Detail: fmt.Sprintf(
-                                                       "The attribute %q was already defined at %s. Each attribute may be defined only once.",
+                                                       "The argument %q was already set at %s. Each argument may be set only once.",
                                                        titem.Name, existing.NameRange.String(),
                                                ),
                                                Subject: &titem.NameRange,
@@ -80,15 +79,15 @@ Token:
                                if bad.Type == TokenOQuote {
                                        diags = append(diags, &hcl.Diagnostic{
                                                Severity: hcl.DiagError,
-                                               Summary:  "Invalid attribute name",
-                                               Detail:   "Attribute names must not be quoted.",
+                                               Summary:  "Invalid argument name",
+                                               Detail:   "Argument names must not be quoted.",
                                                Subject:  &bad.Range,
                                        })
                                } else {
                                        diags = append(diags, &hcl.Diagnostic{
                                                Severity: hcl.DiagError,
-                                               Summary:  "Attribute or block definition required",
-                                               Detail:   "An attribute or block definition is required here.",
+                                               Summary:  "Argument or block definition required",
+                                               Detail:   "An argument or block definition is required here.",
                                                Subject:  &bad.Range,
                                        })
                                }
@@ -120,8 +119,8 @@ func (p *parser) ParseBodyItem() (Node, hcl.Diagnostics) {
                return nil, hcl.Diagnostics{
                        {
                                Severity: hcl.DiagError,
-                               Summary:  "Attribute or block definition required",
-                               Detail:   "An attribute or block definition is required here.",
+                               Summary:  "Argument or block definition required",
+                               Detail:   "An argument or block definition is required here.",
                                Subject:  &ident.Range,
                        },
                }
@@ -131,7 +130,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:
@@ -139,8 +138,8 @@ func (p *parser) ParseBodyItem() (Node, hcl.Diagnostics) {
                return nil, hcl.Diagnostics{
                        {
                                Severity: hcl.DiagError,
-                               Summary:  "Attribute or block definition required",
-                               Detail:   "An attribute or block definition is required here. To define an attribute, use the equals sign \"=\" to introduce the attribute value.",
+                               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,
                        },
                }
@@ -149,7 +148,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 +230,33 @@ 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 attribute definition",
-                                       Detail:   "An attribute 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 {
+                                       summary := "Missing newline after argument"
+                                       detail := "An argument definition must end with a newline."
+
+                                       if end.Type == TokenComma {
+                                               summary = "Unexpected comma after argument"
+                                               detail = "Argument definitions must be separated by newlines, not commas. " + detail
+                                       }
+
+                                       diags = append(diags, &hcl.Diagnostic{
+                                               Severity: hcl.DiagError,
+                                               Summary:  summary,
+                                               Detail:   detail,
+                                               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
                }
        }
 
@@ -218,19 +293,9 @@ Token:
                        diags = append(diags, labelDiags...)
                        labels = append(labels, label)
                        labelRanges = append(labelRanges, labelRange)
-                       if labelDiags.HasErrors() {
-                               p.recoverAfterBodyItem()
-                               return &Block{
-                                       Type:   blockType,
-                                       Labels: labels,
-                                       Body:   nil,
-
-                                       TypeRange:       ident.Range,
-                                       LabelRanges:     labelRanges,
-                                       OpenBraceRange:  ident.Range, // placeholder
-                                       CloseBraceRange: ident.Range, // placeholder
-                               }, diags
-                       }
+                       // parseQuoteStringLiteral recovers up to the closing quote
+                       // if it encounters problems, so we can continue looking for
+                       // more labels and eventually the block body even.
 
                case TokenIdent:
                        tok = p.Read() // eat token
@@ -244,7 +309,7 @@ Token:
                                diags = append(diags, &hcl.Diagnostic{
                                        Severity: hcl.DiagError,
                                        Summary:  "Invalid block definition",
-                                       Detail:   "The equals sign \"=\" indicates an attribute definition, and must not be used when defining a block.",
+                                       Detail:   "The equals sign \"=\" indicates an argument definition, and must not be used when defining a block.",
                                        Subject:  &tok.Range,
                                        Context:  hcl.RangeBetween(ident.Range, tok.Range).Ptr(),
                                })
@@ -273,7 +338,10 @@ Token:
                        return &Block{
                                Type:   blockType,
                                Labels: labels,
-                               Body:   nil,
+                               Body: &Body{
+                                       SrcRange: ident.Range,
+                                       EndRange: ident.Range,
+                               },
 
                                TypeRange:       ident.Range,
                                LabelRanges:     labelRanges,
@@ -285,7 +353,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()
 
@@ -305,6 +417,17 @@ Token:
                p.recoverAfterBodyItem()
        }
 
+       // We must never produce a nil body, since the caller may attempt to
+       // do analysis of a partial result when there's an error, so we'll
+       // insert a placeholder if we otherwise failed to produce a valid
+       // body due to one of the syntax error paths above.
+       if body == nil && diags.HasErrors() {
+               body = &Body{
+                       SrcRange: hcl.RangeBetween(oBrace.Range, cBraceRange),
+                       EndRange: cBraceRange,
+               }
+       }
+
        return &Block{
                Type:   blockType,
                Labels: labels,
@@ -459,7 +582,14 @@ func (p *parser) parseBinaryOps(ops []map[TokenType]*Operation) (Expression, hcl
 
 func (p *parser) parseExpressionWithTraversals() (Expression, hcl.Diagnostics) {
        term, diags := p.parseExpressionTerm()
-       ret := term
+       ret, moreDiags := p.parseExpressionTraversals(term)
+       diags = append(diags, moreDiags...)
+       return ret, diags
+}
+
+func (p *parser) parseExpressionTraversals(from Expression) (Expression, hcl.Diagnostics) {
+       var diags hcl.Diagnostics
+       ret := from
 
 Traversal:
        for {
@@ -657,44 +787,81 @@ Traversal:
                        // the key value is something constant.
 
                        open := p.Read()
-                       // TODO: If we have a TokenStar inside our brackets, parse as
-                       // a Splat expression: foo[*].baz[0].
-                       var close Token
-                       p.PushIncludeNewlines(false) // arbitrary newlines allowed in brackets
-                       keyExpr, keyDiags := p.ParseExpression()
-                       diags = append(diags, keyDiags...)
-                       if p.recovery && keyDiags.HasErrors() {
-                               close = p.recover(TokenCBrack)
-                       } else {
-                               close = p.Read()
+                       switch p.Peek().Type {
+                       case TokenStar:
+                               // This is a full splat expression, like foo[*], which consumes
+                               // the rest of the traversal steps after it using a recursive
+                               // call to this function.
+                               p.Read() // consume star
+                               close := p.Read()
                                if close.Type != TokenCBrack && !p.recovery {
                                        diags = append(diags, &hcl.Diagnostic{
                                                Severity: hcl.DiagError,
-                                               Summary:  "Missing close bracket on index",
-                                               Detail:   "The index operator must end with a closing bracket (\"]\").",
+                                               Summary:  "Missing close bracket on splat index",
+                                               Detail:   "The star for a full splat operator must be immediately followed by a closing bracket (\"]\").",
                                                Subject:  &close.Range,
                                        })
                                        close = p.recover(TokenCBrack)
                                }
-                       }
-                       p.PopIncludeNewlines()
+                               // Splat expressions use a special "anonymous symbol"  as a
+                               // placeholder in an expression to be evaluated once for each
+                               // item in the source expression.
+                               itemExpr := &AnonSymbolExpr{
+                                       SrcRange: hcl.RangeBetween(open.Range, close.Range),
+                               }
+                               // Now we'll recursively call this same function to eat any
+                               // remaining traversal steps against the anonymous symbol.
+                               travExpr, nestedDiags := p.parseExpressionTraversals(itemExpr)
+                               diags = append(diags, nestedDiags...)
 
-                       if lit, isLit := keyExpr.(*LiteralValueExpr); isLit {
-                               litKey, _ := lit.Value(nil)
-                               rng := hcl.RangeBetween(open.Range, close.Range)
-                               step := hcl.TraverseIndex{
-                                       Key:      litKey,
-                                       SrcRange: rng,
+                               ret = &SplatExpr{
+                                       Source: ret,
+                                       Each:   travExpr,
+                                       Item:   itemExpr,
+
+                                       SrcRange:    hcl.RangeBetween(open.Range, travExpr.Range()),
+                                       MarkerRange: hcl.RangeBetween(open.Range, close.Range),
                                }
-                               ret = makeRelativeTraversal(ret, step, rng)
-                       } else {
-                               rng := hcl.RangeBetween(open.Range, close.Range)
-                               ret = &IndexExpr{
-                                       Collection: ret,
-                                       Key:        keyExpr,
 
-                                       SrcRange:  rng,
-                                       OpenRange: open.Range,
+                       default:
+
+                               var close Token
+                               p.PushIncludeNewlines(false) // arbitrary newlines allowed in brackets
+                               keyExpr, keyDiags := p.ParseExpression()
+                               diags = append(diags, keyDiags...)
+                               if p.recovery && keyDiags.HasErrors() {
+                                       close = p.recover(TokenCBrack)
+                               } else {
+                                       close = p.Read()
+                                       if close.Type != TokenCBrack && !p.recovery {
+                                               diags = append(diags, &hcl.Diagnostic{
+                                                       Severity: hcl.DiagError,
+                                                       Summary:  "Missing close bracket on index",
+                                                       Detail:   "The index operator must end with a closing bracket (\"]\").",
+                                                       Subject:  &close.Range,
+                                               })
+                                               close = p.recover(TokenCBrack)
+                                       }
+                               }
+                               p.PopIncludeNewlines()
+
+                               if lit, isLit := keyExpr.(*LiteralValueExpr); isLit {
+                                       litKey, _ := lit.Value(nil)
+                                       rng := hcl.RangeBetween(open.Range, close.Range)
+                                       step := hcl.TraverseIndex{
+                                               Key:      litKey,
+                                               SrcRange: rng,
+                                       }
+                                       ret = makeRelativeTraversal(ret, step, rng)
+                               } else {
+                                       rng := hcl.RangeBetween(open.Range, close.Range)
+                                       ret = &IndexExpr{
+                                               Collection: ret,
+                                               Key:        keyExpr,
+
+                                               SrcRange:  rng,
+                                               OpenRange: open.Range,
+                                       }
                                }
                        }
 
@@ -813,7 +980,7 @@ func (p *parser) parseExpressionTerm() (Expression, hcl.Diagnostics) {
        case TokenOQuote, TokenOHeredoc:
                open := p.Read() // eat opening marker
                closer := p.oppositeBracket(open.Type)
-               exprs, passthru, _, diags := p.parseTemplateInner(closer)
+               exprs, passthru, _, diags := p.parseTemplateInner(closer, tokenOpensFlushHeredoc(open))
 
                closeRange := p.PrevRange()
 
@@ -891,11 +1058,10 @@ func (p *parser) parseExpressionTerm() (Expression, hcl.Diagnostics) {
 }
 
 func (p *parser) numberLitValue(tok Token) (cty.Value, hcl.Diagnostics) {
-       // We'll lean on the cty converter to do the conversion, to ensure that
-       // the behavior is the same as what would happen if converting a
-       // non-literal string to a number.
-       numStrVal := cty.StringVal(string(tok.Bytes))
-       numVal, err := convert.Convert(numStrVal, cty.Number)
+       // The cty.ParseNumberVal is always the same behavior as converting a
+       // string to a number, ensuring we always interpret decimal numbers in
+       // the same way.
+       numVal, err := cty.ParseNumberVal(string(tok.Bytes))
        if err != nil {
                ret := cty.UnknownVal(cty.Number)
                return ret, hcl.Diagnostics{
@@ -1087,13 +1253,19 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) {
                panic("parseObjectCons called without peeker pointing to open brace")
        }
 
-       p.PushIncludeNewlines(true)
-       defer p.PopIncludeNewlines()
-
-       if forKeyword.TokenMatches(p.Peek()) {
+       // We must temporarily stop looking at newlines here while we check for
+       // a "for" keyword, since for expressions are _not_ newline-sensitive,
+       // even though object constructors are.
+       p.PushIncludeNewlines(false)
+       isFor := forKeyword.TokenMatches(p.Peek())
+       p.PopIncludeNewlines()
+       if isFor {
                return p.finishParsingForExpr(open)
        }
 
+       p.PushIncludeNewlines(true)
+       defer p.PopIncludeNewlines()
+
        var close Token
 
        var diags hcl.Diagnostics
@@ -1132,19 +1304,36 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) {
                next = p.Peek()
                if next.Type != TokenEqual && next.Type != TokenColon {
                        if !p.recovery {
-                               if next.Type == TokenNewline || next.Type == TokenComma {
+                               switch next.Type {
+                               case TokenNewline, TokenComma:
                                        diags = append(diags, &hcl.Diagnostic{
                                                Severity: hcl.DiagError,
-                                               Summary:  "Missing item value",
-                                               Detail:   "Expected an item value, introduced by an equals sign (\"=\").",
+                                               Summary:  "Missing attribute value",
+                                               Detail:   "Expected an attribute value, introduced by an equals sign (\"=\").",
                                                Subject:  &next.Range,
                                                Context:  hcl.RangeBetween(open.Range, next.Range).Ptr(),
                                        })
-                               } else {
+                               case TokenIdent:
+                                       // Although this might just be a plain old missing equals
+                                       // sign before a reference, one way to get here is to try
+                                       // to write an attribute name containing a period followed
+                                       // by a digit, which was valid in HCL1, like this:
+                                       //     foo1.2_bar = "baz"
+                                       // We can't know exactly what the user intended here, but
+                                       // we'll augment our message with an extra hint in this case
+                                       // in case it is helpful.
                                        diags = append(diags, &hcl.Diagnostic{
                                                Severity: hcl.DiagError,
                                                Summary:  "Missing key/value separator",
-                                               Detail:   "Expected an equals sign (\"=\") to mark the beginning of the item value.",
+                                               Detail:   "Expected an equals sign (\"=\") to mark the beginning of the attribute value. If you intended to given an attribute name containing periods or spaces, write the name in quotes to create a string literal.",
+                                               Subject:  &next.Range,
+                                               Context:  hcl.RangeBetween(open.Range, next.Range).Ptr(),
+                                       })
+                               default:
+                                       diags = append(diags, &hcl.Diagnostic{
+                                               Severity: hcl.DiagError,
+                                               Summary:  "Missing key/value separator",
+                                               Detail:   "Expected an equals sign (\"=\") to mark the beginning of the attribute value.",
                                                Subject:  &next.Range,
                                                Context:  hcl.RangeBetween(open.Range, next.Range).Ptr(),
                                        })
@@ -1182,8 +1371,8 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) {
                        if !p.recovery {
                                diags = append(diags, &hcl.Diagnostic{
                                        Severity: hcl.DiagError,
-                                       Summary:  "Missing item separator",
-                                       Detail:   "Expected a newline or comma to mark the beginning of the next item.",
+                                       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(),
                                })
@@ -1205,6 +1394,8 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) {
 }
 
 func (p *parser) finishParsingForExpr(open Token) (Expression, hcl.Diagnostics) {
+       p.PushIncludeNewlines(false)
+       defer p.PopIncludeNewlines()
        introducer := p.Read()
        if !forKeyword.TokenMatches(introducer) {
                // Should never happen if callers are behaving
@@ -1277,7 +1468,7 @@ func (p *parser) finishParsingForExpr(open Token) (Expression, hcl.Diagnostics)
                        diags = append(diags, &hcl.Diagnostic{
                                Severity: hcl.DiagError,
                                Summary:  "Invalid 'for' expression",
-                               Detail:   "For expression requires 'in' keyword after names.",
+                               Detail:   "For expression requires the 'in' keyword after its name declarations.",
                                Subject:  p.Peek().Range.Ptr(),
                                Context:  hcl.RangeBetween(open.Range, p.Peek().Range).Ptr(),
                        })
@@ -1305,7 +1496,7 @@ func (p *parser) finishParsingForExpr(open Token) (Expression, hcl.Diagnostics)
                        diags = append(diags, &hcl.Diagnostic{
                                Severity: hcl.DiagError,
                                Summary:  "Invalid 'for' expression",
-                               Detail:   "For expression requires colon after collection expression.",
+                               Detail:   "For expression requires a colon after the collection expression.",
                                Subject:  p.Peek().Range.Ptr(),
                                Context:  hcl.RangeBetween(open.Range, p.Peek().Range).Ptr(),
                        })
@@ -1459,7 +1650,7 @@ Token:
                case TokenTemplateControl, TokenTemplateInterp:
                        which := "$"
                        if tok.Type == TokenTemplateControl {
-                               which = "!"
+                               which = "%"
                        }
 
                        diags = append(diags, &hcl.Diagnostic{
@@ -1472,7 +1663,16 @@ Token:
                                Subject: &tok.Range,
                                Context: hcl.RangeBetween(oQuote.Range, tok.Range).Ptr(),
                        })
-                       p.recover(TokenTemplateSeqEnd)
+
+                       // Now that we're returning an error callers won't attempt to use
+                       // the result for any real operations, but they might try to use
+                       // the partial AST for other analyses, so we'll leave a marker
+                       // to indicate that there was something invalid in the string to
+                       // help avoid misinterpretation of the partial result
+                       ret.WriteString(which)
+                       ret.WriteString("{ ... }")
+
+                       p.recover(TokenTemplateSeqEnd) // we'll try to keep parsing after the sequence ends
 
                case TokenEOF:
                        diags = append(diags, &hcl.Diagnostic{
@@ -1493,7 +1693,7 @@ Token:
                                Subject:  &tok.Range,
                                Context:  hcl.RangeBetween(oQuote.Range, tok.Range).Ptr(),
                        })
-                       p.recover(TokenOQuote)
+                       p.recover(TokenCQuote)
                        break Token
 
                }