Skip to content

Commit fba82e8

Browse files
committed
[FEAT] AST and Types Parsing of Enums
1 parent 3f46322 commit fba82e8

8 files changed

Lines changed: 392 additions & 41 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ go.work.sum
3333

3434
# Do not track generated example code in order to ensure tests pass.
3535
example/*_gen.go
36+
example/*_gen_test.go

enumify.go

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import (
55
"database/sql/driver"
66
"encoding/json"
77
"fmt"
8+
"go/types"
9+
"path/filepath"
10+
"strings"
11+
12+
g "github.com/dave/jennifer/jen"
13+
"golang.org/x/tools/go/packages"
814
)
915

1016
type Enum interface {
@@ -35,11 +41,112 @@ func GenerateComment() string {
3541
return fmt.Sprintf("Code generated by enumify v%s. DO NOT EDIT.", Version())
3642
}
3743

38-
func Generate(opts Options) error {
39-
fmt.Printf("%+v\n", opts)
44+
func Generate(opts Options) (err error) {
45+
f := g.NewFile(opts.Pkg)
46+
f.PackageComment(GenerateComment())
47+
48+
if _, err = opts.discover(); err != nil {
49+
return err
50+
}
51+
52+
if err = f.Save(opts.fileName()); err != nil {
53+
return err
54+
}
4055
return nil
4156
}
4257

43-
func GenerateTests(opts Options) error {
58+
func GenerateTests(opts Options) (err error) {
59+
f := g.NewFile(opts.Pkg + "_test")
60+
f.PackageComment(GenerateComment())
61+
62+
if err = f.Save(opts.testFileName()); err != nil {
63+
return err
64+
}
4465
return nil
4566
}
67+
68+
func (o Options) fileName() string {
69+
ext := filepath.Ext(o.File)
70+
return strings.TrimSuffix(filepath.Base(o.File), ext) + "_gen" + ext
71+
}
72+
73+
func (o Options) testFileName() string {
74+
ext := filepath.Ext(o.File)
75+
return strings.TrimSuffix(filepath.Base(o.File), ext) + "_gen_test" + ext
76+
}
77+
78+
func (o *Options) discover() (etypes EnumTypes, err error) {
79+
// Build tool package discovery configuration.
80+
cfg := &packages.Config{
81+
Mode: packages.NeedTypes | packages.NeedTypesInfo,
82+
}
83+
84+
// NOTE: do not use the driver query file={os.File} here because it will load the
85+
// entire package instead of just the contents of the file. As a result, the
86+
// types discovered by packages.Load will have the package "command-line-arguments"
87+
// scope rather than the scope of the package being inspected.
88+
//
89+
// We prefer this so we can isolate the specific files that have a go generate
90+
// directive and ignore the other files including other files that may also have
91+
// go generate directives.
92+
//
93+
// TODO: what if multiple enums are defined in the same file?
94+
var pkgs []*packages.Package
95+
if pkgs, err = packages.Load(cfg, o.File); err != nil {
96+
return nil, fmt.Errorf("failed to load package %q for inspection: %w", o.File, err)
97+
}
98+
99+
if len(pkgs) == 0 {
100+
return nil, fmt.Errorf("no packages found for inspection")
101+
}
102+
103+
if len(pkgs) > 1 {
104+
return nil, fmt.Errorf("multiple packages found for inspection: %v", pkgs)
105+
}
106+
107+
gopkg := pkgs[0]
108+
if len(gopkg.Errors) > 0 {
109+
return nil, fmt.Errorf("package errors: %v", pkgs[0].Errors)
110+
}
111+
112+
// Get the predeclared uint8 type for comparison
113+
uint8Type := types.Typ[types.Uint8]
114+
115+
// First pass: discover all the enum types in the package.
116+
// An enum type is a type whose underlying type is uint8.
117+
etypes = make(EnumTypes, 0, 1)
118+
scope := gopkg.Types.Scope()
119+
for _, name := range scope.Names() {
120+
obj := scope.Lookup(name)
121+
if typ, ok := obj.(*types.TypeName); ok {
122+
if types.Identical(typ.Type().Underlying(), uint8Type) {
123+
etypes = append(etypes, &EnumType{
124+
Name: name,
125+
Type: obj.Type(),
126+
gopkg: gopkg,
127+
scope: scope,
128+
})
129+
}
130+
}
131+
}
132+
133+
// Second pass: populate the enum types with consts and the name variable.
134+
for _, etype := range etypes {
135+
// Discover the consts and names variable for the enum type.
136+
etype.discover()
137+
138+
// If the names variable is not set, but one was passed in from the command
139+
// line, then attempt to set it on the enum type.
140+
if etype.NamesVar == nil && o.NameVar != "" {
141+
if err = etype.setNamesVar(o.NameVar); err != nil {
142+
return nil, fmt.Errorf("failed to set names variable for enum type %q: %w", etype.Name, err)
143+
}
144+
}
145+
146+
// The enum type must be valid before we can generate code for it.
147+
if err = etype.validate(); err != nil {
148+
return nil, err
149+
}
150+
}
151+
return etypes, nil
152+
}

example/calendar.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package example
2+
3+
// Day is an enum type that should be implemented by the enumify generator.
4+
// It uses a 1D array of strings for the names, which should be discovered by the
5+
// enumify generator due to the go:generate directive above the Day type declaration.
6+
//
7+
//go:generate go run ../cmd/enumify
8+
type Day uint8
9+
10+
// Constants for the Day enum values.
11+
// These values should be discovered by the enumify generator since they use the same
12+
// type as the Day enum, which is the type being generated.
13+
const (
14+
Unknown Day = iota
15+
Monday
16+
Tuesday
17+
Wednesday
18+
Thursday
19+
Friday
20+
Saturday
21+
Sunday
22+
)
23+
24+
// 1D array of strings for the names of the Day enum values.
25+
// This should be discovered by the enumify generator due to the go:generate directive
26+
// and because it matches the dayNames pattern to connect it with the Day enum.
27+
//
28+
//lint:ignore U1000 this is used by the enumify generator
29+
var dayNames = [...]string{
30+
"unknown",
31+
"Monday",
32+
"Tuesday",
33+
"Wednesday",
34+
"Thursday",
35+
"Friday",
36+
"Saturday",
37+
"Sunday",
38+
}
39+
40+
// This enum should be discovered by the enumify generator due to the go:generate
41+
// directive on line 7 of this file and because it matches the Enum spec pattern.
42+
type Month uint8
43+
44+
const (
45+
Monthless Month = iota
46+
January
47+
February
48+
March
49+
April
50+
May
51+
June
52+
July
53+
August
54+
September
55+
October
56+
November
57+
December
58+
)
59+
60+
// This 2D array of strings should match the names pattern for the color enum without
61+
// having to specify it using the -names flag.
62+
//
63+
//lint:ignore U1000 this is used by the enumify generator
64+
var monthNames = [2][13]string{
65+
{"", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"},
66+
{"", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},
67+
}
68+
69+
// This additional enum method should be ignored by the enumify generator.
70+
func (m Month) Abbreviation() string {
71+
if m >= Month(len(monthNames[1])) {
72+
return monthNames[1][Monthless]
73+
}
74+
return monthNames[1][m]
75+
}

example/days.go

Lines changed: 0 additions & 38 deletions
This file was deleted.

example/status.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ var statusTable = [][]string{
2828
{"unknown", "pending", "running", "failed", "success", "cancelled"},
2929
{"text-secondary", "text-info", "text-primary", "text-danger", "text-success", "text-warning"},
3030
}
31+
32+
// This is an unrelated type that should be ignored by the enumify generator.
33+
type Foo struct{}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ module go.rtnl.ai/enumify
33
go 1.26.1
44

55
require (
6+
github.com/dave/jennifer v1.7.1
67
github.com/stretchr/testify v1.11.1
78
go.rtnl.ai/x v1.15.0
9+
golang.org/x/tools v0.43.0
810
)
911

1012
require (
1113
github.com/davecgh/go-spew v1.1.1 // indirect
1214
github.com/pmezard/go-difflib v1.0.0 // indirect
15+
golang.org/x/mod v0.34.0 // indirect
16+
golang.org/x/sync v0.20.0 // indirect
1317
gopkg.in/yaml.v3 v3.0.1 // indirect
1418
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
2+
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
13
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
24
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
6+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
37
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
48
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
59
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
610
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
711
go.rtnl.ai/x v1.15.0 h1:tzMqlAXrwZ4CHNscAawlBbMjDvEwZxSu9AMxJB4CPOs=
812
go.rtnl.ai/x v1.15.0/go.mod h1:ciQ9PaXDtZDznzBrGDBV2yTElKX3aJgtQfi6V8613bo=
13+
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
14+
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
15+
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
16+
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
17+
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
18+
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
919
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1020
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1121
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

0 commit comments

Comments
 (0)