blob: 4daee9fba81bff57cdfbfd6937257f5f8af8def6 [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package repl
import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
// ExpressionEntryCouldContinue is a helper for terraform console's interactive
// mode which serves as a heuristic for whether it seems like the author might
// be trying to split an expression over multiple lines of input.
//
// The current heuristic is whether there's at least one bracketing delimiter
// that isn't closed, but only if any closing brackets already present are
// properly balanced.
//
// This function also always returns false if the last line entered is empty,
// because that seems likely to represent a user trying to force Terraform to
// accept something that didn't pass the heuristic for some reason, at which
// point Terraform can try to evaluate the expression and return an error if
// it's invalid syntax.
func ExpressionEntryCouldContinue(linesSoFar []string) bool {
if len(linesSoFar) == 0 || strings.TrimSpace(linesSoFar[len(linesSoFar)-1]) == "" {
// If there's no input at all or if the last line is empty other than
// spaces, we assume the user is trying to force Terraform to evaluate
// what they entered so far without any further continuation.
return false
}
// We use capacity 8 here as a compromise assuming that most reasonable
// input entered at the console prompt will not use more than eight
// levels of nesting, but even if it does then we'll just reallocate the
// slice and so it's not a big deal.
delimStack := make([]hclsyntax.TokenType, 0, 8)
push := func(typ hclsyntax.TokenType) {
delimStack = append(delimStack, typ)
}
pop := func() hclsyntax.TokenType {
if len(delimStack) == 0 {
return hclsyntax.TokenInvalid
}
ret := delimStack[len(delimStack)-1]
delimStack = delimStack[:len(delimStack)-1]
return ret
}
// We need to scan this all as one string because the HCL lexer has a few
// special cases where it tracks open/close state itself, such as in heredocs.
all := strings.Join(linesSoFar, "\n") + "\n"
toks, diags := hclsyntax.LexExpression([]byte(all), "", hcl.InitialPos)
if diags.HasErrors() {
return false // bail early if the input is already invalid
}
for _, tok := range toks {
switch tok.Type {
case hclsyntax.TokenOBrace, hclsyntax.TokenOBrack, hclsyntax.TokenOParen, hclsyntax.TokenOHeredoc, hclsyntax.TokenTemplateInterp, hclsyntax.TokenTemplateControl:
// Opening delimiters go on our stack so that we can hopefully
// match them with closing delimiters later.
push(tok.Type)
case hclsyntax.TokenCBrace:
open := pop()
if open != hclsyntax.TokenOBrace {
return false
}
case hclsyntax.TokenCBrack:
open := pop()
if open != hclsyntax.TokenOBrack {
return false
}
case hclsyntax.TokenCParen:
open := pop()
if open != hclsyntax.TokenOParen {
return false
}
case hclsyntax.TokenCHeredoc:
open := pop()
if open != hclsyntax.TokenOHeredoc {
return false
}
case hclsyntax.TokenTemplateSeqEnd:
open := pop()
if open != hclsyntax.TokenTemplateInterp && open != hclsyntax.TokenTemplateControl {
return false
}
}
}
// If we get here without returning early then all of the closing delimeters
// were matched by opening delimiters. If our stack still contains at least
// one opening bracket then it seems like the user is intending to type
// more.
return len(delimStack) != 0
}