]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/hcl2/hclwrite/format.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / hcl2 / hclwrite / format.go
1 package hclwrite
2
3 import (
4 "github.com/hashicorp/hcl2/hcl/hclsyntax"
5 )
6
7 var inKeyword = hclsyntax.Keyword([]byte{'i', 'n'})
8
9 // placeholder token used when we don't have a token but we don't want
10 // to pass a real "nil" and complicate things with nil pointer checks
11 var nilToken = &Token{
12 Type: hclsyntax.TokenNil,
13 Bytes: []byte{},
14 SpacesBefore: 0,
15 }
16
17 // format rewrites tokens within the given sequence, in-place, to adjust the
18 // whitespace around their content to achieve canonical formatting.
19 func format(tokens Tokens) {
20 // Formatting is a multi-pass process. More details on the passes below,
21 // but this is the overview:
22 // - adjust the leading space on each line to create appropriate
23 // indentation
24 // - adjust spaces between tokens in a single cell using a set of rules
25 // - adjust the leading space in the "assign" and "comment" cells on each
26 // line to vertically align with neighboring lines.
27 // All of these steps operate in-place on the given tokens, so a caller
28 // may collect a flat sequence of all of the tokens underlying an AST
29 // and pass it here and we will then indirectly modify the AST itself.
30 // Formatting must change only whitespace. Specifically, that means
31 // changing the SpacesBefore attribute on a token while leaving the
32 // other token attributes unchanged.
33
34 lines := linesForFormat(tokens)
35 formatIndent(lines)
36 formatSpaces(lines)
37 formatCells(lines)
38 }
39
40 func formatIndent(lines []formatLine) {
41 // Our methodology for indents is to take the input one line at a time
42 // and count the bracketing delimiters on each line. If a line has a net
43 // increase in open brackets, we increase the indent level by one and
44 // remember how many new openers we had. If the line has a net _decrease_,
45 // we'll compare it to the most recent number of openers and decrease the
46 // dedent level by one each time we pass an indent level remembered
47 // earlier.
48 // The "indent stack" used here allows for us to recognize degenerate
49 // input where brackets are not symmetrical within lines and avoid
50 // pushing things too far left or right, creating confusion.
51
52 // We'll start our indent stack at a reasonable capacity to minimize the
53 // chance of us needing to grow it; 10 here means 10 levels of indent,
54 // which should be more than enough for reasonable HCL uses.
55 indents := make([]int, 0, 10)
56
57 inHeredoc := false
58 for i := range lines {
59 line := &lines[i]
60 if len(line.lead) == 0 {
61 continue
62 }
63
64 if inHeredoc {
65 for _, token := range line.lead {
66 if token.Type == hclsyntax.TokenCHeredoc {
67 inHeredoc = false
68 }
69 }
70 continue // don't touch indentation inside heredocs
71 }
72
73 if line.lead[0].Type == hclsyntax.TokenNewline {
74 // Never place spaces before a newline
75 line.lead[0].SpacesBefore = 0
76 continue
77 }
78
79 netBrackets := 0
80 for _, token := range line.lead {
81 netBrackets += tokenBracketChange(token)
82 if token.Type == hclsyntax.TokenOHeredoc {
83 inHeredoc = true
84 }
85 }
86 for _, token := range line.assign {
87 netBrackets += tokenBracketChange(token)
88 }
89
90 switch {
91 case netBrackets > 0:
92 line.lead[0].SpacesBefore = 2 * len(indents)
93 indents = append(indents, netBrackets)
94 case netBrackets < 0:
95 closed := -netBrackets
96 for closed > 0 && len(indents) > 0 {
97 switch {
98
99 case closed > indents[len(indents)-1]:
100 closed -= indents[len(indents)-1]
101 indents = indents[:len(indents)-1]
102
103 case closed < indents[len(indents)-1]:
104 indents[len(indents)-1] -= closed
105 closed = 0
106
107 default:
108 indents = indents[:len(indents)-1]
109 closed = 0
110 }
111 }
112 line.lead[0].SpacesBefore = 2 * len(indents)
113 default:
114 line.lead[0].SpacesBefore = 2 * len(indents)
115 }
116 }
117 }
118
119 func formatSpaces(lines []formatLine) {
120 for _, line := range lines {
121 for i, token := range line.lead {
122 var before, after *Token
123 if i > 0 {
124 before = line.lead[i-1]
125 } else {
126 before = nilToken
127 }
128 if i < (len(line.lead) - 1) {
129 after = line.lead[i+1]
130 } else {
131 after = nilToken
132 }
133 if spaceAfterToken(token, before, after) {
134 after.SpacesBefore = 1
135 } else {
136 after.SpacesBefore = 0
137 }
138 }
139 for i, token := range line.assign {
140 if i == 0 {
141 // first token in "assign" always has one space before to
142 // separate the equals sign from what it's assigning.
143 token.SpacesBefore = 1
144 }
145
146 var before, after *Token
147 if i > 0 {
148 before = line.assign[i-1]
149 } else {
150 before = nilToken
151 }
152 if i < (len(line.assign) - 1) {
153 after = line.assign[i+1]
154 } else {
155 after = nilToken
156 }
157 if spaceAfterToken(token, before, after) {
158 after.SpacesBefore = 1
159 } else {
160 after.SpacesBefore = 0
161 }
162 }
163
164 }
165 }
166
167 func formatCells(lines []formatLine) {
168
169 chainStart := -1
170 maxColumns := 0
171
172 // We'll deal with the "assign" cell first, since moving that will
173 // also impact the "comment" cell.
174 closeAssignChain := func(i int) {
175 for _, chainLine := range lines[chainStart:i] {
176 columns := chainLine.lead.Columns()
177 spaces := (maxColumns - columns) + 1
178 chainLine.assign[0].SpacesBefore = spaces
179 }
180 chainStart = -1
181 maxColumns = 0
182 }
183 for i, line := range lines {
184 if line.assign == nil {
185 if chainStart != -1 {
186 closeAssignChain(i)
187 }
188 } else {
189 if chainStart == -1 {
190 chainStart = i
191 }
192 columns := line.lead.Columns()
193 if columns > maxColumns {
194 maxColumns = columns
195 }
196 }
197 }
198 if chainStart != -1 {
199 closeAssignChain(len(lines))
200 }
201
202 // Now we'll deal with the comments
203 closeCommentChain := func(i int) {
204 for _, chainLine := range lines[chainStart:i] {
205 columns := chainLine.lead.Columns() + chainLine.assign.Columns()
206 spaces := (maxColumns - columns) + 1
207 chainLine.comment[0].SpacesBefore = spaces
208 }
209 chainStart = -1
210 maxColumns = 0
211 }
212 for i, line := range lines {
213 if line.comment == nil {
214 if chainStart != -1 {
215 closeCommentChain(i)
216 }
217 } else {
218 if chainStart == -1 {
219 chainStart = i
220 }
221 columns := line.lead.Columns() + line.assign.Columns()
222 if columns > maxColumns {
223 maxColumns = columns
224 }
225 }
226 }
227 if chainStart != -1 {
228 closeCommentChain(len(lines))
229 }
230
231 }
232
233 // spaceAfterToken decides whether a particular subject token should have a
234 // space after it when surrounded by the given before and after tokens.
235 // "before" can be TokenNil, if the subject token is at the start of a sequence.
236 func spaceAfterToken(subject, before, after *Token) bool {
237 switch {
238
239 case after.Type == hclsyntax.TokenNewline || after.Type == hclsyntax.TokenNil:
240 // Never add spaces before a newline
241 return false
242
243 case subject.Type == hclsyntax.TokenIdent && after.Type == hclsyntax.TokenOParen:
244 // Don't split a function name from open paren in a call
245 return false
246
247 case subject.Type == hclsyntax.TokenDot || after.Type == hclsyntax.TokenDot:
248 // Don't use spaces around attribute access dots
249 return false
250
251 case after.Type == hclsyntax.TokenComma || after.Type == hclsyntax.TokenEllipsis:
252 // No space right before a comma or ... in an argument list
253 return false
254
255 case subject.Type == hclsyntax.TokenComma:
256 // Always a space after a comma
257 return true
258
259 case subject.Type == hclsyntax.TokenQuotedLit || subject.Type == hclsyntax.TokenStringLit || subject.Type == hclsyntax.TokenOQuote || subject.Type == hclsyntax.TokenOHeredoc || after.Type == hclsyntax.TokenQuotedLit || after.Type == hclsyntax.TokenStringLit || after.Type == hclsyntax.TokenCQuote || after.Type == hclsyntax.TokenCHeredoc:
260 // No extra spaces within templates
261 return false
262
263 case inKeyword.TokenMatches(subject.asHCLSyntax()) && before.Type == hclsyntax.TokenIdent:
264 // This is a special case for inside for expressions where a user
265 // might want to use a literal tuple constructor:
266 // [for x in [foo]: x]
267 // ... in that case, we would normally produce in[foo] thinking that
268 // in is a reference, but we'll recognize it as a keyword here instead
269 // to make the result less confusing.
270 return true
271
272 case after.Type == hclsyntax.TokenOBrack && (subject.Type == hclsyntax.TokenIdent || subject.Type == hclsyntax.TokenNumberLit || tokenBracketChange(subject) < 0):
273 return false
274
275 case subject.Type == hclsyntax.TokenMinus:
276 // Since a minus can either be subtraction or negation, and the latter
277 // should _not_ have a space after it, we need to use some heuristics
278 // to decide which case this is.
279 // We guess that we have a negation if the token before doesn't look
280 // like it could be the end of an expression.
281
282 switch before.Type {
283
284 case hclsyntax.TokenNil:
285 // Minus at the start of input must be a negation
286 return false
287
288 case hclsyntax.TokenOParen, hclsyntax.TokenOBrace, hclsyntax.TokenOBrack, hclsyntax.TokenEqual, hclsyntax.TokenColon, hclsyntax.TokenComma, hclsyntax.TokenQuestion:
289 // Minus immediately after an opening bracket or separator must be a negation.
290 return false
291
292 case hclsyntax.TokenPlus, hclsyntax.TokenStar, hclsyntax.TokenSlash, hclsyntax.TokenPercent, hclsyntax.TokenMinus:
293 // Minus immediately after another arithmetic operator must be negation.
294 return false
295
296 case hclsyntax.TokenEqualOp, hclsyntax.TokenNotEqual, hclsyntax.TokenGreaterThan, hclsyntax.TokenGreaterThanEq, hclsyntax.TokenLessThan, hclsyntax.TokenLessThanEq:
297 // Minus immediately after another comparison operator must be negation.
298 return false
299
300 case hclsyntax.TokenAnd, hclsyntax.TokenOr, hclsyntax.TokenBang:
301 // Minus immediately after logical operator doesn't make sense but probably intended as negation.
302 return false
303
304 default:
305 return true
306 }
307
308 case subject.Type == hclsyntax.TokenOBrace || after.Type == hclsyntax.TokenCBrace:
309 // Unlike other bracket types, braces have spaces on both sides of them,
310 // both in single-line nested blocks foo { bar = baz } and in object
311 // constructor expressions foo = { bar = baz }.
312 if subject.Type == hclsyntax.TokenOBrace && after.Type == hclsyntax.TokenCBrace {
313 // An open brace followed by a close brace is an exception, however.
314 // e.g. foo {} rather than foo { }
315 return false
316 }
317 return true
318
319 // In the unlikely event that an interpolation expression is just
320 // a single object constructor, we'll put a space between the ${ and
321 // the following { to make this more obvious, and then the same
322 // thing for the two braces at the end.
323 case (subject.Type == hclsyntax.TokenTemplateInterp || subject.Type == hclsyntax.TokenTemplateControl) && after.Type == hclsyntax.TokenOBrace:
324 return true
325 case subject.Type == hclsyntax.TokenCBrace && after.Type == hclsyntax.TokenTemplateSeqEnd:
326 return true
327
328 // Don't add spaces between interpolated items
329 case subject.Type == hclsyntax.TokenTemplateSeqEnd && after.Type == hclsyntax.TokenTemplateInterp:
330 return false
331
332 case tokenBracketChange(subject) > 0:
333 // No spaces after open brackets
334 return false
335
336 case tokenBracketChange(after) < 0:
337 // No spaces before close brackets
338 return false
339
340 default:
341 // Most tokens are space-separated
342 return true
343
344 }
345 }
346
347 func linesForFormat(tokens Tokens) []formatLine {
348 if len(tokens) == 0 {
349 return make([]formatLine, 0)
350 }
351
352 // first we'll count our lines, so we can allocate the array for them in
353 // a single block. (We want to minimize memory pressure in this codepath,
354 // so it can be run somewhat-frequently by editor integrations.)
355 lineCount := 1 // if there are zero newlines then there is one line
356 for _, tok := range tokens {
357 if tokenIsNewline(tok) {
358 lineCount++
359 }
360 }
361
362 // To start, we'll just put everything in the "lead" cell on each line,
363 // and then do another pass over the lines afterwards to adjust.
364 lines := make([]formatLine, lineCount)
365 li := 0
366 lineStart := 0
367 for i, tok := range tokens {
368 if tok.Type == hclsyntax.TokenEOF {
369 // The EOF token doesn't belong to any line, and terminates the
370 // token sequence.
371 lines[li].lead = tokens[lineStart:i]
372 break
373 }
374
375 if tokenIsNewline(tok) {
376 lines[li].lead = tokens[lineStart : i+1]
377 lineStart = i + 1
378 li++
379 }
380 }
381
382 // If a set of tokens doesn't end in TokenEOF (e.g. because it's a
383 // fragment of tokens from the middle of a file) then we might fall
384 // out here with a line still pending.
385 if lineStart < len(tokens) {
386 lines[li].lead = tokens[lineStart:]
387 if lines[li].lead[len(lines[li].lead)-1].Type == hclsyntax.TokenEOF {
388 lines[li].lead = lines[li].lead[:len(lines[li].lead)-1]
389 }
390 }
391
392 // Now we'll pick off any trailing comments and attribute assignments
393 // to shuffle off into the "comment" and "assign" cells.
394 inHeredoc := false
395 for i := range lines {
396 line := &lines[i]
397 if len(line.lead) == 0 {
398 // if the line is empty then there's nothing for us to do
399 // (this should happen only for the final line, because all other
400 // lines would have a newline token of some kind)
401 continue
402 }
403
404 if inHeredoc {
405 for _, tok := range line.lead {
406 if tok.Type == hclsyntax.TokenCHeredoc {
407 inHeredoc = false
408 break
409 }
410 }
411 // Inside a heredoc everything is "lead", even if there's a
412 // template interpolation embedded in there that might otherwise
413 // confuse our logic below.
414 continue
415 }
416
417 for _, tok := range line.lead {
418 if tok.Type == hclsyntax.TokenOHeredoc {
419 inHeredoc = true
420 break
421 }
422 }
423
424 if len(line.lead) > 1 && line.lead[len(line.lead)-1].Type == hclsyntax.TokenComment {
425 line.comment = line.lead[len(line.lead)-1:]
426 line.lead = line.lead[:len(line.lead)-1]
427 }
428
429 for i, tok := range line.lead {
430 if i > 0 && tok.Type == hclsyntax.TokenEqual {
431 // We only move the tokens into "assign" if the RHS seems to
432 // be a whole expression, which we determine by counting
433 // brackets. If there's a net positive number of brackets
434 // then that suggests we're introducing a multi-line expression.
435 netBrackets := 0
436 for _, token := range line.lead[i:] {
437 netBrackets += tokenBracketChange(token)
438 }
439
440 if netBrackets == 0 {
441 line.assign = line.lead[i:]
442 line.lead = line.lead[:i]
443 }
444 break
445 }
446 }
447 }
448
449 return lines
450 }
451
452 func tokenIsNewline(tok *Token) bool {
453 if tok.Type == hclsyntax.TokenNewline {
454 return true
455 } else if tok.Type == hclsyntax.TokenComment {
456 // Single line tokens (# and //) consume their terminating newline,
457 // so we need to treat them as newline tokens as well.
458 if len(tok.Bytes) > 0 && tok.Bytes[len(tok.Bytes)-1] == '\n' {
459 return true
460 }
461 }
462 return false
463 }
464
465 func tokenBracketChange(tok *Token) int {
466 switch tok.Type {
467 case hclsyntax.TokenOBrace, hclsyntax.TokenOBrack, hclsyntax.TokenOParen, hclsyntax.TokenTemplateControl, hclsyntax.TokenTemplateInterp:
468 return 1
469 case hclsyntax.TokenCBrace, hclsyntax.TokenCBrack, hclsyntax.TokenCParen, hclsyntax.TokenTemplateSeqEnd:
470 return -1
471 default:
472 return 0
473 }
474 }
475
476 // formatLine represents a single line of source code for formatting purposes,
477 // splitting its tokens into up to three "cells":
478 //
479 // lead: always present, representing everything up to one of the others
480 // assign: if line contains an attribute assignment, represents the tokens
481 // starting at (and including) the equals symbol
482 // comment: if line contains any non-comment tokens and ends with a
483 // single-line comment token, represents the comment.
484 //
485 // When formatting, the leading spaces of the first tokens in each of these
486 // cells is adjusted to align vertically their occurences on consecutive
487 // rows.
488 type formatLine struct {
489 lead Tokens
490 assign Tokens
491 comment Tokens
492 }