Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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/
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,18 @@ cfg := &Config{Name: "app", Timeout: 30, debug: true}

</details>

**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
Expand Down
6 changes: 4 additions & 2 deletions pkg/formatter/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
115 changes: 115 additions & 0 deletions pkg/formatter/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
})
}
62 changes: 62 additions & 0 deletions pkg/formatter/testdata/expected.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"os"
"strings"
)

Expand Down Expand Up @@ -212,19 +213,80 @@ 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{
Address: "addr", Name: "test", age: 25, count: 1,
}
}

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
Expand Down
51 changes: 51 additions & 0 deletions pkg/formatter/testdata/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"os"
"strings"
)

Expand Down Expand Up @@ -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{}
}