| 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() |
| } |