Skip to content

Commit 663cfeb

Browse files
authored
Merge pull request #225 from oferchen/codex/edit-align-to-order-terraform-attributes
2 parents f2b1bf9 + 2d143e9 commit 663cfeb

File tree

3 files changed

+157
-32
lines changed

3 files changed

+157
-32
lines changed

internal/align/terraform.go

Lines changed: 141 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
package align
33

44
import (
5+
"fmt"
56
"sort"
67
"strings"
78

9+
"github.com/hashicorp/hcl/v2/hclsyntax"
810
"github.com/hashicorp/hcl/v2/hclwrite"
11+
ihcl "github.com/oferchen/hclalign/internal/hcl"
912
)
1013

1114
type terraformStrategy struct{}
@@ -16,10 +19,9 @@ func (terraformStrategy) Name() string { return "terraform" }
1619
// are sorted alphabetically. Nested blocks are sorted by type and labels while
1720
// preserving the ordering of their contents. If a required_providers block is
1821
// present, the provider entries inside it are also sorted alphabetically.
19-
func (terraformStrategy) Align(block *hclwrite.Block, _ *Options) error {
22+
func (terraformStrategy) Align(block *hclwrite.Block, opts *Options) error {
2023
body := block.Body()
2124

22-
// Sort provider entries within required_providers blocks
2325
for _, nb := range body.Blocks() {
2426
if nb.Type() == "required_providers" {
2527
attrs := nb.Body().Attributes()
@@ -34,31 +36,154 @@ func (terraformStrategy) Align(block *hclwrite.Block, _ *Options) error {
3436
}
3537
}
3638

37-
// Order top-level blocks by type then labels
39+
attrs := body.Attributes()
3840
blocks := body.Blocks()
39-
sort.SliceStable(blocks, func(i, j int) bool {
40-
bi, bj := blocks[i], blocks[j]
41+
42+
canonical := []string{"required_version", "required_providers", "experiments", "cloud", "backend"}
43+
canonSet := make(map[string]struct{}, len(canonical))
44+
for _, n := range canonical {
45+
canonSet[n] = struct{}{}
46+
}
47+
48+
if opts != nil && opts.Strict {
49+
var missing []string
50+
for _, n := range canonical {
51+
if _, ok := attrs[n]; ok {
52+
continue
53+
}
54+
found := false
55+
for _, b := range blocks {
56+
if b.Type() == n {
57+
found = true
58+
break
59+
}
60+
}
61+
if !found {
62+
missing = append(missing, n)
63+
}
64+
}
65+
if len(missing) > 0 {
66+
sort.Strings(missing)
67+
return fmt.Errorf("terraform: missing attributes or blocks: %s", strings.Join(missing, ", "))
68+
}
69+
var unknown []string
70+
for name := range attrs {
71+
if _, ok := canonSet[name]; !ok {
72+
unknown = append(unknown, name)
73+
}
74+
}
75+
for _, b := range blocks {
76+
if _, ok := canonSet[b.Type()]; !ok {
77+
unknown = append(unknown, b.Type())
78+
}
79+
}
80+
if len(unknown) > 0 {
81+
sort.Strings(unknown)
82+
return fmt.Errorf("terraform: unknown attributes or blocks: %s", strings.Join(unknown, ", "))
83+
}
84+
}
85+
86+
tokens := body.BuildTokens(nil)
87+
newline := ihcl.DetectLineEnding(tokens)
88+
trailingComma := ihcl.HasTrailingComma(tokens)
89+
90+
attrTokens := map[string]ihcl.AttrTokens{}
91+
for name, attr := range attrs {
92+
attrTokens[name] = ihcl.ExtractAttrTokens(attr)
93+
body.RemoveAttribute(name)
94+
}
95+
96+
var reqProviders, cloudBlock, backendBlock *hclwrite.Block
97+
otherBlocks := make([]*hclwrite.Block, 0, len(blocks))
98+
for _, b := range blocks {
99+
body.RemoveBlock(b)
100+
switch b.Type() {
101+
case "required_providers":
102+
reqProviders = b
103+
case "cloud":
104+
cloudBlock = b
105+
case "backend":
106+
backendBlock = b
107+
default:
108+
otherBlocks = append(otherBlocks, b)
109+
}
110+
}
111+
112+
sort.SliceStable(otherBlocks, func(i, j int) bool {
113+
bi, bj := otherBlocks[i], otherBlocks[j]
41114
if bi.Type() != bj.Type() {
42115
return bi.Type() < bj.Type()
43116
}
44117
return strings.Join(bi.Labels(), "\x00") < strings.Join(bj.Labels(), "\x00")
45118
})
46-
for _, b := range body.Blocks() {
47-
body.RemoveBlock(b)
119+
120+
otherAttrNames := make([]string, 0, len(attrTokens))
121+
for name := range attrTokens {
122+
if name != "required_version" && name != "experiments" {
123+
otherAttrNames = append(otherAttrNames, name)
124+
}
48125
}
49-
for _, b := range blocks {
50-
body.AppendBlock(b)
126+
sort.Strings(otherAttrNames)
127+
128+
type item struct {
129+
name string
130+
block *hclwrite.Block
131+
isAttr bool
51132
}
52133

53-
// Gather and order attributes
54-
attrs := body.Attributes()
55-
names := make([]string, 0, len(attrs))
56-
for name := range attrs {
57-
names = append(names, name)
134+
var items []item
135+
if _, ok := attrTokens["required_version"]; ok {
136+
items = append(items, item{name: "required_version", isAttr: true})
137+
}
138+
if reqProviders != nil {
139+
items = append(items, item{block: reqProviders})
140+
}
141+
if _, ok := attrTokens["experiments"]; ok {
142+
items = append(items, item{name: "experiments", isAttr: true})
143+
}
144+
if cloudBlock != nil {
145+
items = append(items, item{block: cloudBlock})
146+
}
147+
if backendBlock != nil {
148+
items = append(items, item{block: backendBlock})
149+
}
150+
for _, name := range otherAttrNames {
151+
items = append(items, item{name: name, isAttr: true})
152+
}
153+
for _, b := range otherBlocks {
154+
items = append(items, item{block: b})
58155
}
59-
sort.Strings(names)
60156

61-
return reorderBlock(block, names)
157+
body.Clear()
158+
if len(items) > 0 {
159+
body.AppendUnstructuredTokens(hclwrite.Tokens{
160+
&hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: newline},
161+
})
162+
}
163+
for _, it := range items {
164+
if it.isAttr {
165+
tok := attrTokens[it.name]
166+
body.AppendUnstructuredTokens(tok.LeadTokens)
167+
body.SetAttributeRaw(it.name, tok.ExprTokens)
168+
} else {
169+
body.AppendUnstructuredTokens(hclwrite.Tokens{
170+
&hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: newline},
171+
})
172+
body.AppendBlock(it.block)
173+
}
174+
}
175+
if trailingComma && len(items) > 0 {
176+
body.AppendUnstructuredTokens(hclwrite.Tokens{
177+
&hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte(",")},
178+
})
179+
}
180+
toks := body.BuildTokens(nil)
181+
if len(toks) > 0 && toks[len(toks)-1].Type != hclsyntax.TokenNewline {
182+
body.AppendUnstructuredTokens(hclwrite.Tokens{
183+
&hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: newline},
184+
})
185+
}
186+
return nil
62187
}
63188

64189
func init() { Register(terraformStrategy{}) }

tests/cases/terraform/aligned.tf

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
terraform {
22
required_version = ">= 1.0"
33

4-
backend "s3" {
5-
region = "us-east-1"
6-
}
7-
8-
cloud {
9-
organization = "hashicorp"
10-
}
11-
124
required_providers {
135
aws = {
146
version = "~> 4.0"
@@ -19,4 +11,12 @@ terraform {
1911
version = "~> 3.0"
2012
}
2113
}
14+
15+
cloud {
16+
organization = "hashicorp"
17+
}
18+
19+
backend "s3" {
20+
region = "us-east-1"
21+
}
2222
}

tests/cases/terraform/out.tf

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
terraform {
22
required_version = ">= 1.0"
33

4-
backend "s3" {
5-
region = "us-east-1"
6-
}
7-
8-
cloud {
9-
organization = "hashicorp"
10-
}
11-
124
required_providers {
135
aws = {
146
version = "~> 4.0"
@@ -19,4 +11,12 @@ terraform {
1911
version = "~> 3.0"
2012
}
2113
}
14+
15+
cloud {
16+
organization = "hashicorp"
17+
}
18+
19+
backend "s3" {
20+
region = "us-east-1"
21+
}
2222
}

0 commit comments

Comments
 (0)