Skip to content

Commit 38cc729

Browse files
committed
hclwrite: Rewrite comments on blocks and attributes
1 parent 527ec31 commit 38cc729

File tree

4 files changed

+689
-0
lines changed

4 files changed

+689
-0
lines changed

hclwrite/ast_block.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,43 @@ func (b *Block) SetLabels(labels []string) {
9696
b.labelsObj().Replace(labels)
9797
}
9898

99+
// Comments returns the comments that annotate the block.
100+
//
101+
// The return value is an empty slice if the block has no lead comments, or a
102+
// slice of strings representing the raw text of the comments, including
103+
// leading and trailing comment markers.
104+
func (b *Block) Comments() []string {
105+
tokens := b.leadComments.content.(*comments).tokens
106+
blockComments := make([]string, len(tokens))
107+
for i := range tokens {
108+
blockComments[i] = string(tokens[i].Bytes)
109+
}
110+
return blockComments
111+
}
112+
113+
// SetComments replaces the comments that annotate the block.
114+
//
115+
// Each item should contain any leading and trailing markers, i.e. single-
116+
// line comments should start with either “#” or “//” and end with a
117+
// newline character, and block comments should start with “/*” and end with
118+
// “*/”.
119+
func (b *Block) SetComments(blockComments []string) {
120+
c := b.leadComments.content.(*comments)
121+
c.tokens = make(Tokens, len(blockComments))
122+
123+
for i := range blockComments {
124+
c.tokens[i] = &Token{
125+
Type: hclsyntax.TokenComment,
126+
Bytes: []byte(blockComments[i]),
127+
}
128+
}
129+
}
130+
131+
// Single-comment alternative to [SetComments]
132+
func (b *Block) SetComment(blockComment string) {
133+
b.SetComments([]string{blockComment})
134+
}
135+
99136
// labelsObj returns the internal node content representation of the block
100137
// labels. This is not part of the public API because we're intentionally
101138
// exposing only a limited API to get/set labels on the block itself in a

hclwrite/ast_block_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,3 +487,182 @@ func TestBlockSetLabels(t *testing.T) {
487487
})
488488
}
489489
}
490+
491+
func TestBlockComments(t *testing.T) {
492+
tests := []struct {
493+
src string
494+
want []string
495+
}{
496+
{
497+
`
498+
block {
499+
}
500+
`,
501+
[]string{},
502+
},
503+
{
504+
`
505+
# Comment
506+
block {
507+
}
508+
`,
509+
[]string{"# Comment\n"},
510+
},
511+
{
512+
`
513+
# First line
514+
# Second line
515+
block {
516+
}
517+
`,
518+
[]string{"# First line\n", "# Second line\n"},
519+
},
520+
{
521+
`
522+
/* Comment */ block {
523+
}
524+
`,
525+
[]string{"/* Comment */"},
526+
},
527+
}
528+
529+
for _, test := range tests {
530+
t.Run(fmt.Sprintf("%s", strings.Join(test.want, " ")), func(t *testing.T) {
531+
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
532+
if len(diags) != 0 {
533+
for _, diag := range diags {
534+
t.Logf("- %s", diag.Error())
535+
}
536+
t.Fatalf("unexpected diagnostics")
537+
}
538+
539+
block := f.Body().Blocks()[0]
540+
got := block.Comments()
541+
if !reflect.DeepEqual(got, test.want) {
542+
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
543+
}
544+
})
545+
}
546+
}
547+
548+
func TestBlockSetComments(t *testing.T) {
549+
tests := []struct {
550+
src string
551+
typeName string
552+
blockComments []string
553+
want Tokens
554+
}{
555+
{
556+
`block {}`,
557+
"block",
558+
[]string{"# Comment\n"},
559+
Tokens{
560+
{
561+
Type: hclsyntax.TokenComment,
562+
Bytes: []byte("# Comment\n"),
563+
SpacesBefore: 0,
564+
},
565+
{
566+
Type: hclsyntax.TokenIdent,
567+
Bytes: []byte(`block`),
568+
SpacesBefore: 0,
569+
},
570+
{
571+
Type: hclsyntax.TokenOBrace,
572+
Bytes: []byte{'{'},
573+
SpacesBefore: 1,
574+
},
575+
{
576+
Type: hclsyntax.TokenCBrace,
577+
Bytes: []byte{'}'},
578+
SpacesBefore: 0,
579+
},
580+
{
581+
Type: hclsyntax.TokenEOF,
582+
Bytes: []byte{},
583+
SpacesBefore: 0,
584+
},
585+
},
586+
},
587+
{
588+
"# Comment\nblock {}",
589+
"block",
590+
nil,
591+
Tokens{
592+
{
593+
Type: hclsyntax.TokenIdent,
594+
Bytes: []byte(`block`),
595+
SpacesBefore: 0,
596+
},
597+
{
598+
Type: hclsyntax.TokenOBrace,
599+
Bytes: []byte{'{'},
600+
SpacesBefore: 1,
601+
},
602+
{
603+
Type: hclsyntax.TokenCBrace,
604+
Bytes: []byte{'}'},
605+
SpacesBefore: 0,
606+
},
607+
{
608+
Type: hclsyntax.TokenEOF,
609+
Bytes: []byte{},
610+
SpacesBefore: 0,
611+
},
612+
},
613+
},
614+
{
615+
"# Old\nblock {}",
616+
"block",
617+
[]string{"# New\n"},
618+
Tokens{
619+
{
620+
Type: hclsyntax.TokenComment,
621+
Bytes: []byte("# New\n"),
622+
SpacesBefore: 0,
623+
},
624+
{
625+
Type: hclsyntax.TokenIdent,
626+
Bytes: []byte(`block`),
627+
SpacesBefore: 0,
628+
},
629+
{
630+
Type: hclsyntax.TokenOBrace,
631+
Bytes: []byte{'{'},
632+
SpacesBefore: 1,
633+
},
634+
{
635+
Type: hclsyntax.TokenCBrace,
636+
Bytes: []byte{'}'},
637+
SpacesBefore: 0,
638+
},
639+
{
640+
Type: hclsyntax.TokenEOF,
641+
Bytes: []byte{},
642+
SpacesBefore: 0,
643+
},
644+
},
645+
},
646+
}
647+
648+
for _, test := range tests {
649+
t.Run(fmt.Sprintf("%s %s in %s", test.typeName, test.blockComments, test.src), func(t *testing.T) {
650+
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
651+
if len(diags) != 0 {
652+
for _, diag := range diags {
653+
t.Logf("- %s", diag.Error())
654+
}
655+
t.Fatalf("unexpected diagnostics")
656+
}
657+
658+
b := f.Body().FirstMatchingBlock(test.typeName, nil)
659+
b.SetComments(test.blockComments)
660+
got := f.BuildTokens(nil)
661+
format(got)
662+
if !reflect.DeepEqual(got, test.want) {
663+
diff := cmp.Diff(test.want, got)
664+
t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff)
665+
}
666+
})
667+
}
668+
}

