-
Notifications
You must be signed in to change notification settings - Fork 40
/
docs_hcl.go
297 lines (255 loc) · 7.93 KB
/
docs_hcl.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tfgen
import (
"github.com/hashicorp/hcl/hcl/scanner"
"github.com/hashicorp/hcl/hcl/token"
"github.com/hashicorp/hcl2/hclparse"
)
// fixHcl attempts to fix certain simple syntactical errors in a particular piece of HCL source text.
//
// For reference, here is the HCL grammar in ~EBNF:
//
// file := objectList EOF
//
// objectList := { objectItem }
//
// objectItem := ( assignmentProperty | objectProperty )
//
// assignmentProperty := ( IDENT | STRING ) '=' value
//
// objectProperty := ( IDENT | STRING ) { ( IDENT | STRING ) } objectValue
//
// value := literalValue | objectValue | listValue
//
// literalValue := NUMBER | FLOAT | BOOL | STRING | HEREDOC
//
// objectValue := '{' objectList '}'
//
// listValue := '[' [ value { ',' value } ] ']'
//
// We want to fix the following errors:
// - missing value in an assignmentProperty
// - missing objectValue in an objectProperty
// - missing values in a list
//
// We do not need to fix;
// - unbalanced braces
// - unbalanced brackets
// - bad assignmentProperty keys
// - etc.
//
// All of the problems we want to fix can be addressed by inserting artificial tokens into the token stream.
//
// NOTE: some valid HCL2 looks like invalid HCL. We try to catch these cases by checking for valid HCL2 first.
func fixHcl(hcl string) (string, bool) {
input := []byte(hcl)
if _, diags := hclparse.NewParser().ParseHCL(input, "main.tf"); len(diags) == 0 {
return "", false
}
h := &hclFixer{
input: input,
scanner: scanner.New(input),
}
h.scanner.Error = func(_ token.Pos, _ string) {}
if !h.file() {
return "", false
}
return string(h.output), true
}
// hclFixer is a utility type that is used by fixHcl.
type hclFixer struct {
input []byte // the input source text
output []byte // the output source text
scanner *scanner.Scanner // the scanner that lexes the input source text
next *token.Token // the next token to return
flushed int // the number of bytes of source text that have been processed
}
// peek returns the next token in the input text, but does not advance the current position or copy the token to the
// output buffer.
func (h *hclFixer) peek() token.Token {
// If the buffer is empty, grab the next token from the scanner.
if h.next == nil {
t := h.scanner.Scan()
// Chew through comments. We save the offset of the first comment token and stamp it onto the first non-comment
// token we see so that patches (if any) will occur before comments. For example, given the following:
//
// resource "aws_s3_bucket" "foo" {
// name = # put a name here
// }
//
// If we patch the missing property value, we want the patch to occur before "# put a name here". If we simply
// skip the comment without recording its offset, we would end up patching before the '}'.
offset := t.Pos.Offset
for t.Type == token.COMMENT {
t = h.scanner.Scan()
}
t.Pos.Offset = offset
// If this token is of a type we never expect to appear in source code, treat it as an illegal token. The
// patcher chews through illegal tokens after applying a patch so that we can handle code like this:
//
// resource "aws_s3_bucket" "foo" {
// name = ... # put a name here
// }
//
// In this case, we want the patcher to be able to add the missing property before the ellipsis and then drop
// the ellipsis from the output.
switch t.Type {
case token.ADD, token.SUB, token.PERIOD:
t.Type = token.ILLEGAL
}
// Finally, stick the new token into the buffer.
h.next = &t
}
return *h.next
}
// scan returns the next token in the input text, copies it to the output buffer, and advances the current position.
func (h *hclFixer) scan() token.Token {
// Pull the current token out of the buffer.
t := h.peek()
h.next = nil
// Now refill the buffer so we can calculate the length of the current token.
h.peek()
// If the current token is not ILLEGAL, flush it to the output.
n := h.next.Pos.Offset - h.flushed
if t.Type != token.ILLEGAL {
h.output = append(h.output, h.input[:n]...)
}
h.input = h.input[n:]
h.flushed += n
return t
}
// patch inserts the given patch into the output stream and then chews through any illegal characters in the source
// test. This is the primary mechanism for correcting errors in the input.
func (h *hclFixer) patch(p string) {
h.output = append(h.output, []byte(p)...)
// Chomp through any illegal tokens to accommodate truly strange input
for h.peek().Type == token.ILLEGAL {
h.scan()
}
}
// file parses an HCL file production.
func (h *hclFixer) file() bool {
if !h.objectList() {
return false
}
return h.scan().Type == token.EOF
}
// objectList parses an HCL objectList production.
func (h *hclFixer) objectList() bool {
for {
// peek should be an IDENT or a STRING
next := h.peek().Type
if next != token.IDENT && next != token.STRING {
return true
}
if !h.objectItem() {
return false
}
}
}
// objectItem parses an HCL objectItem production.
func (h *hclFixer) objectItem() bool {
// The next token should be an IDENT or a STRING
keyPart := h.scan().Type
if keyPart != token.IDENT && keyPart != token.STRING {
return false
}
// If the next token is an '=', parse an assignmentProperty.
next := h.peek().Type
if next == token.ASSIGN {
return h.assignmentProperty()
}
// Otherwise, continue chomping up strings or identifiers until we hit something else. If the thing we hit is not
// an LBRACE, we have a missing object property and will fill one in.
for next == token.IDENT || next == token.STRING {
h.scan()
next = h.peek().Type
}
// If the next token after the key is an LBRACE, parse an objectValue.
if next == token.LBRACE {
return h.objectValue()
}
// Otherwise, we have a missing objectValue. Synthesize one now and carry on.
h.patch("{}")
return true
}
// assignmentProperty parses an HCL assignmentProperty production.
func (h *hclFixer) assignmentProperty() bool {
if h.scan().Type != token.ASSIGN {
return false
}
switch h.peek().Type {
case token.NUMBER, token.FLOAT, token.BOOL, token.STRING, token.HEREDOC:
// Eat the literalValue and return true.
h.scan()
return true
case token.LBRACE:
// Parse an objectValue
return h.objectValue()
case token.LBRACK:
// Parse a listValue
return h.listValue()
default:
// We have a missing value. Sythesize one here and carry on.
h.patch(`""`)
return true
}
}
// objectValue parses an HCL objectValue production.
func (h *hclFixer) objectValue() bool {
return h.scan().Type == token.LBRACE && h.objectList() && h.scan().Type == token.RBRACE
}
// listValue parses an HCL listValue production.
func (h *hclFixer) listValue() bool {
if h.scan().Type != token.LBRACK {
return false
}
for i := 0; ; i++ {
if i != 0 {
switch h.peek().Type {
case token.COMMA:
h.scan()
case token.RBRACK:
h.scan()
return true
default:
return false
}
}
switch h.peek().Type {
case token.NUMBER, token.FLOAT, token.BOOL, token.STRING, token.HEREDOC:
// Eat the literalValue and continue
h.scan()
case token.LBRACE:
// Parse an objectValue
if !h.objectValue() {
return false
}
case token.LBRACK:
// Parse a listValue
if !h.listValue() {
return false
}
case token.RBRACK:
h.scan()
return true
case token.COMMA:
// We have a missing value. Sythesize one here and carry on.
h.patch(`""`)
default:
return false
}
}
}