From 5e5b4b34a70d1a85130637506fef1e46928d7ce0 Mon Sep 17 00:00:00 2001 From: Ofer Chen Date: Fri, 29 Aug 2025 21:27:08 +0200 Subject: [PATCH] feat: order meta arguments before schema attrs --- internal/align/resource.go | 144 ++++++++++++++++++++++++++++---- internal/align/resource_test.go | 4 +- tests/cases/data/aligned.tf | 5 ++ tests/cases/data/fmt.tf | 7 +- tests/cases/data/in.tf | 7 +- tests/cases/resource/aligned.tf | 14 +++- tests/cases/resource/fmt.tf | 14 +++- tests/cases/resource/in.tf | 14 +++- 8 files changed, 178 insertions(+), 31 deletions(-) diff --git a/internal/align/resource.go b/internal/align/resource.go index 0f440a5..66aac29 100644 --- a/internal/align/resource.go +++ b/internal/align/resource.go @@ -6,7 +6,10 @@ import ( "sort" "strings" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclwrite" + + ihcl "github.com/oferchen/hclalign/internal/hcl" ) type resourceStrategy struct{} @@ -20,7 +23,8 @@ func (resourceStrategy) Align(block *hclwrite.Block, opts *Options) error { func init() { Register(resourceStrategy{}) } func schemaAwareOrder(block *hclwrite.Block, opts *Options) error { - attrs := block.Body().Attributes() + body := block.Body() + attrs := body.Attributes() names := make([]string, 0, len(attrs)) for name := range attrs { names = append(names, name) @@ -30,7 +34,9 @@ func schemaAwareOrder(block *hclwrite.Block, opts *Options) error { return reorderBlock(block, names) } - var req, opt, comp, meta, unk []string + originalOrder := ihcl.AttributeOrder(body, attrs) + + var req, opt, comp []string for _, name := range names { if _, ok := opts.Schema.Required[name]; ok { req = append(req, name) @@ -44,17 +50,38 @@ func schemaAwareOrder(block *hclwrite.Block, opts *Options) error { comp = append(comp, name) continue } - if _, ok := opts.Schema.Meta[name]; ok { - meta = append(meta, name) - continue - } - unk = append(unk, name) } sort.Strings(req) sort.Strings(opt) sort.Strings(comp) - sort.Strings(meta) - sort.Strings(unk) + + metaAttrs := []string{} + for _, n := range []string{"provider", "count", "for_each", "depends_on"} { + if _, ok := attrs[n]; ok { + metaAttrs = append(metaAttrs, n) + } + } + + known := map[string]struct{}{} + for _, n := range metaAttrs { + known[n] = struct{}{} + } + for _, n := range req { + known[n] = struct{}{} + } + for _, n := range opt { + known[n] = struct{}{} + } + for _, n := range comp { + known[n] = struct{}{} + } + + var unk []string + for _, n := range originalOrder { + if _, ok := known[n]; !ok { + unk = append(unk, n) + } + } if opts.Strict { for r := range opts.Schema.Required { @@ -67,10 +94,97 @@ func schemaAwareOrder(block *hclwrite.Block, opts *Options) error { } } - final := append([]string{}, req...) - final = append(final, opt...) - final = append(final, comp...) - final = append(final, meta...) - final = append(final, unk...) - return reorderBlock(block, final) + blocks := body.Blocks() + var lifecycleBlock *hclwrite.Block + var provisionerBlocks []*hclwrite.Block + var otherBlocks []*hclwrite.Block + for _, nb := range blocks { + switch nb.Type() { + case "lifecycle": + lifecycleBlock = nb + case "provisioner": + provisionerBlocks = append(provisionerBlocks, nb) + default: + otherBlocks = append(otherBlocks, nb) + } + } + + tokens := body.BuildTokens(nil) + newline := ihcl.DetectLineEnding(tokens) + trailingComma := ihcl.HasTrailingComma(tokens) + + attrTokensMap := map[string]ihcl.AttrTokens{} + for name, attr := range attrs { + attrTokensMap[name] = ihcl.ExtractAttrTokens(attr) + body.RemoveAttribute(name) + } + for _, nb := range blocks { + body.RemoveBlock(nb) + } + + body.Clear() + if len(metaAttrs) > 0 || lifecycleBlock != nil || len(provisionerBlocks) > 0 || len(req) > 0 || len(opt) > 0 || len(comp) > 0 || len(unk) > 0 || len(otherBlocks) > 0 { + body.AppendUnstructuredTokens(hclwrite.Tokens{ + &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: newline}, + }) + } + + appendAttr := func(name string) { + if tok, ok := attrTokensMap[name]; ok { + body.AppendUnstructuredTokens(tok.LeadTokens) + body.SetAttributeRaw(name, tok.ExprTokens) + } + } + + for _, name := range metaAttrs { + appendAttr(name) + } + + if lifecycleBlock != nil { + body.AppendUnstructuredTokens(hclwrite.Tokens{ + &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: newline}, + }) + body.AppendBlock(lifecycleBlock) + } + for _, nb := range provisionerBlocks { + body.AppendUnstructuredTokens(hclwrite.Tokens{ + &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: newline}, + }) + body.AppendBlock(nb) + } + + for _, name := range req { + appendAttr(name) + } + for _, name := range opt { + appendAttr(name) + } + for _, name := range comp { + appendAttr(name) + } + for _, name := range unk { + appendAttr(name) + } + + for _, nb := range otherBlocks { + body.AppendUnstructuredTokens(hclwrite.Tokens{ + &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: newline}, + }) + body.AppendBlock(nb) + } + + if trailingComma && (len(metaAttrs) > 0 || lifecycleBlock != nil || len(provisionerBlocks) > 0 || len(req) > 0 || len(opt) > 0 || len(comp) > 0 || len(unk) > 0 || len(otherBlocks) > 0) { + body.AppendUnstructuredTokens(hclwrite.Tokens{ + &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte(",")}, + }) + } + + toks := body.BuildTokens(nil) + if len(toks) > 0 && toks[len(toks)-1].Type != hclsyntax.TokenNewline { + body.AppendUnstructuredTokens(hclwrite.Tokens{ + &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: newline}, + }) + } + + return nil } diff --git a/internal/align/resource_test.go b/internal/align/resource_test.go index d56cd3c..782ad07 100644 --- a/internal/align/resource_test.go +++ b/internal/align/resource_test.go @@ -37,11 +37,11 @@ func TestSchemaAwareOrder(t *testing.T) { got := string(file.Bytes()) exp := `resource "test_thing" "ex" { + provider = "p" + depends_on = [] foo = 1 bar = 2 baz = 3 - depends_on = [] - provider = "p" random = 4 }` require.Equal(t, exp, got) diff --git a/tests/cases/data/aligned.tf b/tests/cases/data/aligned.tf index 1a109c9..80d36dc 100644 --- a/tests/cases/data/aligned.tf +++ b/tests/cases/data/aligned.tf @@ -1,4 +1,9 @@ data "aws_ami" "example" { + provider = aws.us + count = 1 + depends_on = [] owners = ["amazon"] most_recent = true + bar = "bar" + foo = "foo" } diff --git a/tests/cases/data/fmt.tf b/tests/cases/data/fmt.tf index e2f1e43..5c6d7a8 100644 --- a/tests/cases/data/fmt.tf +++ b/tests/cases/data/fmt.tf @@ -1,4 +1,9 @@ data "aws_ami" "example" { - most_recent = true owners = ["amazon"] + bar = "bar" + provider = aws.us + foo = "foo" + count = 1 + depends_on = [] + most_recent = true } diff --git a/tests/cases/data/in.tf b/tests/cases/data/in.tf index e2f1e43..5c6d7a8 100644 --- a/tests/cases/data/in.tf +++ b/tests/cases/data/in.tf @@ -1,4 +1,9 @@ data "aws_ami" "example" { - most_recent = true owners = ["amazon"] + bar = "bar" + provider = aws.us + foo = "foo" + count = 1 + depends_on = [] + most_recent = true } diff --git a/tests/cases/resource/aligned.tf b/tests/cases/resource/aligned.tf index 072cbe8..d764963 100644 --- a/tests/cases/resource/aligned.tf +++ b/tests/cases/resource/aligned.tf @@ -1,8 +1,14 @@ resource "aws_s3_bucket" "b" { - bucket = "b" - acl = "private" - tags = {} - id = "id" + provider = "aws.us" + count = 1 + for_each = {} + depends_on = [] + bucket = "b" + acl = "private" + tags = {} + id = "id" + bar = "bar" + foo = "foo" } resource "null_resource" "n" { diff --git a/tests/cases/resource/fmt.tf b/tests/cases/resource/fmt.tf index c611fb6..52bfa96 100644 --- a/tests/cases/resource/fmt.tf +++ b/tests/cases/resource/fmt.tf @@ -1,8 +1,14 @@ resource "aws_s3_bucket" "b" { - tags = {} - id = "id" - bucket = "b" - acl = "private" + bar = "bar" + provider = "aws.us" + for_each = {} + foo = "foo" + depends_on = [] + bucket = "b" + count = 1 + acl = "private" + tags = {} + id = "id" } resource "null_resource" "n" { diff --git a/tests/cases/resource/in.tf b/tests/cases/resource/in.tf index c611fb6..52bfa96 100644 --- a/tests/cases/resource/in.tf +++ b/tests/cases/resource/in.tf @@ -1,8 +1,14 @@ resource "aws_s3_bucket" "b" { - tags = {} - id = "id" - bucket = "b" - acl = "private" + bar = "bar" + provider = "aws.us" + for_each = {} + foo = "foo" + depends_on = [] + bucket = "b" + count = 1 + acl = "private" + tags = {} + id = "id" } resource "null_resource" "n" {