From 38cc7290d03f5b144c8bfc78331e70b026c12da9 Mon Sep 17 00:00:00 2001 From: izeau Date: Sun, 26 Mar 2023 23:22:12 +0200 Subject: [PATCH] hclwrite: Rewrite comments on blocks and attributes --- hclwrite/ast_block.go | 37 ++++ hclwrite/ast_block_test.go | 179 ++++++++++++++++++ hclwrite/ast_body.go | 105 +++++++++++ hclwrite/ast_body_test.go | 368 +++++++++++++++++++++++++++++++++++++ 4 files changed, 689 insertions(+) diff --git a/hclwrite/ast_block.go b/hclwrite/ast_block.go index 91e7d4a3..56a20a71 100644 --- a/hclwrite/ast_block.go +++ b/hclwrite/ast_block.go @@ -96,6 +96,43 @@ func (b *Block) SetLabels(labels []string) { b.labelsObj().Replace(labels) } +// Comments returns the comments that annotate the block. +// +// The return value is an empty slice if the block has no lead comments, or a +// slice of strings representing the raw text of the comments, including +// leading and trailing comment markers. +func (b *Block) Comments() []string { + tokens := b.leadComments.content.(*comments).tokens + blockComments := make([]string, len(tokens)) + for i := range tokens { + blockComments[i] = string(tokens[i].Bytes) + } + return blockComments +} + +// SetComments replaces the comments that annotate the block. +// +// Each item should contain any leading and trailing markers, i.e. single- +// line comments should start with either “#” or “//” and end with a +// newline character, and block comments should start with “/*” and end with +// “*/”. +func (b *Block) SetComments(blockComments []string) { + c := b.leadComments.content.(*comments) + c.tokens = make(Tokens, len(blockComments)) + + for i := range blockComments { + c.tokens[i] = &Token{ + Type: hclsyntax.TokenComment, + Bytes: []byte(blockComments[i]), + } + } +} + +// Single-comment alternative to [SetComments] +func (b *Block) SetComment(blockComment string) { + b.SetComments([]string{blockComment}) +} + // labelsObj returns the internal node content representation of the block // labels. This is not part of the public API because we're intentionally // exposing only a limited API to get/set labels on the block itself in a diff --git a/hclwrite/ast_block_test.go b/hclwrite/ast_block_test.go index 0760001a..bbcc70b8 100644 --- a/hclwrite/ast_block_test.go +++ b/hclwrite/ast_block_test.go @@ -487,3 +487,182 @@ func TestBlockSetLabels(t *testing.T) { }) } } + +func TestBlockComments(t *testing.T) { + tests := []struct { + src string + want []string + }{ + { + ` +block { +} +`, + []string{}, + }, + { + ` +# Comment +block { +} +`, + []string{"# Comment\n"}, + }, + { + ` +# First line +# Second line +block { +} +`, + []string{"# First line\n", "# Second line\n"}, + }, + { + ` +/* Comment */ block { +} +`, + []string{"/* Comment */"}, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s", strings.Join(test.want, " ")), func(t *testing.T) { + f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + block := f.Body().Blocks()[0] + got := block.Comments() + if !reflect.DeepEqual(got, test.want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) + } + }) + } +} + +func TestBlockSetComments(t *testing.T) { + tests := []struct { + src string + typeName string + blockComments []string + want Tokens + }{ + { + `block {}`, + "block", + []string{"# Comment\n"}, + Tokens{ + { + Type: hclsyntax.TokenComment, + Bytes: []byte("# Comment\n"), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`block`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "# Comment\nblock {}", + "block", + nil, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`block`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "# Old\nblock {}", + "block", + []string{"# New\n"}, + Tokens{ + { + Type: hclsyntax.TokenComment, + Bytes: []byte("# New\n"), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`block`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s %s in %s", test.typeName, test.blockComments, test.src), func(t *testing.T) { + f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + b := f.Body().FirstMatchingBlock(test.typeName, nil) + b.SetComments(test.blockComments) + got := f.BuildTokens(nil) + format(got) + if !reflect.DeepEqual(got, test.want) { + diff := cmp.Diff(test.want, got) + t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff) + } + }) + } +} diff --git a/hclwrite/ast_body.go b/hclwrite/ast_body.go index 6321509a..badb81b7 100644 --- a/hclwrite/ast_body.go +++ b/hclwrite/ast_body.go @@ -137,6 +137,111 @@ func (b *Body) RemoveBlock(block *Block) bool { return false } +// AttributeLeadComments returns the comments that annotate an attribute. +// +// The return value is nil if there was no such attribute, an empty slice if +// it had no lead comments, or a slice of strings representing the raw text of +// the comments, including leading and trailing comment markers. +func (b *Body) AttributeLeadComments(name string) []string { + attr := b.GetAttribute(name) + if attr == nil { + return nil + } + + tokens := attr.leadComments.content.(*comments).tokens + attrComments := make([]string, len(tokens)) + for i := range tokens { + attrComments[i] = string(tokens[i].Bytes) + } + return attrComments +} + +// SetAttributeLeadComments replaces the comments that annotate an attribute. +// +// Each item should contain any leading and trailing markers, i.e. single- +// line comments should start with either “#” or “//” and end with a +// newline character, and block comments should start with “/*” and end with +// “*/”. +func (b *Body) SetAttributeLeadComments(name string, leadComments []string) *Attribute { + attr := b.GetAttribute(name) + if attr == nil { + return nil + } + + c := attr.leadComments.content.(*comments) + c.tokens = make(Tokens, len(leadComments)) + + for i := range leadComments { + c.tokens[i] = &Token{ + Type: hclsyntax.TokenComment, + Bytes: []byte(leadComments[i]), + } + } + + return attr +} + +// Single-comment alternative to [SetAttributeLeadComments] +func (b *Body) SetAttributeLeadComment(name string, leadComment string) *Attribute { + return b.SetAttributeLeadComments(name, []string{leadComment}) +} + +// AttributeLineComments returns the comments that follow an attribute on +// the same line. +// +// The return value is nil if there was no such attribute, an empty slice if +// it had no line comments, or a slice of strings representing the raw text of +// the comments, including leading and trailing comment markers. +func (b *Body) AttributeLineComments(name string) []string { + attr := b.GetAttribute(name) + if attr == nil { + return nil + } + + tokens := attr.lineComments.content.(*comments).tokens + attrComments := make([]string, len(tokens)) + for i := range tokens { + attrComments[i] = string(tokens[i].Bytes) + } + return attrComments +} + +// SetAttributeLineComments replaces the comments that follow an attribute on +// the same line. +// +// Each item should contain any leading and trailing markers, i.e. single- +// line comments should start with either “#” or “//” and end with a +// newline character, and block comments should start with “/*” and end with +// “*/”. +// +// While it is technically possible to set multiple single-line comments using +// this function, doing so is discouraged since the parser would never produce +// such constructs. For single-line comments, prefer to use +// [SetAttributeLineComment]. +func (b *Body) SetAttributeLineComments(name string, lineComments []string) *Attribute { + attr := b.GetAttribute(name) + if attr == nil { + return nil + } + + c := attr.lineComments.content.(*comments) + c.tokens = make(Tokens, len(lineComments)) + + for i := range lineComments { + c.tokens[i] = &Token{ + Type: hclsyntax.TokenComment, + Bytes: []byte(lineComments[i]), + } + } + + return attr +} + +// Single-comment alternative to [SetAttributeLineComments] +func (b *Body) SetAttributeLineComment(name string, lineComment string) *Attribute { + return b.SetAttributeLineComments(name, []string{lineComment}) +} + // SetAttributeRaw either replaces the expression of an existing attribute // of the given name or adds a new attribute definition to the end of the block, // using the given tokens verbatim as the expression. diff --git a/hclwrite/ast_body_test.go b/hclwrite/ast_body_test.go index 1e235234..bd6b202e 100644 --- a/hclwrite/ast_body_test.go +++ b/hclwrite/ast_body_test.go @@ -769,6 +769,374 @@ func TestBodySetAttributeTraversal(t *testing.T) { } } +func TestBodyAttributeLeadComments(t *testing.T) { + tests := []struct { + src string + name string + want []string + }{ + { + "", + "a", + nil, + }, + { + "a = 1", + "a", + []string{}, + }, + { + "# Comment\na = 1", + "a", + []string{"# Comment\n"}, + }, + { + "// Comment\na = 1", + "a", + []string{"// Comment\n"}, + }, + { + "/* Comment */ a = 1", + "a", + []string{"/* Comment */"}, + }, + { + "# First line\n# Second line\na = 1", + "a", + []string{"# First line\n", "# Second line\n"}, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s in %s", test.name, test.src), func(t *testing.T) { + f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + got := f.Body().AttributeLeadComments(test.name) + if !reflect.DeepEqual(got, test.want) { + diff := cmp.Diff(test.want, got) + t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff) + } + }) + } +} + +func TestBodySetAttributeLeadComments(t *testing.T) { + tests := []struct { + src string + name string + leadComments []string + want Tokens + }{ + { + "", + "a", + []string{"# Comment\n"}, + Tokens{{Type: hclsyntax.TokenEOF, Bytes: []byte{}}}, + }, + { + "a = 1", + "a", + []string{"# Comment\n"}, + Tokens{ + { + Type: hclsyntax.TokenComment, + Bytes: []byte("# Comment\n"), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`a`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNumberLit, + Bytes: []byte{'1'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "# Old\na = 1", + "a", + nil, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`a`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNumberLit, + Bytes: []byte{'1'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "# Old\na = 1", + "a", + []string{"# New\n"}, + Tokens{ + { + Type: hclsyntax.TokenComment, + Bytes: []byte("# New\n"), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`a`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNumberLit, + Bytes: []byte{'1'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s in %s", test.name, test.src), func(t *testing.T) { + f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + f.Body().SetAttributeLeadComments(test.name, test.leadComments) + got := f.BuildTokens(nil) + format(got) + if !reflect.DeepEqual(got, test.want) { + diff := cmp.Diff(test.want, got) + t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff) + } + }) + } +} + +func TestBodyAttributeLineComments(t *testing.T) { + tests := []struct { + src string + name string + want []string + }{ + { + "", + "a", + nil, + }, + { + "a = 1", + "a", + []string{}, + }, + { + "a = 1 # Comment\n", + "a", + []string{"# Comment\n"}, + }, + { + "a = 1 // Comment\n", + "a", + []string{"// Comment\n"}, + }, + { + "a = 1 /* Comment */", + "a", + []string{"/* Comment */"}, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s in %s", test.name, test.src), func(t *testing.T) { + f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + got := f.Body().AttributeLineComments(test.name) + if !reflect.DeepEqual(got, test.want) { + diff := cmp.Diff(test.want, got) + t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff) + } + }) + } +} + +func TestBodySetAttributeLineComments(t *testing.T) { + tests := []struct { + src string + name string + lineComments []string + want Tokens + }{ + { + "", + "a", + []string{"# Comment\n"}, + Tokens{ + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }}, + }, + { + "a = 1", + "a", + []string{"# Comment\n"}, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`a`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNumberLit, + Bytes: []byte{'1'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenComment, + Bytes: []byte("# Comment\n"), + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "a = 1 # Old", + "a", + nil, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`a`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNumberLit, + Bytes: []byte{'1'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "a = 1 # Old\n", + "a", + []string{"# New\n"}, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`a`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEqual, + Bytes: []byte{'='}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNumberLit, + Bytes: []byte{'1'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenComment, + Bytes: []byte("# New\n"), + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s in %s", test.name, test.src), func(t *testing.T) { + f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + f.Body().SetAttributeLineComments(test.name, test.lineComments) + got := f.BuildTokens(nil) + format(got) + if !reflect.DeepEqual(got, test.want) { + diff := cmp.Diff(test.want, got) + t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff) + } + }) + } +} + func TestBodySetAttributeRaw(t *testing.T) { tests := []struct { src string