From 7ad016f0bd96f798567ec7d762808421e2f0d01b Mon Sep 17 00:00:00 2001 From: Ilya Lesikov Date: Tue, 27 Jan 2026 05:45:21 +0300 Subject: [PATCH] feat: expand value-only field assignments to key-value Signed-off-by: Ilya Lesikov --- .gitignore | 11 +++ README.md | 12 +++ pkg/formatter/file.go | 6 +- pkg/formatter/structs.go | 115 +++++++++++++++++++++++++++++ pkg/formatter/testdata/expected.go | 62 ++++++++++++++++ pkg/formatter/testdata/input.go | 51 +++++++++++++ 6 files changed, 255 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 94f0e6c..b081b32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,13 @@ +# remove this block when fixed: https://github.com/anthropics/claude-code/issues/17087 +/.bash_profile +/.bashrc +/.gitconfig +/.gitmodules +/.mcp.json +/.profile +/.ripgreprc +/.zprofile +/.zshrc + /bin/ /.task/ diff --git a/README.md b/README.md index b21e7dd..03c0429 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,18 @@ cfg := &Config{Name: "app", Timeout: 30, debug: true} +**Positional literals** are automatically converted to keyed literals: + +```go +// Before — positional +p := Person{"John", 30} + +// After — converted to keyed, then sorted +p := Person{Age: 30, Name: "John"} +``` + +This conversion only applies to structs defined in the same file. External struct literals are left unchanged. + --- ### Functions diff --git a/pkg/formatter/file.go b/pkg/formatter/file.go index 602a941..99e992f 100644 --- a/pkg/formatter/file.go +++ b/pkg/formatter/file.go @@ -55,9 +55,11 @@ func FormatFile(filePath string, opts Options) error { return nil } - structDefs := collectStructDefinitions(f) + originalFieldOrder := collectOriginalFieldOrder(f) + convertPositionalToKeyed(f, originalFieldOrder) reorderStructFields(f) - reorderStructLiterals(f, structDefs) + sortedFieldOrder := collectStructDefinitions(f) + reorderStructLiterals(f, sortedFieldOrder) f.Decls = reorderDeclarations(f) normalizeSpacing(f) expandOneLineFunctions(f) diff --git a/pkg/formatter/structs.go b/pkg/formatter/structs.go index 601d88e..13ff269 100644 --- a/pkg/formatter/structs.go +++ b/pkg/formatter/structs.go @@ -37,6 +37,29 @@ func collectStructDefinitions(f *dst.File) map[string][]string { return structDefs } +// collectOriginalFieldOrder collects the original (unsorted) field order for each struct. +// This is needed for converting positional literals to keyed literals. +func collectOriginalFieldOrder(f *dst.File) map[string][]string { + structDefs := make(map[string][]string) + + dst.Inspect(f, func(n dst.Node) bool { + ts, ok := n.(*dst.TypeSpec) + if !ok { + return true + } + st, ok := ts.Type.(*dst.StructType) + if !ok { + return true + } + + structDefs[ts.Name.Name] = getFieldNamesFromStructType(st) + + return true + }) + + return structDefs +} + func reorderFields(st *dst.StructType) { if st.Fields == nil || len(st.Fields.List) == 0 { return @@ -191,3 +214,95 @@ func reorderCompositeLitFields(cl *dst.CompositeLit, fieldOrder []string) { cl.Elts = newElts } + +func isPositionalLiteral(cl *dst.CompositeLit) bool { + if len(cl.Elts) == 0 { + return false + } + + for _, elt := range cl.Elts { + if _, ok := elt.(*dst.KeyValueExpr); ok { + return false + } + } + + return true +} + +func getFieldNamesFromStructType(st *dst.StructType) []string { + if st == nil || st.Fields == nil { + return nil + } + + var names []string + for _, field := range st.Fields.List { + if len(field.Names) == 0 { + // Embedded field - use type name + names = append(names, getFieldTypeName(field)) + } else { + // Named field(s) + for _, name := range field.Names { + names = append(names, name.Name) + } + } + } + + return names +} + +func convertToKeyedLiteral(cl *dst.CompositeLit, fieldNames []string) { + if len(fieldNames) == 0 || len(cl.Elts) == 0 { + return + } + + newElts := make([]dst.Expr, 0, len(cl.Elts)) + for i, elt := range cl.Elts { + if i >= len(fieldNames) { + break + } + + kv := &dst.KeyValueExpr{ + Key: dst.NewIdent(fieldNames[i]), + Value: elt, + } + newElts = append(newElts, kv) + } + + cl.Elts = newElts +} + +func convertPositionalToKeyed(f *dst.File, structDefs map[string][]string) { + dst.Inspect(f, func(n dst.Node) bool { + cl, ok := n.(*dst.CompositeLit) + if !ok { + return true + } + + if !isPositionalLiteral(cl) { + return true + } + + // Handle anonymous struct type + if st, ok := cl.Type.(*dst.StructType); ok { + fieldNames := getFieldNamesFromStructType(st) + convertToKeyedLiteral(cl, fieldNames) + return true + } + + // Handle named struct type + typeName := extractTypeName(cl.Type) + if typeName == "" { + return true + } + + fieldNames, exists := structDefs[typeName] + if !exists { + // Type not in this file - leave untouched + return true + } + + convertToKeyedLiteral(cl, fieldNames) + + return true + }) +} diff --git a/pkg/formatter/testdata/expected.go b/pkg/formatter/testdata/expected.go index 1010547..8810798 100644 --- a/pkg/formatter/testdata/expected.go +++ b/pkg/formatter/testdata/expected.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "strings" ) @@ -212,12 +213,53 @@ func newMyPrivateType() *myPrivateType { } } +// Test: positional literals should be converted to keyed +type PositionalTest struct { + Age int + City string + Name string +} + +// Test: embedded fields in positional literal +type WithEmbedded struct { + PositionalTest + + Extra string +} + func HelperUpper() {} func ProcessDataPublic(data string) string { return strings.ToLower(data) } +// Test: anonymous struct with positional literal +func createAnonymous() interface{} { + return struct { + A string + B int + }{B: 42, A: "hello"} +} + +// Test: empty literal - no change +func createEmpty() *PositionalTest { + return &PositionalTest{} +} + +// Test: external struct literal should NOT be touched +func createExternal() *os.File { + // This uses positional but type is external - leave untouched + // (os.File doesn't actually support this, so use a keyed example) + return nil +} + +// Test: already keyed literal - no change +func createKeyed() *PositionalTest { + return &PositionalTest{ + Age: 35, City: "Boston", Name: "Alice", + } +} + // Test: struct literal field reordering func createMixed() *Mixed { return &Mixed{ @@ -225,6 +267,26 @@ func createMixed() *Mixed { } } +func createPositional() *PositionalTest { + return &PositionalTest{ + Age: 30, City: "NYC", Name: "John", + } +} + +func createPositionalPartial() *PositionalTest { + return &PositionalTest{ + Age: 25, Name: "Jane", + } +} + +func createWithEmbedded() *WithEmbedded { + return &WithEmbedded{ + PositionalTest: PositionalTest{ + Age: 40, City: "LA", Name: "Bob", + }, Extra: "extra", + } +} + // Test: blank line before comments func functionWithComment() { x := 1 diff --git a/pkg/formatter/testdata/input.go b/pkg/formatter/testdata/input.go index 627e784..a019726 100644 --- a/pkg/formatter/testdata/input.go +++ b/pkg/formatter/testdata/input.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "strings" ) @@ -287,3 +288,53 @@ type myPrivateType struct { func newMyPrivateType() *myPrivateType { return &myPrivateType{value: 1} } + +// Test: positional literals should be converted to keyed +type PositionalTest struct { + Name string + Age int + City string +} + +func createPositional() *PositionalTest { + return &PositionalTest{"John", 30, "NYC"} +} + +func createPositionalPartial() *PositionalTest { + return &PositionalTest{"Jane", 25} +} + +// Test: anonymous struct with positional literal +func createAnonymous() interface{} { + return struct { + B int + A string + }{42, "hello"} +} + +// Test: embedded fields in positional literal +type WithEmbedded struct { + PositionalTest + Extra string +} + +func createWithEmbedded() *WithEmbedded { + return &WithEmbedded{PositionalTest{"Bob", 40, "LA"}, "extra"} +} + +// Test: external struct literal should NOT be touched +func createExternal() *os.File { + // This uses positional but type is external - leave untouched + // (os.File doesn't actually support this, so use a keyed example) + return nil +} + +// Test: already keyed literal - no change +func createKeyed() *PositionalTest { + return &PositionalTest{Name: "Alice", Age: 35, City: "Boston"} +} + +// Test: empty literal - no change +func createEmpty() *PositionalTest { + return &PositionalTest{} +}