hclwrite/ast_body.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,111 @@ func (b *Body) RemoveBlock(block *Block) bool {
137137
return false
138138
}
139139

140+
// AttributeLeadComments returns the comments that annotate an attribute.
141+
//
142+
// The return value is nil if there was no such attribute, an empty slice if
143+
// it had no lead comments, or a slice of strings representing the raw text of
144+
// the comments, including leading and trailing comment markers.
145+
func (b *Body) AttributeLeadComments(name string) []string {
146+
attr := b.GetAttribute(name)
147+
if attr == nil {
148+
return nil
149+
}
150+
151+
tokens := attr.leadComments.content.(*comments).tokens
152+
attrComments := make([]string, len(tokens))
153+
for i := range tokens {
154+
attrComments[i] = string(tokens[i].Bytes)
155+
}
156+
return attrComments
157+
}
158+
159+
// SetAttributeLeadComments replaces the comments that annotate an attribute.
160+
//
161+
// Each item should contain any leading and trailing markers, i.e. single-
162+
// line comments should start with either “#” or “//” and end with a
163+
// newline character, and block comments should start with “/*” and end with
164+
// “*/”.
165+
func (b *Body) SetAttributeLeadComments(name string, leadComments []string) *Attribute {
166+
attr := b.GetAttribute(name)
167+
if attr == nil {
168+
return nil
169+
}
170+
171+
c := attr.leadComments.content.(*comments)
172+
c.tokens = make(Tokens, len(leadComments))
173+
174+
for i := range leadComments {
175+
c.tokens[i] = &Token{
176+
Type: hclsyntax.TokenComment,
177+
Bytes: []byte(leadComments[i]),
178+
}
179+
}
180+
181+
return attr
182+
}
183+
184+
// Single-comment alternative to [SetAttributeLeadComments]
185+
func (b *Body) SetAttributeLeadComment(name string, leadComment string) *Attribute {
186+
return b.SetAttributeLeadComments(name, []string{leadComment})
187+
}
188+
189+
// AttributeLineComments returns the comments that follow an attribute on
190+
// the same line.
191+
//
192+
// The return value is nil if there was no such attribute, an empty slice if
193+
// it had no line comments, or a slice of strings representing the raw text of
194+
// the comments, including leading and trailing comment markers.
195+
func (b *Body) AttributeLineComments(name string) []string {
196+
attr := b.GetAttribute(name)
197+
if attr == nil {
198+
return nil
199+
}
200+
201+
tokens := attr.lineComments.content.(*comments).tokens
202+
attrComments := make([]string, len(tokens))
203+
for i := range tokens {
204+
attrComments[i] = string(tokens[i].Bytes)
205+
}
206+
return attrComments
207+
}
208+
209+
// SetAttributeLineComments replaces the comments that follow an attribute on
210+
// the same line.
211+
//
212+
// Each item should contain any leading and trailing markers, i.e. single-
213+
// line comments should start with either “#” or “//” and end with a
214+
// newline character, and block comments should start with “/*” and end with
215+
// “*/”.
216+
//
217+
// While it is technically possible to set multiple single-line comments using
218+
// this function, doing so is discouraged since the parser would never produce
219+
// such constructs. For single-line comments, prefer to use
220+
// [SetAttributeLineComment].
221+
func (b *Body) SetAttributeLineComments(name string, lineComments []string) *Attribute {
222+
attr := b.GetAttribute(name)
223+
if attr == nil {
224+
return nil
225+
}
226+
227+
c := attr.lineComments.content.(*comments)
228+
c.tokens = make(Tokens, len(lineComments))
229+
230+
for i := range lineComments {
231+
c.tokens[i] = &Token{
232+
Type: hclsyntax.TokenComment,
233+
Bytes: []byte(lineComments[i]),
234+
}
235+
}
236+
237+
return attr
238+
}
239+
240+
// Single-comment alternative to [SetAttributeLineComments]
241+
func (b *Body) SetAttributeLineComment(name string, lineComment string) *Attribute {
242+
return b.SetAttributeLineComments(name, []string{lineComment})
243+
}
244+
140245
// SetAttributeRaw either replaces the expression of an existing attribute
141246
// of the given name or adds a new attribute definition to the end of the block,
142247
// using the given tokens verbatim as the expression.

0 commit comments

Comments
 (0)