6c4344623b
The main HCL package is more visible this way, and so it's easier than having to pick it out from dozens of other package directories.
213 lines
5.9 KiB
Go
213 lines
5.9 KiB
Go
package hclsyntax
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
)
|
|
|
|
// This is set to true at init() time in tests, to enable more useful output
|
|
// if a stack discipline error is detected. It should not be enabled in
|
|
// normal mode since there is a performance penalty from accessing the
|
|
// runtime stack to produce the traces, but could be temporarily set to
|
|
// true for debugging if desired.
|
|
var tracePeekerNewlinesStack = false
|
|
|
|
type peeker struct {
|
|
Tokens Tokens
|
|
NextIndex int
|
|
|
|
IncludeComments bool
|
|
IncludeNewlinesStack []bool
|
|
|
|
// used only when tracePeekerNewlinesStack is set
|
|
newlineStackChanges []peekerNewlineStackChange
|
|
}
|
|
|
|
// for use in debugging the stack usage only
|
|
type peekerNewlineStackChange struct {
|
|
Pushing bool // if false, then popping
|
|
Frame runtime.Frame
|
|
Include bool
|
|
}
|
|
|
|
func newPeeker(tokens Tokens, includeComments bool) *peeker {
|
|
return &peeker{
|
|
Tokens: tokens,
|
|
IncludeComments: includeComments,
|
|
|
|
IncludeNewlinesStack: []bool{true},
|
|
}
|
|
}
|
|
|
|
func (p *peeker) Peek() Token {
|
|
ret, _ := p.nextToken()
|
|
return ret
|
|
}
|
|
|
|
func (p *peeker) Read() Token {
|
|
ret, nextIdx := p.nextToken()
|
|
p.NextIndex = nextIdx
|
|
return ret
|
|
}
|
|
|
|
func (p *peeker) NextRange() hcl.Range {
|
|
return p.Peek().Range
|
|
}
|
|
|
|
func (p *peeker) PrevRange() hcl.Range {
|
|
if p.NextIndex == 0 {
|
|
return p.NextRange()
|
|
}
|
|
|
|
return p.Tokens[p.NextIndex-1].Range
|
|
}
|
|
|
|
func (p *peeker) nextToken() (Token, int) {
|
|
for i := p.NextIndex; i < len(p.Tokens); i++ {
|
|
tok := p.Tokens[i]
|
|
switch tok.Type {
|
|
case TokenComment:
|
|
if !p.IncludeComments {
|
|
// Single-line comment tokens, starting with # or //, absorb
|
|
// the trailing newline that terminates them as part of their
|
|
// bytes. When we're filtering out comments, we must as a
|
|
// special case transform these to newline tokens in order
|
|
// to properly parse newline-terminated block items.
|
|
|
|
if p.includingNewlines() {
|
|
if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' {
|
|
fakeNewline := Token{
|
|
Type: TokenNewline,
|
|
Bytes: tok.Bytes[len(tok.Bytes)-1 : len(tok.Bytes)],
|
|
|
|
// We use the whole token range as the newline
|
|
// range, even though that's a little... weird,
|
|
// because otherwise we'd need to go count
|
|
// characters again in order to figure out the
|
|
// column of the newline, and that complexity
|
|
// isn't justified when ranges of newlines are
|
|
// so rarely printed anyway.
|
|
Range: tok.Range,
|
|
}
|
|
return fakeNewline, i + 1
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
case TokenNewline:
|
|
if !p.includingNewlines() {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return tok, i + 1
|
|
}
|
|
|
|
// if we fall out here then we'll return the EOF token, and leave
|
|
// our index pointed off the end of the array so we'll keep
|
|
// returning EOF in future too.
|
|
return p.Tokens[len(p.Tokens)-1], len(p.Tokens)
|
|
}
|
|
|
|
func (p *peeker) includingNewlines() bool {
|
|
return p.IncludeNewlinesStack[len(p.IncludeNewlinesStack)-1]
|
|
}
|
|
|
|
func (p *peeker) PushIncludeNewlines(include bool) {
|
|
if tracePeekerNewlinesStack {
|
|
// Record who called us so that we can more easily track down any
|
|
// mismanagement of the stack in the parser.
|
|
callers := []uintptr{0}
|
|
runtime.Callers(2, callers)
|
|
frames := runtime.CallersFrames(callers)
|
|
frame, _ := frames.Next()
|
|
p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
|
|
true, frame, include,
|
|
})
|
|
}
|
|
|
|
p.IncludeNewlinesStack = append(p.IncludeNewlinesStack, include)
|
|
}
|
|
|
|
func (p *peeker) PopIncludeNewlines() bool {
|
|
stack := p.IncludeNewlinesStack
|
|
remain, ret := stack[:len(stack)-1], stack[len(stack)-1]
|
|
p.IncludeNewlinesStack = remain
|
|
|
|
if tracePeekerNewlinesStack {
|
|
// Record who called us so that we can more easily track down any
|
|
// mismanagement of the stack in the parser.
|
|
callers := []uintptr{0}
|
|
runtime.Callers(2, callers)
|
|
frames := runtime.CallersFrames(callers)
|
|
frame, _ := frames.Next()
|
|
p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
|
|
false, frame, ret,
|
|
})
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// AssertEmptyNewlinesStack checks if the IncludeNewlinesStack is empty, doing
|
|
// panicking if it is not. This can be used to catch stack mismanagement that
|
|
// might otherwise just cause confusing downstream errors.
|
|
//
|
|
// This function is a no-op if the stack is empty when called.
|
|
//
|
|
// If newlines stack tracing is enabled by setting the global variable
|
|
// tracePeekerNewlinesStack at init time, a full log of all of the push/pop
|
|
// calls will be produced to help identify which caller in the parser is
|
|
// misbehaving.
|
|
func (p *peeker) AssertEmptyIncludeNewlinesStack() {
|
|
if len(p.IncludeNewlinesStack) != 1 {
|
|
// Should never happen; indicates mismanagement of the stack inside
|
|
// the parser.
|
|
if p.newlineStackChanges != nil { // only if traceNewlinesStack is enabled above
|
|
panic(fmt.Errorf(
|
|
"non-empty IncludeNewlinesStack after parse with %d calls unaccounted for:\n%s",
|
|
len(p.IncludeNewlinesStack)-1,
|
|
formatPeekerNewlineStackChanges(p.newlineStackChanges),
|
|
))
|
|
} else {
|
|
panic(fmt.Errorf("non-empty IncludeNewlinesStack after parse: %#v", p.IncludeNewlinesStack))
|
|
}
|
|
}
|
|
}
|
|
|
|
func formatPeekerNewlineStackChanges(changes []peekerNewlineStackChange) string {
|
|
indent := 0
|
|
var buf bytes.Buffer
|
|
for _, change := range changes {
|
|
funcName := change.Frame.Function
|
|
if idx := strings.LastIndexByte(funcName, '.'); idx != -1 {
|
|
funcName = funcName[idx+1:]
|
|
}
|
|
filename := change.Frame.File
|
|
if idx := strings.LastIndexByte(filename, filepath.Separator); idx != -1 {
|
|
filename = filename[idx+1:]
|
|
}
|
|
|
|
switch change.Pushing {
|
|
|
|
case true:
|
|
buf.WriteString(strings.Repeat(" ", indent))
|
|
fmt.Fprintf(&buf, "PUSH %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)
|
|
indent++
|
|
|
|
case false:
|
|
indent--
|
|
buf.WriteString(strings.Repeat(" ", indent))
|
|
fmt.Fprintf(&buf, "POP %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)
|
|
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|