Fix generated enums ordering (#4664)

* enumsort: enumsort

* enumsort: update changelog

* Add tests for enum ordering fix

Tests added:
- EnumOrderingTest: Verifies multiple enums are output in alphabetical order
- EnumElementOrderingTest: Verifies enum elements are sorted by TSName
- TSNameEnumElementOrderingTest: Verifies TSName() interface enums are also sorted

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Viktor Poletansky 2025-12-10 10:20:22 +03:00 committed by GitHub
commit 23bd70e057
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 291 additions and 5 deletions

View file

@ -123,7 +123,15 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
// if we have enums for this package, add them as well
var enums, enumsExist = b.enumsToGenerateTS[packageName]
if enumsExist {
for enumName, enum := range enums {
// Sort the enum names first to make the output deterministic
sortedEnumNames := make([]string, 0, len(enums))
for enumName := range enums {
sortedEnumNames = append(sortedEnumNames, enumName)
}
sort.Strings(sortedEnumNames)
for _, enumName := range sortedEnumNames {
enum := enums[enumName]
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
@ -172,7 +180,7 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
}
// Sort the package names first to make the output deterministic
sortedPackageNames := make([]string, 0)
sortedPackageNames := make([]string, 0, len(models))
for packageName := range models {
sortedPackageNames = append(sortedPackageNames, packageName)
}

View file

@ -0,0 +1,271 @@
package binding_test
// Test for PR #4664: Fix generated enums ordering
// This test ensures that enum output is deterministic regardless of map iteration order
// ZFirstEnum - named with Z prefix to test alphabetical sorting
type ZFirstEnum int
const (
ZFirstEnumValue1 ZFirstEnum = iota
ZFirstEnumValue2
)
var AllZFirstEnumValues = []struct {
Value ZFirstEnum
TSName string
}{
{ZFirstEnumValue1, "ZValue1"},
{ZFirstEnumValue2, "ZValue2"},
}
// ASecondEnum - named with A prefix to test alphabetical sorting
type ASecondEnum int
const (
ASecondEnumValue1 ASecondEnum = iota
ASecondEnumValue2
)
var AllASecondEnumValues = []struct {
Value ASecondEnum
TSName string
}{
{ASecondEnumValue1, "AValue1"},
{ASecondEnumValue2, "AValue2"},
}
// MMiddleEnum - named with M prefix to test alphabetical sorting
type MMiddleEnum int
const (
MMiddleEnumValue1 MMiddleEnum = iota
MMiddleEnumValue2
)
var AllMMiddleEnumValues = []struct {
Value MMiddleEnum
TSName string
}{
{MMiddleEnumValue1, "MValue1"},
{MMiddleEnumValue2, "MValue2"},
}
type EntityWithMultipleEnums struct {
Name string `json:"name"`
EnumZ ZFirstEnum `json:"enumZ"`
EnumA ASecondEnum `json:"enumA"`
EnumM MMiddleEnum `json:"enumM"`
}
func (e EntityWithMultipleEnums) Get() EntityWithMultipleEnums {
return e
}
// EnumOrderingTest tests that multiple enums in the same package are output
// in alphabetical order by enum name. Before PR #4664, the order was
// non-deterministic due to Go map iteration order.
var EnumOrderingTest = BindingTest{
name: "EnumOrderingTest",
structs: []interface{}{
&EntityWithMultipleEnums{},
},
enums: []interface{}{
// Intentionally add enums in non-alphabetical order
AllZFirstEnumValues,
AllASecondEnumValues,
AllMMiddleEnumValues,
},
exemptions: nil,
shouldError: false,
TsGenerationOptionsTest: TsGenerationOptionsTest{
TsPrefix: "",
TsSuffix: "",
},
// Expected output should have enums in alphabetical order: ASecondEnum, MMiddleEnum, ZFirstEnum
want: `export namespace binding_test {
export enum ASecondEnum {
AValue1 = 0,
AValue2 = 1,
}
export enum MMiddleEnum {
MValue1 = 0,
MValue2 = 1,
}
export enum ZFirstEnum {
ZValue1 = 0,
ZValue2 = 1,
}
export class EntityWithMultipleEnums {
name: string;
enumZ: ZFirstEnum;
enumA: ASecondEnum;
enumM: MMiddleEnum;
static createFrom(source: any = {}) {
return new EntityWithMultipleEnums(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.enumZ = source["enumZ"];
this.enumA = source["enumA"];
this.enumM = source["enumM"];
}
}
}
`,
}
// EnumElementOrderingEnum tests sorting of enum elements by TSName
type EnumElementOrderingEnum string
const (
EnumElementZ EnumElementOrderingEnum = "z_value"
EnumElementA EnumElementOrderingEnum = "a_value"
EnumElementM EnumElementOrderingEnum = "m_value"
)
// AllEnumElementOrderingValues intentionally lists values out of alphabetical order
// to test that AddEnum sorts them
var AllEnumElementOrderingValues = []struct {
Value EnumElementOrderingEnum
TSName string
}{
{EnumElementZ, "Zebra"},
{EnumElementA, "Apple"},
{EnumElementM, "Mango"},
}
type EntityWithUnorderedEnumElements struct {
Name string `json:"name"`
Enum EnumElementOrderingEnum `json:"enum"`
}
func (e EntityWithUnorderedEnumElements) Get() EntityWithUnorderedEnumElements {
return e
}
// EnumElementOrderingTest tests that enum elements are sorted alphabetically
// by their TSName within an enum. Before PR #4664, elements appeared in the
// order they were added, which could be arbitrary.
var EnumElementOrderingTest = BindingTest{
name: "EnumElementOrderingTest",
structs: []interface{}{
&EntityWithUnorderedEnumElements{},
},
enums: []interface{}{
AllEnumElementOrderingValues,
},
exemptions: nil,
shouldError: false,
TsGenerationOptionsTest: TsGenerationOptionsTest{
TsPrefix: "",
TsSuffix: "",
},
// Expected output should have enum elements sorted: Apple, Mango, Zebra
want: `export namespace binding_test {
export enum EnumElementOrderingEnum {
Apple = "a_value",
Mango = "m_value",
Zebra = "z_value",
}
export class EntityWithUnorderedEnumElements {
name: string;
enum: EnumElementOrderingEnum;
static createFrom(source: any = {}) {
return new EntityWithUnorderedEnumElements(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.enum = source["enum"];
}
}
}
`,
}
// TSNameEnumElementOrdering tests sorting with TSName() method enum
type TSNameEnumElementOrdering string
const (
TSNameEnumZ TSNameEnumElementOrdering = "z_value"
TSNameEnumA TSNameEnumElementOrdering = "a_value"
TSNameEnumM TSNameEnumElementOrdering = "m_value"
)
func (v TSNameEnumElementOrdering) TSName() string {
switch v {
case TSNameEnumZ:
return "Zebra"
case TSNameEnumA:
return "Apple"
case TSNameEnumM:
return "Mango"
default:
return "Unknown"
}
}
// AllTSNameEnumValues intentionally out of order
var AllTSNameEnumValues = []TSNameEnumElementOrdering{TSNameEnumZ, TSNameEnumA, TSNameEnumM}
type EntityWithTSNameEnumOrdering struct {
Name string `json:"name"`
Enum TSNameEnumElementOrdering `json:"enum"`
}
func (e EntityWithTSNameEnumOrdering) Get() EntityWithTSNameEnumOrdering {
return e
}
// TSNameEnumElementOrderingTest tests that enums using TSName() method
// also have their elements sorted alphabetically by the TSName.
var TSNameEnumElementOrderingTest = BindingTest{
name: "TSNameEnumElementOrderingTest",
structs: []interface{}{
&EntityWithTSNameEnumOrdering{},
},
enums: []interface{}{
AllTSNameEnumValues,
},
exemptions: nil,
shouldError: false,
TsGenerationOptionsTest: TsGenerationOptionsTest{
TsPrefix: "",
TsSuffix: "",
},
// Expected output should have enum elements sorted: Apple, Mango, Zebra
want: `export namespace binding_test {
export enum TSNameEnumElementOrdering {
Apple = "a_value",
Mango = "m_value",
Zebra = "z_value",
}
export class EntityWithTSNameEnumOrdering {
name: string;
enum: TSNameEnumElementOrdering;
static createFrom(source: any = {}) {
return new EntityWithTSNameEnumOrdering(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.enum = source["enum"];
}
}
}
`,
}

View file

@ -55,6 +55,10 @@ func TestBindings_GenerateModels(t *testing.T) {
Generics2Test,
IgnoredTest,
DeepElementsTest,
// PR #4664: Enum ordering tests
EnumOrderingTest,
EnumElementOrderingTest,
TSNameEnumElementOrderingTest,
}
testLogger := &logger.Logger{}

View file

@ -2,6 +2,7 @@ package typescriptify
import (
"bufio"
"cmp"
"fmt"
"io"
"log"
@ -9,6 +10,7 @@ import (
"path"
"reflect"
"regexp"
"slices"
"strings"
"time"
@ -372,6 +374,9 @@ func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify {
elements = append(elements, el)
}
slices.SortFunc(elements, func(a, b enumElement) int {
return cmp.Compare(a.name, b.name)
})
ty := reflect.TypeOf(elements[0].value)
t.enums[ty] = elements
t.enumTypes = append(t.enumTypes, EnumType{Type: ty})
@ -516,9 +521,6 @@ func (t TypeScriptify) ConvertToFile(fileName string, packageName string) error
if _, err := f.WriteString(converted); err != nil {
return err
}
if err != nil {
return err
}
return nil
}