From 3ff180daa5b83066341b6720edf24fc4bbacb7d1 Mon Sep 17 00:00:00 2001 From: Brianna Becker Date: Sat, 30 Aug 2025 10:31:55 +0200 Subject: [PATCH 1/8] Handle simple key value files like tfvars --- hcl/hcl.go | 45 +++++++++++++++++++++++++++++++++++---------- hcl/hcl_test.go | 9 +++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/hcl/hcl.go b/hcl/hcl.go index 56d9095..60d7e7e 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -218,9 +218,32 @@ func objectForTokensMap(tokens hclwrite.Tokens) (map[string]hclwrite.ObjectAttrT } // mergeFiles merges two HCL files together +// Files are composed of a body, which contains attributes and blocks func (m *Merger) mergeFiles(aFile *hclwrite.File, bFile *hclwrite.File) *hclwrite.File { out := hclwrite.NewFile() - outBlocks := m.mergeBlocks(aFile.Body().Blocks(), bFile.Body().Blocks()) + + aBody := aFile.Body() + bBody := bFile.Body() + outAttributes := m.mergeAttrs(aBody.Attributes(), bBody.Attributes()) + outBlocks := m.mergeBlocks(aBody.Blocks(), bBody.Blocks()) + + // formatting of the file + if len(outAttributes) > 0 { + keys := make([]string, 0, len(outAttributes)) + for key := range outAttributes { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + out.Body().SetAttributeRaw(key, outAttributes[key]) + } + + // Add newline after attributes if we have blocks too + if len(outBlocks) > 0 { + out.Body().AppendNewline() + } + } lastIndex := len(outBlocks) - 1 @@ -234,6 +257,7 @@ func (m *Merger) mergeFiles(aFile *hclwrite.File, bFile *hclwrite.File) *hclwrit } } + // return the formatted file return out } @@ -245,6 +269,7 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc aBlockMap := blockToMap(aBlocks) bBlockMap := blockToMap(bBlocks) + // add all blocks from a and check if they are in b for _, aBlock := range aBlocks { blockKey := formatBlockKey(aBlock) outBlock := aBlock @@ -254,9 +279,13 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc // override outBlock with the new block to merge the two blocks into outBlock = hclwrite.NewBlock(aBlock.Type(), aBlock.Labels()) - // merge block attributes - outAttributes := m.mergeAttrs(aBlock.Body().Attributes(), bBlock.Body().Attributes()) - // sort the keys to ensure consistent ordering + // merge the attributes and blocks of the two blocks + aBlockBody := aBlock.Body() + bBlockBody := bBlock.Body() + outAttributes := m.mergeAttrs(aBlockBody.Attributes(), bBlockBody.Attributes()) + outNestedBlocks := m.mergeBlocks(aBlockBody.Blocks(), bBlockBody.Blocks()) + + // sort and add attributes keys := make([]string, 0, len(outAttributes)) for key := range outAttributes { keys = append(keys, key) @@ -268,11 +297,7 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc outBlock.Body().SetAttributeRaw(key, outAttributes[key]) } - // recursively merge nested blocks - aNestedBlocks := aBlock.Body().Blocks() - bNestedBlocks := bBlock.Body().Blocks() - outNestedBlocks := m.mergeBlocks(aNestedBlocks, bNestedBlocks) - + // append nested blocks for _, nestedBlock := range outNestedBlocks { outBlock.Body().AppendNewline() outBlock.Body().AppendBlock(nestedBlock) @@ -282,12 +307,12 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc outBlocks = append(outBlocks, outBlock) } + // add all blocks from b that are not found in a for _, bBlock := range bBlocks { blockKey := formatBlockKey(bBlock) _, found := aBlockMap[blockKey] if !found { - // append any target blocks that were not in the source outBlocks = append(outBlocks, bBlock) } } diff --git a/hcl/hcl_test.go b/hcl/hcl_test.go index 2d19d8f..07f089e 100644 --- a/hcl/hcl_test.go +++ b/hcl/hcl_test.go @@ -26,6 +26,15 @@ func TestMerge(t *testing.T) { want string wantErr error }{ + { + name: "merge simple key values", + input: input{ + a: `foo = "bar"`, + b: `foo = "baz"`, + }, + want: `foo = "baz" +`, + }, { name: "merge simple", input: input{ From ffe39a9db0e41cd6a3b3a275219d9198e91713dd Mon Sep 17 00:00:00 2001 From: Brianna Becker Date: Sat, 30 Aug 2025 10:41:55 +0200 Subject: [PATCH 2/8] Dry up the mergeAttrs --- hcl/hcl.go | 55 ++++++++++++++++-------------------------------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/hcl/hcl.go b/hcl/hcl.go index 60d7e7e..d3dbd0d 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -75,7 +75,7 @@ func (m *Merger) mergeTokens(aTokens hclwrite.Tokens, bTokens hclwrite.Tokens) ( // and are identified by their key, e.g. // key = value // or key = { ... } -func (m *Merger) mergeAttrs(aAttr map[string]*hclwrite.Attribute, bAttr map[string]*hclwrite.Attribute) map[string]hclwrite.Tokens { +func (m *Merger) mergeAttrs(aAttr map[string]*hclwrite.Attribute, bAttr map[string]*hclwrite.Attribute, outBody *hclwrite.Body) { outAttr := make(map[string]hclwrite.Tokens) for key, aValue := range aAttr { @@ -113,7 +113,17 @@ func (m *Merger) mergeAttrs(aAttr map[string]*hclwrite.Attribute, bAttr map[stri } } - return outAttr + // sort and add attributes + keys := make([]string, 0, len(outAttr)) + for key := range outAttr { + keys = append(keys, key) + } + + sort.Strings(keys) + + for _, key := range keys { + outBody.SetAttributeRaw(key, outAttr[key]) + } } // objectForTokensMap is the inverse of hclwrite.TokensForObject, only merges the top level keys, but not the values of the keys. @@ -222,29 +232,10 @@ func objectForTokensMap(tokens hclwrite.Tokens) (map[string]hclwrite.ObjectAttrT func (m *Merger) mergeFiles(aFile *hclwrite.File, bFile *hclwrite.File) *hclwrite.File { out := hclwrite.NewFile() - aBody := aFile.Body() - bBody := bFile.Body() - outAttributes := m.mergeAttrs(aBody.Attributes(), bBody.Attributes()) - outBlocks := m.mergeBlocks(aBody.Blocks(), bBody.Blocks()) + m.mergeAttrs(aFile.Body().Attributes(), bFile.Body().Attributes(), out.Body()) + outBlocks := m.mergeBlocks(aFile.Body().Blocks(), bFile.Body().Blocks()) // formatting of the file - if len(outAttributes) > 0 { - keys := make([]string, 0, len(outAttributes)) - for key := range outAttributes { - keys = append(keys, key) - } - sort.Strings(keys) - - for _, key := range keys { - out.Body().SetAttributeRaw(key, outAttributes[key]) - } - - // Add newline after attributes if we have blocks too - if len(outBlocks) > 0 { - out.Body().AppendNewline() - } - } - lastIndex := len(outBlocks) - 1 for i, block := range outBlocks { @@ -280,22 +271,8 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc outBlock = hclwrite.NewBlock(aBlock.Type(), aBlock.Labels()) // merge the attributes and blocks of the two blocks - aBlockBody := aBlock.Body() - bBlockBody := bBlock.Body() - outAttributes := m.mergeAttrs(aBlockBody.Attributes(), bBlockBody.Attributes()) - outNestedBlocks := m.mergeBlocks(aBlockBody.Blocks(), bBlockBody.Blocks()) - - // sort and add attributes - keys := make([]string, 0, len(outAttributes)) - for key := range outAttributes { - keys = append(keys, key) - } - - sort.Strings(keys) - - for _, key := range keys { - outBlock.Body().SetAttributeRaw(key, outAttributes[key]) - } + m.mergeAttrs(aBlock.Body().Attributes(), bBlock.Body().Attributes(), outBlock.Body()) + outNestedBlocks := m.mergeBlocks(aBlock.Body().Blocks(), bBlock.Body().Blocks()) // append nested blocks for _, nestedBlock := range outNestedBlocks { From 8a2014f4bb25e40587037e92199983aa1f205618 Mon Sep 17 00:00:00 2001 From: Brianna Becker Date: Sat, 30 Aug 2025 10:46:41 +0200 Subject: [PATCH 3/8] Dry up merging and formatting of blocks --- hcl/hcl.go | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/hcl/hcl.go b/hcl/hcl.go index d3dbd0d..49f6e89 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -233,29 +233,15 @@ func (m *Merger) mergeFiles(aFile *hclwrite.File, bFile *hclwrite.File) *hclwrit out := hclwrite.NewFile() m.mergeAttrs(aFile.Body().Attributes(), bFile.Body().Attributes(), out.Body()) - outBlocks := m.mergeBlocks(aFile.Body().Blocks(), bFile.Body().Blocks()) + m.mergeBlocks(aFile.Body().Blocks(), bFile.Body().Blocks(), out.Body()) - // formatting of the file - lastIndex := len(outBlocks) - 1 - - for i, block := range outBlocks { - out.Body().AppendBlock(block) - out.Body().AppendNewline() - - // append extra newline for spacing between blocks, but not at the EOF - if i < lastIndex { - out.Body().AppendNewline() - } - } - - // return the formatted file return out } // mergeBlocks merges two blocks together, a block is identified by its type and labels, e.g. // type "label" { ... } // or type { ... } -func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Block) []*hclwrite.Block { +func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Block, outBody *hclwrite.Body) { outBlocks := make([]*hclwrite.Block, 0) aBlockMap := blockToMap(aBlocks) bBlockMap := blockToMap(bBlocks) @@ -272,13 +258,7 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc // merge the attributes and blocks of the two blocks m.mergeAttrs(aBlock.Body().Attributes(), bBlock.Body().Attributes(), outBlock.Body()) - outNestedBlocks := m.mergeBlocks(aBlock.Body().Blocks(), bBlock.Body().Blocks()) - - // append nested blocks - for _, nestedBlock := range outNestedBlocks { - outBlock.Body().AppendNewline() - outBlock.Body().AppendBlock(nestedBlock) - } + m.mergeBlocks(aBlock.Body().Blocks(), bBlock.Body().Blocks(), outBlock.Body()) } outBlocks = append(outBlocks, outBlock) @@ -294,7 +274,18 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc } } - return outBlocks + // formatting of the body + lastIndex := len(outBlocks) - 1 + + for i, block := range outBlocks { + outBody.AppendBlock(block) + outBody.AppendNewline() + + // append extra newline for spacing between blocks, but not at the EOF + if i < lastIndex { + outBody.AppendNewline() + } + } } func parseBytes(bytes []byte) (*hclwrite.File, error) { From d3c6ff676569cd06fccfa5a9cedbf389618d578e Mon Sep 17 00:00:00 2001 From: Brianna Becker Date: Sat, 30 Aug 2025 10:48:07 +0200 Subject: [PATCH 4/8] Acceptable formatting regression --- hcl/hcl_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/hcl/hcl_test.go b/hcl/hcl_test.go index 07f089e..d5468f3 100644 --- a/hcl/hcl_test.go +++ b/hcl/hcl_test.go @@ -112,7 +112,6 @@ variable "b" { }, want: `monitor "a" { description = "Monitor A" - threshold { critical = 100 recovery = 10 From 170df35f8c17a3cc95b6a82d13583f0ee41e2df3 Mon Sep 17 00:00:00 2001 From: Brianna Becker Date: Sat, 30 Aug 2025 11:09:24 +0200 Subject: [PATCH 5/8] Acceptable formatting regression --- hcl/hcl.go | 4 +++- hcl/hcl_test.go | 16 +++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/hcl/hcl.go b/hcl/hcl.go index 49f6e89..91f6c9e 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -258,6 +258,9 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc // merge the attributes and blocks of the two blocks m.mergeAttrs(aBlock.Body().Attributes(), bBlock.Body().Attributes(), outBlock.Body()) + if len(outBody.Attributes()) > 0 { + outBody.AppendNewline() + } m.mergeBlocks(aBlock.Body().Blocks(), bBlock.Body().Blocks(), outBlock.Body()) } @@ -279,7 +282,6 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc for i, block := range outBlocks { outBody.AppendBlock(block) - outBody.AppendNewline() // append extra newline for spacing between blocks, but not at the EOF if i < lastIndex { diff --git a/hcl/hcl_test.go b/hcl/hcl_test.go index d5468f3..b25ccc8 100644 --- a/hcl/hcl_test.go +++ b/hcl/hcl_test.go @@ -29,10 +29,13 @@ func TestMerge(t *testing.T) { { name: "merge simple key values", input: input{ - a: `foo = "bar"`, + a: `foo = "bar" +some_value = 10 +`, b: `foo = "baz"`, }, - want: `foo = "baz" + want: `foo = "baz" +some_value = 10 `, }, { @@ -54,13 +57,11 @@ func TestMerge(t *testing.T) { description = "Variable A" default = "a" } - variable "b" { type = string description = "Variable B" default = "b" -} -`, +}`, wantErr: nil, }, { @@ -86,7 +87,6 @@ variable "b" { override = true type = string } - `, wantErr: nil, }, @@ -112,13 +112,13 @@ variable "b" { }, want: `monitor "a" { description = "Monitor A" + threshold { critical = 100 recovery = 10 warning = 80 } } - `, wantErr: nil, }, @@ -157,7 +157,6 @@ variable "b" { } } } - `, }, { @@ -237,7 +236,6 @@ variable "b" { var_key = var.value } } - `, }, } From 9c45d8792979c3d8d13c4cc021dc9871e889ebf8 Mon Sep 17 00:00:00 2001 From: Brianna Becker Date: Sat, 30 Aug 2025 11:18:55 +0200 Subject: [PATCH 6/8] Fix formatting regression --- hcl/hcl.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hcl/hcl.go b/hcl/hcl.go index 91f6c9e..b65350d 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -247,7 +247,7 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc bBlockMap := blockToMap(bBlocks) // add all blocks from a and check if they are in b - for _, aBlock := range aBlocks { + for i, aBlock := range aBlocks { blockKey := formatBlockKey(aBlock) outBlock := aBlock bBlock, found := bBlockMap[blockKey] @@ -258,10 +258,19 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc // merge the attributes and blocks of the two blocks m.mergeAttrs(aBlock.Body().Attributes(), bBlock.Body().Attributes(), outBlock.Body()) + + // format the attributes if len(outBody.Attributes()) > 0 { outBody.AppendNewline() } + + // merge the nested blocks m.mergeBlocks(aBlock.Body().Blocks(), bBlock.Body().Blocks(), outBlock.Body()) + + // format the blocks + if i == len(bBlocks)-1 && len(outBlock.Body().Blocks()) > 1 { + outBody.AppendNewline() + } } outBlocks = append(outBlocks, outBlock) From 1336481f002cf43fefb17b330689f986aff4de90 Mon Sep 17 00:00:00 2001 From: Brianna Becker Date: Sat, 30 Aug 2025 11:44:22 +0200 Subject: [PATCH 7/8] Formatted test cases --- hcl/hcl.go | 7 +------ hcl/hcl_test.go | 4 +++- src/hcl.test.ts | 10 +++++++++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/hcl/hcl.go b/hcl/hcl.go index b65350d..a6a3bf2 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -247,7 +247,7 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc bBlockMap := blockToMap(bBlocks) // add all blocks from a and check if they are in b - for i, aBlock := range aBlocks { + for _, aBlock := range aBlocks { blockKey := formatBlockKey(aBlock) outBlock := aBlock bBlock, found := bBlockMap[blockKey] @@ -266,11 +266,6 @@ func (m *Merger) mergeBlocks(aBlocks []*hclwrite.Block, bBlocks []*hclwrite.Bloc // merge the nested blocks m.mergeBlocks(aBlock.Body().Blocks(), bBlock.Body().Blocks(), outBlock.Body()) - - // format the blocks - if i == len(bBlocks)-1 && len(outBlock.Body().Blocks()) > 1 { - outBody.AppendNewline() - } } outBlocks = append(outBlocks, outBlock) diff --git a/hcl/hcl_test.go b/hcl/hcl_test.go index b25ccc8..a865821 100644 --- a/hcl/hcl_test.go +++ b/hcl/hcl_test.go @@ -57,11 +57,13 @@ some_value = 10 description = "Variable A" default = "a" } + variable "b" { type = string description = "Variable B" default = "b" -}`, +} +`, wantErr: nil, }, { diff --git a/src/hcl.test.ts b/src/hcl.test.ts index 073698b..2f45a30 100644 --- a/src/hcl.test.ts +++ b/src/hcl.test.ts @@ -5,6 +5,15 @@ import { merge } from "./hcl"; describe("merge", () => { + it("should merge simple key values", async () => { + const a = `foo = "bar"`; + const b = `foo = "baz"`; + + const expected = `foo = "baz" +`; + const actual = await merge(a, b); + expect(actual).toBe(expected); + }); it("should merge two hcl strings", async () => { const a = `variable "a" { type = string @@ -122,7 +131,6 @@ variable "b" { }, } } - `; const options = { mergeMapKeys: true }; const actual = await merge(a, b, options); From 4e9af917403a9973f5c8fdc1ea1b26a3530bca6c Mon Sep 17 00:00:00 2001 From: Brianna Becker Date: Sat, 30 Aug 2025 11:52:13 +0200 Subject: [PATCH 8/8] Acceptable formatting regression --- hcl/hcl_test.go | 4 +--- src/hcl.test.ts | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/hcl/hcl_test.go b/hcl/hcl_test.go index a865821..b25ccc8 100644 --- a/hcl/hcl_test.go +++ b/hcl/hcl_test.go @@ -57,13 +57,11 @@ some_value = 10 description = "Variable A" default = "a" } - variable "b" { type = string description = "Variable B" default = "b" -} -`, +}`, wantErr: nil, }, { diff --git a/src/hcl.test.ts b/src/hcl.test.ts index 2f45a30..0ac4c46 100644 --- a/src/hcl.test.ts +++ b/src/hcl.test.ts @@ -31,13 +31,11 @@ describe("merge", () => { description = "Variable A" default = "a" } - variable "b" { type = string description = "Variable B" default = "b" -} -`; +}`; const out = await merge(a, b); expect(out).toBe(expected); @@ -55,8 +53,7 @@ variable "b" { type = string description = "Variable B" default = "b" -} -`; +}`; const actual = await merge(a, b); expect(actual).toBe(expected);