2017-09-11 23:40:37 +00:00
|
|
|
package hclsyntax
|
2017-05-30 01:09:58 +00:00
|
|
|
|
2017-05-30 02:01:37 +00:00
|
|
|
import (
|
2018-02-17 01:37:22 +00:00
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"path/filepath"
|
|
|
|
"runtime"
|
|
|
|
"strings"
|
|
|
|
|
2019-09-09 23:08:19 +00:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
2017-05-30 02:01:37 +00:00
|
|
|
)
|
|
|
|
|
2018-02-17 01:37:22 +00:00
|
|
|
// 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
|
|
|
|
|
2017-05-30 01:09:58 +00:00
|
|
|
type peeker struct {
|
|
|
|
Tokens Tokens
|
|
|
|
NextIndex int
|
|
|
|
|
|
|
|
IncludeComments bool
|
|
|
|
IncludeNewlinesStack []bool
|
2018-02-17 01:37:22 +00:00
|
|
|
|
|
|
|
// 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
|
2017-05-30 01:09:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-09-11 23:40:37 +00:00
|
|
|
func (p *peeker) NextRange() hcl.Range {
|
2017-05-30 02:01:37 +00:00
|
|
|
return p.Peek().Range
|
|
|
|
}
|
|
|
|
|
2017-09-11 23:40:37 +00:00
|
|
|
func (p *peeker) PrevRange() hcl.Range {
|
2017-05-30 02:01:37 +00:00
|
|
|
if p.NextIndex == 0 {
|
|
|
|
return p.NextRange()
|
|
|
|
}
|
|
|
|
|
|
|
|
return p.Tokens[p.NextIndex-1].Range
|
|
|
|
}
|
|
|
|
|
2017-05-30 01:09:58 +00:00
|
|
|
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 {
|
2017-06-09 14:19:25 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-30 01:09:58 +00:00
|
|
|
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) {
|
2018-02-17 01:37:22 +00:00
|
|
|
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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-05-30 01:09:58 +00:00
|
|
|
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
|
2018-02-17 01:37:22 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-05-30 01:09:58 +00:00
|
|
|
return ret
|
|
|
|
}
|
2018-02-17 01:37:22 +00:00
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|