8 "github.com/hashicorp/hcl/hcl/ast"
9 "github.com/hashicorp/hcl/hcl/token"
16 infinity = 1 << 30 // offset or line
20 unindent = []byte("\uE123") // in the private use space
27 comments []*ast.CommentGroup // may be nil, contains all comments
28 standaloneComments []*ast.CommentGroup // contains all standalone comments (not assigned to any node)
34 type ByPosition []*ast.CommentGroup
36 func (b ByPosition) Len() int { return len(b) }
37 func (b ByPosition) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
38 func (b ByPosition) Less(i, j int) bool { return b[i].Pos().Before(b[j].Pos()) }
40 // collectComments comments all standalone comments which are not lead or line
42 func (p *printer) collectComments(node ast.Node) {
43 // first collect all comments. This is already stored in
44 // ast.File.(comments)
45 ast.Walk(node, func(nn ast.Node) (ast.Node, bool) {
46 switch t := nn.(type) {
48 p.comments = t.Comments
54 standaloneComments := make(map[token.Pos]*ast.CommentGroup, 0)
55 for _, c := range p.comments {
56 standaloneComments[c.Pos()] = c
59 // next remove all lead and line comments from the overall comment map.
60 // This will give us comments which are standalone, comments which are not
61 // assigned to any kind of node.
62 ast.Walk(node, func(nn ast.Node) (ast.Node, bool) {
63 switch t := nn.(type) {
64 case *ast.LiteralType:
65 if t.LeadComment != nil {
66 for _, comment := range t.LeadComment.List {
67 if _, ok := standaloneComments[comment.Pos()]; ok {
68 delete(standaloneComments, comment.Pos())
73 if t.LineComment != nil {
74 for _, comment := range t.LineComment.List {
75 if _, ok := standaloneComments[comment.Pos()]; ok {
76 delete(standaloneComments, comment.Pos())
81 if t.LeadComment != nil {
82 for _, comment := range t.LeadComment.List {
83 if _, ok := standaloneComments[comment.Pos()]; ok {
84 delete(standaloneComments, comment.Pos())
89 if t.LineComment != nil {
90 for _, comment := range t.LineComment.List {
91 if _, ok := standaloneComments[comment.Pos()]; ok {
92 delete(standaloneComments, comment.Pos())
101 for _, c := range standaloneComments {
102 p.standaloneComments = append(p.standaloneComments, c)
105 sort.Sort(ByPosition(p.standaloneComments))
108 // output prints creates b printable HCL output and returns it.
109 func (p *printer) output(n interface{}) []byte {
112 switch t := n.(type) {
114 // File doesn't trace so we add the tracing here
115 defer un(trace(p, "File"))
116 return p.output(t.Node)
117 case *ast.ObjectList:
118 defer un(trace(p, "ObjectList"))
122 // Determine the location of the next actual non-comment
123 // item. If we're at the end, the next item is at "infinity"
124 var nextItem token.Pos
125 if index != len(t.Items) {
126 nextItem = t.Items[index].Pos()
128 nextItem = token.Pos{Offset: infinity, Line: infinity}
131 // Go through the standalone comments in the file and print out
132 // the comments that we should be for this object item.
133 for _, c := range p.standaloneComments {
134 // Go through all the comments in the group. The group
135 // should be printed together, not separated by double newlines.
137 newlinePrinted := false
138 for _, comment := range c.List {
139 // We only care about comments after the previous item
140 // we've printed so that comments are printed in the
141 // correct locations (between two objects for example).
142 // And before the next item.
143 if comment.Pos().After(p.prev) && comment.Pos().Before(nextItem) {
144 // if we hit the end add newlines so we can print the comment
145 // we don't do this if prev is invalid which means the
146 // beginning of the file since the first comment should
147 // be at the first line.
148 if !newlinePrinted && p.prev.IsValid() && index == len(t.Items) {
149 buf.Write([]byte{newline, newline})
150 newlinePrinted = true
153 // Write the actual comment.
154 buf.WriteString(comment.Text)
155 buf.WriteByte(newline)
157 // Set printed to true to note that we printed something
162 // If we're not at the last item, write a new line so
163 // that there is a newline separating this comment from
165 if printed && index != len(t.Items) {
166 buf.WriteByte(newline)
170 if index == len(t.Items) {
174 buf.Write(p.output(t.Items[index]))
175 if index != len(t.Items)-1 {
176 // Always write a newline to separate us from the next item
177 buf.WriteByte(newline)
179 // Need to determine if we're going to separate the next item
180 // with a blank line. The logic here is simple, though there
181 // are a few conditions:
183 // 1. The next object is more than one line away anyways,
184 // so we need an empty line.
186 // 2. The next object is not a "single line" object, so
187 // we need an empty line.
189 // 3. This current object is not a single line object,
190 // so we need an empty line.
191 current := t.Items[index]
192 next := t.Items[index+1]
193 if next.Pos().Line != t.Items[index].Pos().Line+1 ||
194 !p.isSingleLineObject(next) ||
195 !p.isSingleLineObject(current) {
196 buf.WriteByte(newline)
202 buf.WriteString(t.Token.Text)
203 case *ast.ObjectItem:
205 buf.Write(p.objectItem(t))
206 case *ast.LiteralType:
207 buf.Write(p.literalType(t))
210 case *ast.ObjectType:
211 buf.Write(p.objectType(t))
213 fmt.Printf(" unknown type: %T\n", n)
219 func (p *printer) literalType(lit *ast.LiteralType) []byte {
220 result := []byte(lit.Token.Text)
221 switch lit.Token.Type {
223 // Clear the trailing newline from heredocs
224 if result[len(result)-1] == '\n' {
225 result = result[:len(result)-1]
228 // Poison lines 2+ so that we don't indent them
229 result = p.heredocIndent(result)
231 // If this is a multiline string, poison lines 2+ so we don't
233 if bytes.IndexRune(result, '\n') >= 0 {
234 result = p.heredocIndent(result)
241 // objectItem returns the printable HCL form of an object item. An object type
242 // starts with one/multiple keys and has a value. The value might be of any
244 func (p *printer) objectItem(o *ast.ObjectItem) []byte {
245 defer un(trace(p, fmt.Sprintf("ObjectItem: %s", o.Keys[0].Token.Text)))
248 if o.LeadComment != nil {
249 for _, comment := range o.LeadComment.List {
250 buf.WriteString(comment.Text)
251 buf.WriteByte(newline)
255 for i, k := range o.Keys {
256 buf.WriteString(k.Token.Text)
260 if o.Assign.IsValid() && i == len(o.Keys)-1 && len(o.Keys) == 1 {
266 buf.Write(p.output(o.Val))
268 if o.Val.Pos().Line == o.Keys[0].Pos().Line && o.LineComment != nil {
270 for _, comment := range o.LineComment.List {
271 buf.WriteString(comment.Text)
278 // objectType returns the printable HCL form of an object type. An object type
279 // begins with a brace and ends with a brace.
280 func (p *printer) objectType(o *ast.ObjectType) []byte {
281 defer un(trace(p, "ObjectType"))
286 var nextItem token.Pos
287 var commented, newlinePrinted bool
289 // Determine the location of the next actual non-comment
290 // item. If we're at the end, the next item is the closing brace
291 if index != len(o.List.Items) {
292 nextItem = o.List.Items[index].Pos()
297 // Go through the standalone comments in the file and print out
298 // the comments that we should be for this object item.
299 for _, c := range p.standaloneComments {
301 var lastCommentPos token.Pos
302 for _, comment := range c.List {
303 // We only care about comments after the previous item
304 // we've printed so that comments are printed in the
305 // correct locations (between two objects for example).
306 // And before the next item.
307 if comment.Pos().After(p.prev) && comment.Pos().Before(nextItem) {
308 // If there are standalone comments and the initial newline has not
309 // been printed yet, do it now.
311 newlinePrinted = true
312 buf.WriteByte(newline)
315 // add newline if it's between other printed nodes
318 buf.WriteByte(newline)
321 // Store this position
322 lastCommentPos = comment.Pos()
324 // output the comment itself
325 buf.Write(p.indent(p.heredocIndent([]byte(comment.Text))))
327 // Set printed to true to note that we printed something
331 if index != len(o.List.Items) {
332 buf.WriteByte(newline) // do not print on the end
338 // Stuff to do if we had comments
340 // Always write a newline
341 buf.WriteByte(newline)
343 // If there is another item in the object and our comment
344 // didn't hug it directly, then make sure there is a blank
345 // line separating them.
346 if nextItem != o.Rbrace && nextItem.Line != lastCommentPos.Line+1 {
347 buf.WriteByte(newline)
352 if index == len(o.List.Items) {
357 // At this point we are sure that it's not a totally empty block: print
358 // the initial newline if it hasn't been printed yet by the previous
359 // block about standalone comments.
361 buf.WriteByte(newline)
362 newlinePrinted = true
365 // check if we have adjacent one liner items. If yes we'll going to align
367 var aligned []*ast.ObjectItem
368 for _, item := range o.List.Items[index:] {
369 // we don't group one line lists
370 if len(o.List.Items) == 1 {
374 // one means a oneliner with out any lead comment
375 // two means a oneliner with lead comment
376 // anything else might be something else
377 cur := lines(string(p.objectItem(item)))
384 nextPos := token.Pos{}
385 if index != len(o.List.Items)-1 {
386 nextPos = o.List.Items[index+1].Pos()
389 prevPos := token.Pos{}
391 prevPos = o.List.Items[index-1].Pos()
394 // fmt.Println("DEBUG ----------------")
395 // fmt.Printf("prev = %+v prevPos: %s\n", prev, prevPos)
396 // fmt.Printf("cur = %+v curPos: %s\n", cur, curPos)
397 // fmt.Printf("next = %+v nextPos: %s\n", next, nextPos)
399 if curPos.Line+1 == nextPos.Line {
400 aligned = append(aligned, item)
405 if curPos.Line-1 == prevPos.Line {
406 aligned = append(aligned, item)
409 // finish if we have a new line or comment next. This happens
410 // if the next item is not adjacent
411 if curPos.Line+1 != nextPos.Line {
420 // put newlines if the items are between other non aligned items.
421 // newlines are also added if there is a standalone comment already, so
423 if !commented && index != len(aligned) {
424 buf.WriteByte(newline)
427 if len(aligned) >= 1 {
428 p.prev = aligned[len(aligned)-1].Pos()
430 items := p.alignedItems(aligned)
431 buf.Write(p.indent(items))
433 p.prev = o.List.Items[index].Pos()
435 buf.Write(p.indent(p.objectItem(o.List.Items[index])))
439 buf.WriteByte(newline)
446 func (p *printer) alignedItems(items []*ast.ObjectItem) []byte {
449 // find the longest key and value length, needed for alignment
450 var longestKeyLen int // longest key length
451 var longestValLen int // longest value length
452 for _, item := range items {
453 key := len(item.Keys[0].Token.Text)
454 val := len(p.output(item.Val))
456 if key > longestKeyLen {
460 if val > longestValLen {
465 for i, item := range items {
466 if item.LeadComment != nil {
467 for _, comment := range item.LeadComment.List {
468 buf.WriteString(comment.Text)
469 buf.WriteByte(newline)
473 for i, k := range item.Keys {
474 keyLen := len(k.Token.Text)
475 buf.WriteString(k.Token.Text)
476 for i := 0; i < longestKeyLen-keyLen+1; i++ {
481 if i == len(item.Keys)-1 && len(item.Keys) == 1 {
487 val := p.output(item.Val)
491 if item.Val.Pos().Line == item.Keys[0].Pos().Line && item.LineComment != nil {
492 for i := 0; i < longestValLen-valLen+1; i++ {
496 for _, comment := range item.LineComment.List {
497 buf.WriteString(comment.Text)
501 // do not print for the last item
502 if i != len(items)-1 {
503 buf.WriteByte(newline)
510 // list returns the printable HCL form of an list type.
511 func (p *printer) list(l *ast.ListType) []byte {
516 for _, item := range l.List {
517 // for now we assume that the list only contains literal types
518 if lit, ok := item.(*ast.LiteralType); ok {
519 lineLen := len(lit.Token.Text)
520 if lineLen > longestLine {
521 longestLine = lineLen
526 insertSpaceBeforeItem := false
527 lastHadLeadComment := false
528 for i, item := range l.List {
529 // Keep track of whether this item is a heredoc since that has
532 if lit, ok := item.(*ast.LiteralType); ok && lit.Token.Type == token.HEREDOC {
536 if item.Pos().Line != l.Lbrack.Line {
537 // multiline list, add newline before we add each item
538 buf.WriteByte(newline)
539 insertSpaceBeforeItem = false
541 // If we have a lead comment, then we want to write that first
543 if lit, ok := item.(*ast.LiteralType); ok && lit.LeadComment != nil {
546 // If this isn't the first item and the previous element
547 // didn't have a lead comment, then we need to add an extra
548 // newline to properly space things out. If it did have a
549 // lead comment previously then this would be done
551 if i > 0 && !lastHadLeadComment {
552 buf.WriteByte(newline)
555 for _, comment := range lit.LeadComment.List {
556 buf.Write(p.indent([]byte(comment.Text)))
557 buf.WriteByte(newline)
561 // also indent each line
562 val := p.output(item)
564 buf.Write(p.indent(val))
566 // if this item is a heredoc, then we output the comma on
567 // the next line. This is the only case this happens.
570 buf.WriteByte(newline)
571 comma = p.indent(comma)
576 if lit, ok := item.(*ast.LiteralType); ok && lit.LineComment != nil {
577 // if the next item doesn't have any comments, do not align
578 buf.WriteByte(blank) // align one space
579 for i := 0; i < longestLine-curLen; i++ {
583 for _, comment := range lit.LineComment.List {
584 buf.WriteString(comment.Text)
588 lastItem := i == len(l.List)-1
590 buf.WriteByte(newline)
593 if leadComment && !lastItem {
594 buf.WriteByte(newline)
597 lastHadLeadComment = leadComment
599 if insertSpaceBeforeItem {
601 insertSpaceBeforeItem = false
604 // Output the item itself
605 // also indent each line
606 val := p.output(item)
610 // If this is a heredoc item we always have to output a newline
611 // so that it parses properly.
613 buf.WriteByte(newline)
616 // If this isn't the last element, write a comma.
617 if i != len(l.List)-1 {
619 insertSpaceBeforeItem = true
622 if lit, ok := item.(*ast.LiteralType); ok && lit.LineComment != nil {
623 // if the next item doesn't have any comments, do not align
624 buf.WriteByte(blank) // align one space
625 for i := 0; i < longestLine-curLen; i++ {
629 for _, comment := range lit.LineComment.List {
630 buf.WriteString(comment.Text)
641 // indent indents the lines of the given buffer for each non-empty line
642 func (p *printer) indent(buf []byte) []byte {
644 if p.cfg.SpacesWidth != 0 {
645 for i := 0; i < p.cfg.SpacesWidth; i++ {
646 prefix = append(prefix, blank)
654 for _, c := range buf {
655 if bol && c != '\n' {
656 res = append(res, prefix...)
665 // unindent removes all the indentation from the tombstoned lines
666 func (p *printer) unindent(buf []byte) []byte {
668 for i := 0; i < len(buf); i++ {
669 skip := len(buf)-i <= len(unindent)
671 skip = !bytes.Equal(unindent, buf[i:i+len(unindent)])
674 res = append(res, buf[i])
678 // We have a marker. we have to backtrace here and clean out
679 // any whitespace ahead of our tombstone up to a \n
680 for j := len(res) - 1; j >= 0; j-- {
688 // Skip the entire unindent marker
689 i += len(unindent) - 1
695 // heredocIndent marks all the 2nd and further lines as unindentable
696 func (p *printer) heredocIndent(buf []byte) []byte {
699 for _, c := range buf {
700 if bol && c != '\n' {
701 res = append(res, unindent...)
709 // isSingleLineObject tells whether the given object item is a single
710 // line object such as "obj {}".
712 // A single line object:
714 // * has no lead comments (hence multi-line)
715 // * has no assignment
716 // * has no values in the stanza (within {})
718 func (p *printer) isSingleLineObject(val *ast.ObjectItem) bool {
719 // If there is a lead comment, can't be one line
720 if val.LeadComment != nil {
724 // If there is assignment, we always break by line
725 if val.Assign.IsValid() {
729 // If it isn't an object type, then its not a single line object
730 ot, ok := val.Val.(*ast.ObjectType)
735 // If the object has no items, it is single line!
736 return len(ot.List.Items) == 0
739 func lines(txt string) int {
741 for i := 0; i < len(txt); i++ {
749 // ----------------------------------------------------------------------------
752 func (p *printer) printTrace(a ...interface{}) {
757 const dots = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . "
759 i := 2 * p.indentTrace
769 func trace(p *printer, msg string) *printer {
770 p.printTrace(msg, "(")
775 // Usage pattern: defer un(trace(p, "..."))
776 func un(p *printer) {