Skip to content

Commit 1c9a5ea

Browse files
committed
feat: expand value-only field assignments to key-value
Signed-off-by: Ilya Lesikov <ilya@lesikov.com>
1 parent 992cbb9 commit 1c9a5ea

File tree

6 files changed

+255
-2
lines changed

6 files changed

+255
-2
lines changed

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,13 @@
1+
# remove this block when fixed: https://github.com/anthropics/claude-code/issues/17087
2+
/.bash_profile
3+
/.bashrc
4+
/.gitconfig
5+
/.gitmodules
6+
/.mcp.json
7+
/.profile
8+
/.ripgreprc
9+
/.zprofile
10+
/.zshrc
11+
112
/bin/
213
/.task/

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,18 @@ cfg := &Config{Name: "app", Timeout: 30, debug: true}
276276

277277
</details>
278278

279+
**Positional literals** are automatically converted to keyed literals:
280+
281+
```go
282+
// Before — positional
283+
p := Person{"John", 30}
284+
285+
// After — converted to keyed, then sorted
286+
p := Person{Age: 30, Name: "John"}
287+
```
288+
289+
This conversion only applies to structs defined in the same file. External struct literals are left unchanged.
290+
279291
---
280292

281293
### Functions

pkg/formatter/file.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ func FormatFile(filePath string, opts Options) error {
5555
return nil
5656
}
5757

58-
structDefs := collectStructDefinitions(f)
58+
originalFieldOrder := collectOriginalFieldOrder(f)
59+
convertPositionalToKeyed(f, originalFieldOrder)
5960
reorderStructFields(f)
60-
reorderStructLiterals(f, structDefs)
61+
sortedFieldOrder := collectStructDefinitions(f)
62+
reorderStructLiterals(f, sortedFieldOrder)
6163
f.Decls = reorderDeclarations(f)
6264
normalizeSpacing(f)
6365
expandOneLineFunctions(f)

pkg/formatter/structs.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,29 @@ func collectStructDefinitions(f *dst.File) map[string][]string {
3737
return structDefs
3838
}
3939

40+
// collectOriginalFieldOrder collects the original (unsorted) field order for each struct.
41+
// This is needed for converting positional literals to keyed literals.
42+
func collectOriginalFieldOrder(f *dst.File) map[string][]string {
43+
structDefs := make(map[string][]string)
44+
45+
dst.Inspect(f, func(n dst.Node) bool {
46+
ts, ok := n.(*dst.TypeSpec)
47+
if !ok {
48+
return true
49+
}
50+
st, ok := ts.Type.(*dst.StructType)
51+
if !ok {
52+
return true
53+
}
54+
55+
structDefs[ts.Name.Name] = getFieldNamesFromStructType(st)
56+
57+
return true
58+
})
59+
60+
return structDefs
61+
}
62+
4063
func reorderFields(st *dst.StructType) {
4164
if st.Fields == nil || len(st.Fields.List) == 0 {
4265
return
@@ -191,3 +214,95 @@ func reorderCompositeLitFields(cl *dst.CompositeLit, fieldOrder []string) {
191214

192215
cl.Elts = newElts
193216
}
217+
218+
func isPositionalLiteral(cl *dst.CompositeLit) bool {
219+
if len(cl.Elts) == 0 {
220+
return false
221+
}
222+
223+
for _, elt := range cl.Elts {
224+
if _, ok := elt.(*dst.KeyValueExpr); ok {
225+
return false
226+
}
227+
}
228+
229+
return true
230+
}
231+
232+
func getFieldNamesFromStructType(st *dst.StructType) []string {
233+
if st == nil || st.Fields == nil {
234+
return nil
235+
}
236+
237+
var names []string
238+
for _, field := range st.Fields.List {
239+
if len(field.Names) == 0 {
240+
// Embedded field - use type name
241+
names = append(names, getFieldTypeName(field))
242+
} else {
243+
// Named field(s)
244+
for _, name := range field.Names {
245+
names = append(names, name.Name)
246+
}
247+
}
248+
}
249+
250+
return names
251+
}
252+
253+
func convertToKeyedLiteral(cl *dst.CompositeLit, fieldNames []string) {
254+
if len(fieldNames) == 0 || len(cl.Elts) == 0 {
255+
return
256+
}
257+
258+
newElts := make([]dst.Expr, 0, len(cl.Elts))
259+
for i, elt := range cl.Elts {
260+
if i >= len(fieldNames) {
261+
break
262+
}
263+
264+
kv := &dst.KeyValueExpr{
265+
Key: dst.NewIdent(fieldNames[i]),
266+
Value: elt,
267+
}
268+
newElts = append(newElts, kv)
269+
}
270+
271+
cl.Elts = newElts
272+
}
273+
274+
func convertPositionalToKeyed(f *dst.File, structDefs map[string][]string) {
275+
dst.Inspect(f, func(n dst.Node) bool {
276+
cl, ok := n.(*dst.CompositeLit)
277+
if !ok {
278+
return true
279+
}
280+
281+
if !isPositionalLiteral(cl) {
282+
return true
283+
}
284+
285+
// Handle anonymous struct type
286+
if st, ok := cl.Type.(*dst.StructType); ok {
287+
fieldNames := getFieldNamesFromStructType(st)
288+
convertToKeyedLiteral(cl, fieldNames)
289+
return true
290+
}
291+
292+
// Handle named struct type
293+
typeName := extractTypeName(cl.Type)
294+
if typeName == "" {
295+
return true
296+
}
297+
298+
fieldNames, exists := structDefs[typeName]
299+
if !exists {
300+
// Type not in this file - leave untouched
301+
return true
302+
}
303+
304+
convertToKeyedLiteral(cl, fieldNames)
305+
306+
return true
307+
})
308+
}

pkg/formatter/testdata/expected.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"fmt"
5+
"os"
56
"strings"
67
)
78

@@ -212,19 +213,80 @@ func newMyPrivateType() *myPrivateType {
212213
}
213214
}
214215

