10 "github.com/hashicorp/hcl2/hcl"
13 // This is set to true at init() time in tests, to enable more useful output
14 // if a stack discipline error is detected. It should not be enabled in
15 // normal mode since there is a performance penalty from accessing the
16 // runtime stack to produce the traces, but could be temporarily set to
17 // true for debugging if desired.
18 var tracePeekerNewlinesStack = false
25 IncludeNewlinesStack []bool
27 // used only when tracePeekerNewlinesStack is set
28 newlineStackChanges []peekerNewlineStackChange
31 // for use in debugging the stack usage only
32 type peekerNewlineStackChange struct {
33 Pushing bool // if false, then popping
38 func newPeeker(tokens Tokens, includeComments bool) *peeker {
41 IncludeComments: includeComments,
43 IncludeNewlinesStack: []bool{true},
47 func (p *peeker) Peek() Token {
48 ret, _ := p.nextToken()
52 func (p *peeker) Read() Token {
53 ret, nextIdx := p.nextToken()
58 func (p *peeker) NextRange() hcl.Range {
62 func (p *peeker) PrevRange() hcl.Range {
67 return p.Tokens[p.NextIndex-1].Range
70 func (p *peeker) nextToken() (Token, int) {
71 for i := p.NextIndex; i < len(p.Tokens); i++ {
75 if !p.IncludeComments {
76 // Single-line comment tokens, starting with # or //, absorb
77 // the trailing newline that terminates them as part of their
78 // bytes. When we're filtering out comments, we must as a
79 // special case transform these to newline tokens in order
80 // to properly parse newline-terminated block items.
82 if p.includingNewlines() {
83 if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' {
86 Bytes: tok.Bytes[len(tok.Bytes)-1 : len(tok.Bytes)],
88 // We use the whole token range as the newline
89 // range, even though that's a little... weird,
90 // because otherwise we'd need to go count
91 // characters again in order to figure out the
92 // column of the newline, and that complexity
93 // isn't justified when ranges of newlines are
94 // so rarely printed anyway.
97 return fakeNewline, i + 1
104 if !p.includingNewlines() {
112 // if we fall out here then we'll return the EOF token, and leave
113 // our index pointed off the end of the array so we'll keep
114 // returning EOF in future too.
115 return p.Tokens[len(p.Tokens)-1], len(p.Tokens)
118 func (p *peeker) includingNewlines() bool {
119 return p.IncludeNewlinesStack[len(p.IncludeNewlinesStack)-1]
122 func (p *peeker) PushIncludeNewlines(include bool) {
123 if tracePeekerNewlinesStack {
124 // Record who called us so that we can more easily track down any
125 // mismanagement of the stack in the parser.
126 callers := []uintptr{0}
127 runtime.Callers(2, callers)
128 frames := runtime.CallersFrames(callers)
129 frame, _ := frames.Next()
130 p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
131 true, frame, include,
135 p.IncludeNewlinesStack = append(p.IncludeNewlinesStack, include)
138 func (p *peeker) PopIncludeNewlines() bool {
139 stack := p.IncludeNewlinesStack
140 remain, ret := stack[:len(stack)-1], stack[len(stack)-1]
141 p.IncludeNewlinesStack = remain
143 if tracePeekerNewlinesStack {
144 // Record who called us so that we can more easily track down any
145 // mismanagement of the stack in the parser.
146 callers := []uintptr{0}
147 runtime.Callers(2, callers)
148 frames := runtime.CallersFrames(callers)
149 frame, _ := frames.Next()
150 p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{
158 // AssertEmptyNewlinesStack checks if the IncludeNewlinesStack is empty, doing
159 // panicking if it is not. This can be used to catch stack mismanagement that
160 // might otherwise just cause confusing downstream errors.
162 // This function is a no-op if the stack is empty when called.
164 // If newlines stack tracing is enabled by setting the global variable
165 // tracePeekerNewlinesStack at init time, a full log of all of the push/pop
166 // calls will be produced to help identify which caller in the parser is
168 func (p *peeker) AssertEmptyIncludeNewlinesStack() {
169 if len(p.IncludeNewlinesStack) != 1 {
170 // Should never happen; indicates mismanagement of the stack inside
172 if p.newlineStackChanges != nil { // only if traceNewlinesStack is enabled above
174 "non-empty IncludeNewlinesStack after parse with %d calls unaccounted for:\n%s",
175 len(p.IncludeNewlinesStack)-1,
176 formatPeekerNewlineStackChanges(p.newlineStackChanges),
179 panic(fmt.Errorf("non-empty IncludeNewlinesStack after parse: %#v", p.IncludeNewlinesStack))
184 func formatPeekerNewlineStackChanges(changes []peekerNewlineStackChange) string {
187 for _, change := range changes {
188 funcName := change.Frame.Function
189 if idx := strings.LastIndexByte(funcName, '.'); idx != -1 {
190 funcName = funcName[idx+1:]
192 filename := change.Frame.File
193 if idx := strings.LastIndexByte(filename, filepath.Separator); idx != -1 {
194 filename = filename[idx+1:]
197 switch change.Pushing {
200 buf.WriteString(strings.Repeat(" ", indent))
201 fmt.Fprintf(&buf, "PUSH %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)
206 buf.WriteString(strings.Repeat(" ", indent))
207 fmt.Fprintf(&buf, "POP %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line)