]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package hclsyntax |
2 | ||
3 | import ( | |
4 | "bytes" | |
5 | "fmt" | |
6 | "path/filepath" | |
7 | "runtime" | |
8 | "strings" | |
9 | ||
10 | "github.com/hashicorp/hcl2/hcl" | |
11 | ) | |
12 | ||
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 | |
19 | ||
20 | type peeker struct { | |
21 | Tokens Tokens | |
22 | NextIndex int | |
23 | ||
24 | IncludeComments bool | |
25 | IncludeNewlinesStack []bool | |
26 | ||
27 | // used only when tracePeekerNewlinesStack is set | |
28 | newlineStackChanges []peekerNewlineStackChange | |
29 | } | |
30 | ||
31 | // for use in debugging the stack usage only | |
32 | type peekerNewlineStackChange struct { | |
33 | Pushing bool // if false, then popping | |
34 | Frame runtime.Frame | |
35 | Include bool | |
36 | } | |
37 | ||
38 | func newPeeker(tokens Tokens, includeComments bool) *peeker { | |
39 | return &peeker{ | |
40 | Tokens: tokens, | |
41 | IncludeComments: includeComments, | |
42 | ||
43 | IncludeNewlinesStack: []bool{true}, | |
44 | } | |
45 | } | |
46 | ||
47 | func (p *peeker) Peek() Token { | |
48 | ret, _ := p.nextToken() | |
49 | return ret | |
50 | } | |
51 | ||
52 | func (p *peeker) Read() Token { | |
53 | ret, nextIdx := p.nextToken() | |
54 | p.NextIndex = nextIdx | |
55 | return ret | |
56 | } | |
57 | ||
58 | func (p *peeker) NextRange() hcl.Range { | |
59 | return p.Peek().Range | |
60 | } | |
61 | ||
62 | func (p *peeker) PrevRange() hcl.Range { | |
63 | if p.NextIndex == 0 { | |
64 | return p.NextRange() | |
65 | } | |
66 | ||
67 | return p.Tokens[p.NextIndex-1].Range | |
68 | } | |
69 | ||
70 | func (p *peeker) nextToken() (Token, int) { | |
71 | for i := p.NextIndex; i < len(p.Tokens); i++ { | |
72 | tok := p.Tokens[i] | |
73 | switch tok.Type { | |
74 | case TokenComment: | |
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. | |
81 | ||
82 | if p.includingNewlines() { | |
83 | if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' { | |
84 | fakeNewline := Token{ | |
85 | Type: TokenNewline, | |
86 | Bytes: tok.Bytes[len(tok.Bytes)-1 : len(tok.Bytes)], | |
87 | ||
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. | |
95 | Range: tok.Range, | |
96 | } | |
97 | return fakeNewline, i + 1 | |
98 | } | |
99 | } | |
100 | ||
101 | continue | |
102 | } | |
103 | case TokenNewline: | |
104 | if !p.includingNewlines() { | |
105 | continue | |
106 | } | |
107 | } | |
108 | ||
109 | return tok, i + 1 | |
110 | } | |
111 | ||
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) | |
116 | } | |
117 | ||
118 | func (p *peeker) includingNewlines() bool { | |
119 | return p.IncludeNewlinesStack[len(p.IncludeNewlinesStack)-1] | |
120 | } | |
121 | ||
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, | |
132 | }) | |
133 | } | |
134 | ||
135 | p.IncludeNewlinesStack = append(p.IncludeNewlinesStack, include) | |
136 | } | |
137 | ||
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 | |
142 | ||
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{ | |
151 | false, frame, ret, | |
152 | }) | |
153 | } | |
154 | ||
155 | return ret | |
156 | } | |
157 | ||
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. | |
161 | // | |
162 | // This function is a no-op if the stack is empty when called. | |
163 | // | |
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 | |
167 | // misbehaving. | |
168 | func (p *peeker) AssertEmptyIncludeNewlinesStack() { | |
169 | if len(p.IncludeNewlinesStack) != 1 { | |
170 | // Should never happen; indicates mismanagement of the stack inside | |
171 | // the parser. | |
172 | if p.newlineStackChanges != nil { // only if traceNewlinesStack is enabled above | |
173 | panic(fmt.Errorf( | |
174 | "non-empty IncludeNewlinesStack after parse with %d calls unaccounted for:\n%s", | |
175 | len(p.IncludeNewlinesStack)-1, | |
176 | formatPeekerNewlineStackChanges(p.newlineStackChanges), | |
177 | )) | |
178 | } else { | |
179 | panic(fmt.Errorf("non-empty IncludeNewlinesStack after parse: %#v", p.IncludeNewlinesStack)) | |
180 | } | |
181 | } | |
182 | } | |
183 | ||
184 | func formatPeekerNewlineStackChanges(changes []peekerNewlineStackChange) string { | |
185 | indent := 0 | |
186 | var buf bytes.Buffer | |
187 | for _, change := range changes { | |
188 | funcName := change.Frame.Function | |
189 | if idx := strings.LastIndexByte(funcName, '.'); idx != -1 { | |
190 | funcName = funcName[idx+1:] | |
191 | } | |
192 | filename := change.Frame.File | |
193 | if idx := strings.LastIndexByte(filename, filepath.Separator); idx != -1 { | |
194 | filename = filename[idx+1:] | |
195 | } | |
196 | ||
197 | switch change.Pushing { | |
198 | ||
199 | case true: | |
200 | buf.WriteString(strings.Repeat(" ", indent)) | |
201 | fmt.Fprintf(&buf, "PUSH %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line) | |
202 | indent++ | |
203 | ||
204 | case false: | |
205 | indent-- | |
206 | buf.WriteString(strings.Repeat(" ", indent)) | |
207 | fmt.Fprintf(&buf, "POP %#v (%s at %s:%d)\n", change.Include, funcName, filename, change.Frame.Line) | |
208 | ||
209 | } | |
210 | } | |
211 | return buf.String() | |
212 | } |