github.com/hashicorp/hcl/v2@v2.20.0/hclsyntax/peeker.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package hclsyntax 5 6 import ( 7 "bytes" 8 "fmt" 9 "path/filepath" 10 "runtime" 11 "strings" 12 13 "github.com/hashicorp/hcl/v2" 14 ) 15 16 // This is set to true at init() time in tests, to enable more useful output 17 // if a stack discipline error is detected. It should not be enabled in 18 // normal mode since there is a performance penalty from accessing the 19 // runtime stack to produce the traces, but could be temporarily set to 20 // true for debugging if desired. 21 var tracePeekerNewlinesStack = false 22 23 type peeker struct { 24 Tokens Tokens 25 NextIndex int 26 27 IncludeComments bool 28 IncludeNewlinesStack []bool 29 30 // used only when tracePeekerNewlinesStack is set 31 newlineStackChanges []peekerNewlineStackChange 32 } 33 34 // for use in debugging the stack usage only 35 type peekerNewlineStackChange struct { 36 Pushing bool // if false, then popping 37 Frame runtime.Frame 38 Include bool 39 } 40 41 func newPeeker(tokens Tokens, includeComments bool) *peeker { 42 return &peeker{ 43 Tokens: tokens, 44 IncludeComments: includeComments, 45 46 IncludeNewlinesStack: []bool{true}, 47 } 48 } 49 50 func (p *peeker) Peek() Token { 51 ret, _ := p.nextToken() 52 return ret 53 } 54 55 func (p *peeker) Read() Token { 56 ret, nextIdx := p.nextToken() 57 p.NextIndex = nextIdx 58 return ret 59 } 60 61 func (p *peeker) NextRange() hcl.Range { 62 return p.Peek().Range 63 } 64 65 func (p *peeker) PrevRange() hcl.Range { 66 if p.NextIndex == 0 { 67 return p.NextRange() 68 } 69 70 return p.Tokens[p.NextIndex-1].Range 71 } 72 73 func (p *peeker) nextToken() (Token, int) { 74 for i := p.NextIndex; i < len(p.Tokens); i++ { 75 tok := p.Tokens[i] 76 switch tok.Type { 77 case TokenComment: 78 if !p.IncludeComments { 79 // Single-line comment tokens, starting with # or //, absorb 80 // the trailing newline that terminates them as part of their 81 // bytes. When we're filtering out comments, we must as a 82 // special case transform these to newline tokens in order 83 // to properly parse newline-terminated block items. 84 85 if p.includingNewlines() { 86 if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' { 87 fakeNewline := Token{ 88 Type: TokenNewline, 89 Bytes: tok.Bytes[len(tok.Bytes)-1 : len(tok.Bytes)], 90 91 // We use the whole token range as the newline 92 // range, even though that's a little... weird, 93 // because otherwise we'd need to go count 94 // characters again in order to figure out the 95 // column of the newline, and that complexity 96 // isn't justified when ranges of newlines are 97 // so rarely printed anyway. 98 Range: tok.Range, 99 } 100 return fakeNewline, i + 1 101 } 102 } 103 104 continue 105 } 106 case TokenNewline: 107 if !p.includingNewlines() { 108 continue 109 } 110 } 111 112 return tok, i + 1 113 } 114 115 // if we fall out here then we'll return the EOF token, and leave 116 // our index pointed off the end of the array so we'll keep 117 // returning EOF in future too. 118 return p.Tokens[len(p.Tokens)-1], len(p.Tokens) 119 } 120 121 func (p *peeker) includingNewlines() bool { 122 return p.IncludeNewlinesStack[len(p.IncludeNewlinesStack)-1] 123 } 124 125 func (p *peeker) PushIncludeNewlines(include bool) { 126 if tracePeekerNewlinesStack { 127 // Record who called us so that we can more easily track down any 128 // mismanagement of the stack in the parser. 129 callers := []uintptr{0} 130 runtime.Callers(2, callers) 131 frames := runtime.CallersFrames(callers) 132 frame, _ := frames.Next() 133 p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{ 134 true, frame, include, 135 }) 136 } 137 138 p.IncludeNewlinesStack = append(p.IncludeNewlinesStack, include) 139 } 140 141 func (p *peeker) PopIncludeNewlines() bool { 142 stack := p.IncludeNewlinesStack 143 remain, ret := stack[:len(stack)-1], stack[len(stack)-1] 144 p.IncludeNewlinesStack = remain 145 146 if tracePeekerNewlinesStack { 147 // Record who called us so that we can more easily track down any 148 // mismanagement of the stack in the parser. 149 callers := []uintptr{0} 150 runtime.Callers(2, callers) 151 frames := runtime.CallersFrames(callers) 152 frame, _ := frames.Next() 153 p.newlineStackChanges = append(p.newlineStackChanges, peekerNewlineStackChange{ 154 false, frame, ret, 155 }) 156 } 157 158 return ret 159 } 160 161 // AssertEmptyNewlinesStack checks if the IncludeNewlinesStack is empty, doing 162 // panicking if it is not. This can be used to catch stack mismanagement that 163 // might otherwise just cause confusing downstream errors. 164 // 165 // This function is a no-op if the stack is empty when called. 166 // 167 // If newlines stack tracing is enabled by setting the global variable 168 // tracePeekerNewlinesStack at init time, a full log of all of the push/pop 169 // calls will be produced to help identify which caller in the parser is 170 // misbehaving. 171 func (p *peeker) AssertEmptyIncludeNewlinesStack() { 172 if len(p.IncludeNewlinesStack) != 1 { 173 // Should never happen; indicates mismanagement of the stack inside 174 // the parser. 175 if p.newlineStackChanges != nil { // only if traceNewlinesStack is enabled above 176 panic(fmt.Errorf( 177 "non-empty IncludeNewlinesStack after parse with %d calls unaccounted for:\n%s", 178 len(p.IncludeNewlinesStack)-1, 179 formatPeekerNewlineStackChanges(p.newlineStackChanges), 180 )) 181 } else { 182 panic(fmt.Errorf("non-empty IncludeNewlinesStack after parse: %#v", p.IncludeNewlinesStack)) 183 } 184 } 185 } 186 187 func formatPeekerNewlineStackChanges(changes []peekerNewlineStackChange) string { 188 indent := 0 189 var buf bytes.Buffer 190 for _, change := range changes { 191 funcName := change.Frame.Function 192 if idx := strings.LastIndexByte(funcName, '.'); idx != -1 { 193 funcName = funcName[idx+1:] 194 } 195 filename := change.Frame.File 196 if idx := strings.LastIndexByte(filename, filepath.Separator); idx != -1 { 197 filename = filename[idx+1:] 198 } 199 200 switch change.Pushing { 201 202 case true: 203 buf.WriteString(strings.Repeat(" ", indent)) 204 fmt.Fprintf(&buf, "PUSH %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line) 205 indent++ 206 207 case false: 208 indent-- 209 buf.WriteString(strings.Repeat(" ", indent)) 210 fmt.Fprintf(&buf, "POP %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line) 211 212 } 213 } 214 return buf.String() 215 }