216+
// Test: positional literals should be converted to keyed
217+
type PositionalTest struct {
218+
Age int
219+
City string
220+
Name string
221+
}
222+
223+
// Test: embedded fields in positional literal
224+
type WithEmbedded struct {
225+
PositionalTest
226+
227+
Extra string
228+
}
229+
215230
func HelperUpper() {}
216231

217232
func ProcessDataPublic(data string) string {
218233
return strings.ToLower(data)
219234
}
220235

236+
// Test: anonymous struct with positional literal
237+
func createAnonymous() interface{} {
238+
return struct {
239+
A string
240+
B int
241+
}{B: 42, A: "hello"}
242+
}
243+
244+
// Test: empty literal - no change
245+
func createEmpty() *PositionalTest {
246+
return &PositionalTest{}
247+
}
248+
249+
// Test: external struct literal should NOT be touched
250+
func createExternal() *os.File {
251+
// This uses positional but type is external - leave untouched
252+
// (os.File doesn't actually support this, so use a keyed example)
253+
return nil
254+
}
255+
256+
// Test: already keyed literal - no change
257+
func createKeyed() *PositionalTest {
258+
return &PositionalTest{
259+
Age: 35, City: "Boston", Name: "Alice",
260+
}
261+
}
262+
221263
// Test: struct literal field reordering
222264
func createMixed() *Mixed {
223265
return &Mixed{
224266
Address: "addr", Name: "test", age: 25, count: 1,
225267
}
226268
}
227269

270+
func createPositional() *PositionalTest {
271+
return &PositionalTest{
272+
Age: 30, City: "NYC", Name: "John",
273+
}
274+
}
275+
276+
func createPositionalPartial() *PositionalTest {
277+
return &PositionalTest{
278+
Age: 25, Name: "Jane",
279+
}
280+
}
281+
282+
func createWithEmbedded() *WithEmbedded {
283+
return &WithEmbedded{
284+
PositionalTest: PositionalTest{
285+
Age: 40, City: "LA", Name: "Bob",
286+
}, Extra: "extra",
287+
}
288+
}
289+
228290
// Test: blank line before comments
229291
func functionWithComment() {
230292
x := 1

pkg/formatter/testdata/input.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"fmt"
5+
"os"
56
"strings"
67
)
78

@@ -287,3 +288,53 @@ type myPrivateType struct {
287288
func newMyPrivateType() *myPrivateType {
288289
return &myPrivateType{value: 1}
289290
}
291+
292+
// Test: positional literals should be converted to keyed
293+
type PositionalTest struct {
294+
Name string
295+
Age int
296+
City string
297+
}
298+
299+
func createPositional() *PositionalTest {
300+
return &PositionalTest{"John", 30, "NYC"}
301+
}
302+
303+
func createPositionalPartial() *PositionalTest {
304+
return &PositionalTest{"Jane", 25}
305+
}
306+
307+
// Test: anonymous struct with positional literal
308+
func createAnonymous() interface{} {
309+
return struct {
310+
B int
311+
A string
312+
}{42, "hello"}
313+
}
314+
315+
// Test: embedded fields in positional literal
316+
type WithEmbedded struct {
317+
PositionalTest
318+
Extra string
319+
}
320+
321+
func createWithEmbedded() *WithEmbedded {
322+
return &WithEmbedded{PositionalTest{"Bob", 40, "LA"}, "extra"}
323+
}
324+
325+
// Test: external struct literal should NOT be touched
326+
func createExternal() *os.File {
327+
// This uses positional but type is external - leave untouched
328+
// (os.File doesn't actually support this, so use a keyed example)
329+
return nil
330+
}
331+
332+
// Test: already keyed literal - no change
333+
func createKeyed() *PositionalTest {
334+
return &PositionalTest{Name: "Alice", Age: 35, City: "Boston"}
335+
}
336+
337+
// Test: empty literal - no change
338+
func createEmpty() *PositionalTest {
339+
return &PositionalTest{}
340+
}

0 commit comments

Comments
 (0)