From 2cf1605c2cbfe74d0dc8a796d4999b6e72f4a857 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 16:52:42 -0500 Subject: [PATCH 01/23] feat: Support generating generics in interfaces --- scripts/apitypings/main.go | 84 ++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index c6bc015dfbd36..ca0b95a42dfee 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -39,8 +39,9 @@ func main() { // TypescriptTypes holds all the code blocks created. type TypescriptTypes struct { // Each entry is the type name, and it's typescript code block. - Types map[string]string - Enums map[string]string + Types map[string]string + Enums map[string]string + Generics map[string]string } // String just combines all the codeblocks. @@ -50,6 +51,7 @@ func (t TypescriptTypes) String() string { sortedTypes := make([]string, 0, len(t.Types)) sortedEnums := make([]string, 0, len(t.Enums)) + sortedGenerics := make([]string, 0, len(t.Generics)) for k := range t.Types { sortedTypes = append(sortedTypes, k) @@ -57,9 +59,13 @@ func (t TypescriptTypes) String() string { for k := range t.Enums { sortedEnums = append(sortedEnums, k) } + for k := range t.Generics { + sortedGenerics = append(sortedGenerics, k) + } sort.Strings(sortedTypes) sort.Strings(sortedEnums) + sort.Strings(sortedGenerics) for _, k := range sortedTypes { v := t.Types[k] @@ -73,6 +79,12 @@ func (t TypescriptTypes) String() string { _, _ = s.WriteRune('\n') } + for _, k := range sortedGenerics { + v := t.Generics[k] + _, _ = s.WriteString(v) + _, _ = s.WriteRune('\n') + } + return strings.TrimRight(s.String(), "\n") } @@ -129,6 +141,7 @@ func (g *Generator) parsePackage(ctx context.Context, patterns ...string) error // generateAll will generate for all types found in the pkg func (g *Generator) generateAll() (*TypescriptTypes, error) { structs := make(map[string]string) + generics := make(map[string]string) enums := make(map[string]types.Object) enumConsts := make(map[string][]*types.Const) @@ -170,12 +183,11 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { if !ok { panic("all typename should be named types") } - switch named.Underlying().(type) { + switch underNamed := named.Underlying().(type) { case *types.Struct: // type struct // Structs are obvious. - st, _ := obj.Type().Underlying().(*types.Struct) - codeBlock, err := g.buildStruct(obj, st) + codeBlock, err := g.buildStruct(obj, underNamed) if err != nil { return nil, xerrors.Errorf("generate %q: %w", obj.Name(), err) } @@ -205,7 +217,35 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { str.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), ts.ValueType)) structs[obj.Name()] = str.String() case *types.Array, *types.Slice: - // TODO: @emyrk if you need this, follow the same design as "*types.Map" case. + // TODO: @emyrk if you need this, follow the same design as "*types.Map" case. + case *types.Interface: + // Interfaces are used as generics. Non-generic interfaces are + // not supported. + if underNamed.NumEmbeddeds() == 1 { + union, ok := underNamed.EmbeddedType(0).(*types.Union) + if !ok { + // If the underlying is not a union, but has 1 type. It's + // just that one type. + union = types.NewUnion([]*types.Term{ + // Set the tilde to true to support underlying. + // Doesn't actually affect our generation. + types.NewTerm(true, underNamed.EmbeddedType(0)), + }) + } + + block, err := g.buildUnion(obj, union) + if err != nil { + return nil, xerrors.Errorf("generate union %q: %w", obj.Name(), err) + } + generics[obj.Name()] = block + } + case *types.Signature: + // Ignore named functions. + default: + // If you hit this error, you added a new unsupported named type. + // The easiest way to solve this is add a new case above with + // your type and a TODO to implement it. + return nil, xerrors.Errorf("unsupported named type %q", underNamed.String()) } case *types.Var: // TODO: Are any enums var declarations? This is also codersdk.Me. @@ -242,8 +282,9 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { } return &TypescriptTypes{ - Types: structs, - Enums: enumCodeBlocks, + Types: structs, + Enums: enumCodeBlocks, + Generics: generics, }, nil } @@ -252,6 +293,33 @@ func (g *Generator) posLine(obj types.Object) string { return fmt.Sprintf("// From %s\n", filepath.Join("codersdk", filepath.Base(file.Name()))) } +// buildStruct just prints the typescript def for a type. +func (g *Generator) buildUnion(obj types.Object, st *types.Union) (string, error) { + var s strings.Builder + _, _ = s.WriteString(g.posLine(obj)) + + allTypes := make([]string, 0, st.Len()) + var optional bool + for i := 0; i < st.Len(); i++ { + term := st.Term(i) + scriptType, err := g.typescriptType(term.Type()) + if err != nil { + return "", xerrors.Errorf("union %q for %q failed to get type: %w", st.String(), obj.Name(), err) + } + allTypes = append(allTypes, scriptType.ValueType) + optional = optional || scriptType.Optional + } + + qMark := "" + if optional { + qMark = "?" + } + + s.WriteString(fmt.Sprintf("export type %s%s = %s\n", obj.Name(), qMark, strings.Join(allTypes, " | "))) + + return s.String(), nil +} + // buildStruct just prints the typescript def for a type. func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, error) { var s strings.Builder From 0ecb427f9175fed92fa823d7abeb6a9b382ccd4c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 17:22:48 -0500 Subject: [PATCH 02/23] Switch struct to a template --- scripts/apitypings/main.go | 70 ++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index ca0b95a42dfee..10192c66573a4 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "fmt" "go/types" @@ -10,6 +11,7 @@ import ( "regexp" "sort" "strings" + "text/template" "github.com/fatih/structtag" "golang.org/x/tools/go/packages" @@ -320,11 +322,33 @@ func (g *Generator) buildUnion(obj types.Object, st *types.Union) (string, error return s.String(), nil } +type structTemplateState struct { + PosLine string + Name string + Fields []string + Extends string + AboveLine string +} + +const structTemplate = `{{ .PosLine -}} +{{ if .AboveLine }}{{ .AboveLine }} +{{ end }}export interface {{ .Name }}{{ if .Extends }} extends {{ .Extends }}{{ end }}{ +{{- range .Fields }} +{{ . -}} +{{- end }} +} +` + // buildStruct just prints the typescript def for a type. func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, error) { - var s strings.Builder - _, _ = s.WriteString(g.posLine(obj)) - _, _ = s.WriteString(fmt.Sprintf("export interface %s ", obj.Name())) + state := structTemplateState{} + tpl, err := template.New("struct").Parse(structTemplate) + if err != nil { + return "", xerrors.Errorf("parse struct template: %w", err) + } + + state.PosLine = g.posLine(obj) + state.Name = obj.Name() // Handle named embedded structs in the codersdk package via extension. var extends []string @@ -340,10 +364,9 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err } } if len(extends) > 0 { - _, _ = s.WriteString(fmt.Sprintf("extends %s ", strings.Join(extends, ", "))) + state.Extends = strings.Join(extends, ", ") } - _, _ = s.WriteString("{\n") // For each field in the struct, we print 1 line of the typescript interface for i := 0; i < st.NumFields(); i++ { if extendedFields[i] { @@ -399,21 +422,29 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err } if tsType.AboveTypeLine != "" { - _, _ = s.WriteString(tsType.AboveTypeLine) - _, _ = s.WriteRune('\n') + state.AboveLine = tsType.AboveTypeLine } optional := "" if jsonOptional || tsType.Optional { optional = "?" } - _, _ = s.WriteString(fmt.Sprintf("%sreadonly %s%s: %s\n", indent, jsonName, optional, tsType.ValueType)) + state.Fields = append(state.Fields, fmt.Sprintf("%sreadonly %s%s: %s", indent, jsonName, optional, tsType.ValueType)) } - _, _ = s.WriteString("}\n") - return s.String(), nil + + data := bytes.NewBuffer(make([]byte, 0)) + err = tpl.Execute(data, state) + if err != nil { + return "", xerrors.Errorf("execute struct template: %w", err) + } + return data.String(), nil } type TypescriptType struct { - ValueType string + // GenericMapping gives a unique character for mapping the value type + // to a generic. This is only useful if you can use generic syntax. + // This is optional, as the ValueType will have the correct constraints. + GenericMapping string + ValueType string // AboveTypeLine lets you put whatever text you want above the typescript // type line. AboveTypeLine string @@ -562,6 +593,23 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { AboveTypeLine: indentedComment("eslint-disable-next-line")}, nil } return TypescriptType{}, xerrors.New("only empty interface types are supported") + case *types.TypeParam: + _, ok := ty.Underlying().(*types.Interface) + if !ok { + // If it's not an interface, it is likely a usage of generics that + // we have not hit yet. Feel free to add support for it. + return TypescriptType{}, xerrors.New("type param must be an interface") + } + + generic := ty.Constraint() + // This is kinda a hack, but we want just the end of the name. + name := strings.TrimSuffix("github.com/coder/coder/codersdk.", generic.String()) + return TypescriptType{ + GenericMapping: ty.Obj().Name(), + ValueType: name, + AboveTypeLine: "", + Optional: false, + }, nil } // These are all the other types we need to support. From 6163ace2431ddb9ae0f9c468fed94b74a3d431f3 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 17:29:56 -0500 Subject: [PATCH 03/23] Support generics in apitypings --- scripts/apitypings/main.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 10192c66573a4..55cf07bce939b 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -326,23 +326,26 @@ type structTemplateState struct { PosLine string Name string Fields []string + Generics []string Extends string AboveLine string } const structTemplate = `{{ .PosLine -}} {{ if .AboveLine }}{{ .AboveLine }} -{{ end }}export interface {{ .Name }}{{ if .Extends }} extends {{ .Extends }}{{ end }}{ -{{- range .Fields }} -{{ . -}} -{{- end }} +{{ end }}export interface {{ .Name }}{{ if .Generics }}<{{ join .Generics ", " }}>{{ end }}{{ if .Extends }} extends {{ .Extends }}{{ end }} { +{{ join .Fields "\n"}} } ` // buildStruct just prints the typescript def for a type. func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, error) { state := structTemplateState{} - tpl, err := template.New("struct").Parse(structTemplate) + tpl := template.New("struct") + tpl.Funcs(template.FuncMap{ + "join": strings.Join, + }) + tpl, err := tpl.Parse(structTemplate) if err != nil { return "", xerrors.Errorf("parse struct template: %w", err) } @@ -428,7 +431,12 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err if jsonOptional || tsType.Optional { optional = "?" } - state.Fields = append(state.Fields, fmt.Sprintf("%sreadonly %s%s: %s", indent, jsonName, optional, tsType.ValueType)) + valueType := tsType.ValueType + if tsType.GenericMapping != "" { + valueType = tsType.GenericMapping + state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", tsType.GenericMapping, tsType.ValueType)) + } + state.Fields = append(state.Fields, fmt.Sprintf("%sreadonly %s%s: %s", indent, jsonName, optional, valueType)) } data := bytes.NewBuffer(make([]byte, 0)) @@ -603,7 +611,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { generic := ty.Constraint() // This is kinda a hack, but we want just the end of the name. - name := strings.TrimSuffix("github.com/coder/coder/codersdk.", generic.String()) + name := strings.TrimPrefix(generic.String(), "github.com/coder/coder/codersdk.") return TypescriptType{ GenericMapping: ty.Obj().Name(), ValueType: name, From 7c94c8ba4655ef950c2ceb18bca7a010d9760b3a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 17:37:43 -0500 Subject: [PATCH 04/23] Ignore generics already declared --- scripts/apitypings/main.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 55cf07bce939b..b25617e2754ae 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -370,6 +370,7 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err state.Extends = strings.Join(extends, ", ") } + genericsUsed := make(map[string]string) // For each field in the struct, we print 1 line of the typescript interface for i := 0; i < st.NumFields(); i++ { if extendedFields[i] { @@ -434,7 +435,15 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err valueType := tsType.ValueType if tsType.GenericMapping != "" { valueType = tsType.GenericMapping - state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", tsType.GenericMapping, tsType.ValueType)) + // Don't add a generic twice + if _, ok := genericsUsed[tsType.GenericMapping]; !ok { + // TODO: We should probably check that the generic mapping is + // not a different type. Like 'T' being referenced to 2 different + // constraints. I don't think this is possible though in valid + // go, so I'm going to ignore this for now. + state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", tsType.GenericMapping, tsType.ValueType)) + } + genericsUsed[tsType.GenericMapping] = tsType.ValueType } state.Fields = append(state.Fields, fmt.Sprintf("%sreadonly %s%s: %s", indent, jsonName, optional, valueType)) } From daa5eec261c6c926f7e17d08cf9f97d6b92ce4f6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 17:39:43 -0500 Subject: [PATCH 05/23] Do not add ? to export type --- scripts/apitypings/main.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index b25617e2754ae..84f173b0c449b 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -312,12 +312,11 @@ func (g *Generator) buildUnion(obj types.Object, st *types.Union) (string, error optional = optional || scriptType.Optional } - qMark := "" if optional { - qMark = "?" + allTypes = append(allTypes, "null") } - s.WriteString(fmt.Sprintf("export type %s%s = %s\n", obj.Name(), qMark, strings.Join(allTypes, " | "))) + s.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), strings.Join(allTypes, " | "))) return s.String(), nil } From 85e8b63238d734e729b2f8c3527a9f24ce21ac9f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 17:47:51 -0500 Subject: [PATCH 06/23] Remove duplicate types --- coderd/util/slice/slice.go | 16 ++++++++++++++++ coderd/util/slice/slice_test.go | 16 ++++++++++++++++ scripts/apitypings/main.go | 4 ++++ 3 files changed, 36 insertions(+) diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 2cc6c2a4bb0d5..eeda87591e180 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -22,6 +22,22 @@ func Overlap[T comparable](a []T, b []T) bool { }) } +// Unique returns a new slice with all duplicate elements removed. +// This is a slow function on large lists. +// TODO: Sort elements and implement a faster search algorithm if we +// +// really start to use this. +func Unique[T comparable](a []T) []T { + cpy := make([]T, 0, len(a)) + for _, v := range a { + v := v + if !Contains(cpy, v) { + cpy = append(cpy, v) + } + } + return cpy +} + func OverlapCompare[T any](a []T, b []T, equal func(a, b T) bool) bool { // For each element in b, if at least 1 is contained in 'a', // return true. diff --git a/coderd/util/slice/slice_test.go b/coderd/util/slice/slice_test.go index d69b6c9c440ed..103b9603a272a 100644 --- a/coderd/util/slice/slice_test.go +++ b/coderd/util/slice/slice_test.go @@ -9,6 +9,22 @@ import ( "github.com/coder/coder/coderd/util/slice" ) +func TestUnique(t *testing.T) { + t.Parallel() + + require.ElementsMatch(t, + []int{1, 2, 3, 4, 5}, + slice.Unique([]int{ + 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, + })) + + require.ElementsMatch(t, + []string{"a"}, + slice.Unique([]string{ + "a", "a", "a", + })) +} + func TestContains(t *testing.T) { t.Parallel() diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 84f173b0c449b..84c39683d63b6 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -13,6 +13,8 @@ import ( "strings" "text/template" + "github.com/coder/coder/coderd/util/slice" + "github.com/fatih/structtag" "golang.org/x/tools/go/packages" "golang.org/x/xerrors" @@ -316,6 +318,8 @@ func (g *Generator) buildUnion(obj types.Object, st *types.Union) (string, error allTypes = append(allTypes, "null") } + allTypes = slice.Unique(allTypes) + s.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), strings.Join(allTypes, " | "))) return s.String(), nil From 1b70b9b43378b4bde5caa924dbfc5fb325e35f28 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 21:20:04 -0500 Subject: [PATCH 07/23] Add generic support to instantiating generics --- scripts/apitypings/main.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 84c39683d63b6..26b0b0b569267 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -578,6 +578,17 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { if obj := g.pkg.Types.Scope().Lookup(name); obj != nil { // Sweet! Using other typescript types as fields. This could be an // enum or another struct + if args := n.TypeArgs(); args != nil && args.Len() > 0 { + genericArgs := make([]string, 0, args.Len()) + for i := 0; i < args.Len(); i++ { + genType, err := g.typescriptType(args.At(i)) + if err != nil { + return TypescriptType{}, xerrors.Errorf("generic field %q<%q>: %w", name, args.At(i).String(), err) + } + genericArgs = append(genericArgs, genType.ValueType) + } + name += fmt.Sprintf("<%s>", strings.Join(genericArgs, ", ")) + } return TypescriptType{ValueType: name}, nil } From 3cba866d58bebdeba5921b66306aa108766f09b7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 23:15:20 -0500 Subject: [PATCH 08/23] fix: Add better generic bubbling up support Add unit test --- scripts/apitypings/main.go | 371 ++++++++++++------ scripts/apitypings/main_test.go | 35 ++ .../apitypings/testdata/generics/generics.go | 30 ++ .../apitypings/testdata/generics/generics.ts | 28 ++ 4 files changed, 337 insertions(+), 127 deletions(-) create mode 100644 scripts/apitypings/main_test.go create mode 100644 scripts/apitypings/testdata/generics/generics.go create mode 100644 scripts/apitypings/testdata/generics/generics.ts diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 26b0b0b569267..200c6f43e8fcb 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -31,13 +31,25 @@ const ( func main() { ctx := context.Background() log := slog.Make(sloghuman.Sink(os.Stderr)) - codeBlocks, err := GenerateFromDirectory(ctx, log, baseDir) + output, err := Generate(baseDir) if err != nil { log.Fatal(ctx, err.Error()) } // Just cat the output to a file to capture it - _, _ = fmt.Println(codeBlocks.String()) + fmt.Println(output) +} + +func Generate(directory string) (string, error) { + ctx := context.Background() + log := slog.Make(sloghuman.Sink(os.Stderr)) + codeBlocks, err := GenerateFromDirectory(ctx, log, directory) + if err != nil { + return "", err + } + + // Just cat the output to a file to capture it + return codeBlocks.String(), nil } // TypescriptTypes holds all the code blocks created. @@ -95,7 +107,8 @@ func (t TypescriptTypes) String() string { // GenerateFromDirectory will return all the typescript code blocks for a directory func GenerateFromDirectory(ctx context.Context, log slog.Logger, directory string) (*TypescriptTypes, error) { g := Generator{ - log: log, + log: log, + builtins: make(map[string]string), } err := g.parsePackage(ctx, directory) if err != nil { @@ -114,6 +127,15 @@ type Generator struct { // Package we are scanning. pkg *packages.Package log slog.Logger + + // builtins is kinda a hack to get around the fact that using builtin + // generic constraints is common. We want to support them even though + // they are external to our package. + // It is also a string because the builtins are not proper go types. Meaning + // if you inspect the types, they are not "correct". Things like "comparable" + // cannot be implemented in go. So they are a first class thing that we just + // have to make a static string for ¯\_(ツ)_/¯ + builtins map[string]string } // parsePackage takes a list of patterns such as a directory, and parses them. @@ -144,13 +166,15 @@ func (g *Generator) parsePackage(ctx context.Context, patterns ...string) error // generateAll will generate for all types found in the pkg func (g *Generator) generateAll() (*TypescriptTypes, error) { - structs := make(map[string]string) - generics := make(map[string]string) - enums := make(map[string]types.Object) - enumConsts := make(map[string][]*types.Const) + m := &Maps{ + Structs: make(map[string]string), + Generics: make(map[string]string), + Enums: make(map[string]types.Object), + EnumConsts: make(map[string][]*types.Const), + IgnoredTypes: make(map[string]struct{}), + } // Look for comments that indicate to ignore a type for typescript generation. - ignoredTypes := make(map[string]struct{}) ignoreRegex := regexp.MustCompile("@typescript-ignore[:]?(?P.*)") for _, file := range g.pkg.Syntax { for _, comment := range file.Comments { @@ -161,7 +185,7 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { if len(matches) >= ignored && matches[ignored] != "" { arr := strings.Split(matches[ignored], ",") for _, s := range arr { - ignoredTypes[strings.TrimSpace(s)] = struct{}{} + m.IgnoredTypes[strings.TrimSpace(s)] = struct{}{} } } } @@ -170,107 +194,24 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { for _, n := range g.pkg.Types.Scope().Names() { obj := g.pkg.Types.Scope().Lookup(n) - if obj == nil || obj.Type() == nil { - // This would be weird, but it is if the package does not have the type def. - continue - } - - // Exclude ignored types - if _, ok := ignoredTypes[obj.Name()]; ok { - continue + err := g.generateOne(m, obj) + if err != nil { + return nil, xerrors.Errorf("%q: %w", n, err) } + } - switch obj := obj.(type) { - // All named types are type declarations - case *types.TypeName: - named, ok := obj.Type().(*types.Named) - if !ok { - panic("all typename should be named types") - } - switch underNamed := named.Underlying().(type) { - case *types.Struct: - // type struct - // Structs are obvious. - codeBlock, err := g.buildStruct(obj, underNamed) - if err != nil { - return nil, xerrors.Errorf("generate %q: %w", obj.Name(), err) - } - structs[obj.Name()] = codeBlock - case *types.Basic: - // type string - // These are enums. Store to expand later. - enums[obj.Name()] = obj - case *types.Map: - // Declared maps that are not structs are still valid codersdk objects. - // Handle them custom by calling 'typescriptType' directly instead of - // iterating through each struct field. - // These types support no json/typescript tags. - // These are **NOT** enums, as a map in Go would never be used for an enum. - ts, err := g.typescriptType(obj.Type().Underlying()) - if err != nil { - return nil, xerrors.Errorf("(map) generate %q: %w", obj.Name(), err) - } - - var str strings.Builder - _, _ = str.WriteString(g.posLine(obj)) - if ts.AboveTypeLine != "" { - str.WriteString(ts.AboveTypeLine) - str.WriteRune('\n') - } - // Use similar output syntax to enums. - str.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), ts.ValueType)) - structs[obj.Name()] = str.String() - case *types.Array, *types.Slice: - // TODO: @emyrk if you need this, follow the same design as "*types.Map" case. - case *types.Interface: - // Interfaces are used as generics. Non-generic interfaces are - // not supported. - if underNamed.NumEmbeddeds() == 1 { - union, ok := underNamed.EmbeddedType(0).(*types.Union) - if !ok { - // If the underlying is not a union, but has 1 type. It's - // just that one type. - union = types.NewUnion([]*types.Term{ - // Set the tilde to true to support underlying. - // Doesn't actually affect our generation. - types.NewTerm(true, underNamed.EmbeddedType(0)), - }) - } - - block, err := g.buildUnion(obj, union) - if err != nil { - return nil, xerrors.Errorf("generate union %q: %w", obj.Name(), err) - } - generics[obj.Name()] = block - } - case *types.Signature: - // Ignore named functions. - default: - // If you hit this error, you added a new unsupported named type. - // The easiest way to solve this is add a new case above with - // your type and a TODO to implement it. - return nil, xerrors.Errorf("unsupported named type %q", underNamed.String()) - } - case *types.Var: - // TODO: Are any enums var declarations? This is also codersdk.Me. - case *types.Const: - // We only care about named constant types, since they are enums - if named, ok := obj.Type().(*types.Named); ok { - name := named.Obj().Name() - enumConsts[name] = append(enumConsts[name], obj) - } - case *types.Func: - // Noop - default: - fmt.Println(obj.Name()) + // Add the builtins + for n, value := range g.builtins { + if value != "" { + m.Generics[n] = value } } // Write all enums enumCodeBlocks := make(map[string]string) - for name, v := range enums { + for name, v := range m.Enums { var values []string - for _, elem := range enumConsts[name] { + for _, elem := range m.EnumConsts[name] { // TODO: If we have non string constants, we need to handle that // here. values = append(values, elem.Val().String()) @@ -286,12 +227,118 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { } return &TypescriptTypes{ - Types: structs, + Types: m.Structs, Enums: enumCodeBlocks, - Generics: generics, + Generics: m.Generics, }, nil } +type Maps struct { + Structs map[string]string + Generics map[string]string + Enums map[string]types.Object + EnumConsts map[string][]*types.Const + IgnoredTypes map[string]struct{} +} + +func (g *Generator) generateOne(m *Maps, obj types.Object) error { + if obj == nil || obj.Type() == nil { + // This would be weird, but it is if the package does not have the type def. + return nil + } + + // Exclude ignored types + if _, ok := m.IgnoredTypes[obj.Name()]; ok { + return nil + } + + switch obj := obj.(type) { + // All named types are type declarations + case *types.TypeName: + named, ok := obj.Type().(*types.Named) + if !ok { + panic("all typename should be named types") + } + switch underNamed := named.Underlying().(type) { + case *types.Struct: + // type struct + // Structs are obvious. + codeBlock, err := g.buildStruct(obj, underNamed) + if err != nil { + return xerrors.Errorf("generate %q: %w", obj.Name(), err) + } + m.Structs[obj.Name()] = codeBlock + case *types.Basic: + // type string + // These are enums. Store to expand later. + m.Enums[obj.Name()] = obj + case *types.Map: + // Declared maps that are not structs are still valid codersdk objects. + // Handle them custom by calling 'typescriptType' directly instead of + // iterating through each struct field. + // These types support no json/typescript tags. + // These are **NOT** enums, as a map in Go would never be used for an enum. + ts, err := g.typescriptType(obj.Type().Underlying()) + if err != nil { + return xerrors.Errorf("(map) generate %q: %w", obj.Name(), err) + } + + var str strings.Builder + _, _ = str.WriteString(g.posLine(obj)) + if ts.AboveTypeLine != "" { + str.WriteString(ts.AboveTypeLine) + str.WriteRune('\n') + } + // Use similar output syntax to enums. + str.WriteString(fmt.Sprintf("export type %s = %s\n", obj.Name(), ts.ValueType)) + m.Structs[obj.Name()] = str.String() + case *types.Array, *types.Slice: + // TODO: @emyrk if you need this, follow the same design as "*types.Map" case. + case *types.Interface: + // Interfaces are used as generics. Non-generic interfaces are + // not supported. + if underNamed.NumEmbeddeds() == 1 { + union, ok := underNamed.EmbeddedType(0).(*types.Union) + if !ok { + // If the underlying is not a union, but has 1 type. It's + // just that one type. + union = types.NewUnion([]*types.Term{ + // Set the tilde to true to support underlying. + // Doesn't actually affect our generation. + types.NewTerm(true, underNamed.EmbeddedType(0)), + }) + } + + block, err := g.buildUnion(obj, union) + if err != nil { + return xerrors.Errorf("generate union %q: %w", obj.Name(), err) + } + m.Generics[obj.Name()] = block + } + case *types.Signature: + // Ignore named functions. + default: + // If you hit this error, you added a new unsupported named type. + // The easiest way to solve this is add a new case above with + // your type and a TODO to implement it. + return xerrors.Errorf("unsupported named type %q", underNamed.String()) + } + case *types.Var: + // TODO: Are any enums var declarations? This is also codersdk.Me. + case *types.Const: + // We only care about named constant types, since they are enums + if named, ok := obj.Type().(*types.Named); ok { + name := named.Obj().Name() + m.EnumConsts[name] = append(m.EnumConsts[name], obj) + } + case *types.Func: + // Noop + default: + fmt.Println(obj.Name()) + } + return nil +} + func (g *Generator) posLine(obj types.Object) string { file := g.pkg.Fset.File(obj.Pos()) return fmt.Sprintf("// From %s\n", filepath.Join("codersdk", filepath.Base(file.Name()))) @@ -436,17 +483,20 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err optional = "?" } valueType := tsType.ValueType - if tsType.GenericMapping != "" { - valueType = tsType.GenericMapping - // Don't add a generic twice - if _, ok := genericsUsed[tsType.GenericMapping]; !ok { - // TODO: We should probably check that the generic mapping is - // not a different type. Like 'T' being referenced to 2 different - // constraints. I don't think this is possible though in valid - // go, so I'm going to ignore this for now. - state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", tsType.GenericMapping, tsType.ValueType)) + if tsType.GenericValue != "" { + valueType = tsType.GenericValue + for name, constraint := range tsType.GenericTypes { + if _, ok := genericsUsed[name]; ok { + // Don't add a generic twice + // TODO: We should probably check that the generic mapping is + // not a different type. Like 'T' being referenced to 2 different + // constraints. I don't think this is possible though in valid + // go, so I'm going to ignore this for now. + continue + } + state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", name, constraint)) + genericsUsed[name] = constraint } - genericsUsed[tsType.GenericMapping] = tsType.ValueType } state.Fields = append(state.Fields, fmt.Sprintf("%sreadonly %s%s: %s", indent, jsonName, optional, valueType)) } @@ -460,11 +510,14 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err } type TypescriptType struct { - // GenericMapping gives a unique character for mapping the value type + // GenericValue gives a unique character for mapping the value type // to a generic. This is only useful if you can use generic syntax. // This is optional, as the ValueType will have the correct constraints. - GenericMapping string - ValueType string + GenericValue string + // GenericTypes is a map of generic name to actual constraint + // Example: 'C = comparable'. + GenericTypes map[string]string + ValueType string // AboveTypeLine lets you put whatever text you want above the typescript // type line. AboveTypeLine string @@ -575,21 +628,40 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // put the name as it will be defined in the typescript codeblock // we generate. name := n.Obj().Name() + genericName := "" + genericTypes := make(map[string]string) if obj := g.pkg.Types.Scope().Lookup(name); obj != nil { // Sweet! Using other typescript types as fields. This could be an // enum or another struct if args := n.TypeArgs(); args != nil && args.Len() > 0 { - genericArgs := make([]string, 0, args.Len()) + genericConstraints := make([]string, 0, args.Len()) + genericNames := make([]string, 0, args.Len()) for i := 0; i < args.Len(); i++ { genType, err := g.typescriptType(args.At(i)) if err != nil { return TypescriptType{}, xerrors.Errorf("generic field %q<%q>: %w", name, args.At(i).String(), err) } - genericArgs = append(genericArgs, genType.ValueType) + + if param, ok := args.At(i).(*types.TypeParam); ok { + // Using a generic defined by the parent. + gname := param.Obj().Name() + genericNames = append(genericNames, gname) + genericTypes[gname] = genType.ValueType + } else { + // Defining a generic + genericNames = append(genericNames, genType.ValueType) + } + + genericConstraints = append(genericConstraints, genType.ValueType) } - name += fmt.Sprintf("<%s>", strings.Join(genericArgs, ", ")) + genericName = name + fmt.Sprintf("<%s>", strings.Join(genericNames, ", ")) + name += fmt.Sprintf("<%s>", strings.Join(genericConstraints, ", ")) } - return TypescriptType{ValueType: name}, nil + return TypescriptType{ + GenericTypes: genericTypes, + GenericValue: genericName, + ValueType: name, + }, nil } // If it's a struct, just use the name of the struct type @@ -633,13 +705,40 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { } generic := ty.Constraint() - // This is kinda a hack, but we want just the end of the name. - name := strings.TrimPrefix(generic.String(), "github.com/coder/coder/codersdk.") + // We don't mess with multiple packages, so just trim the package path + // from the name. + pkgPath := ty.Obj().Pkg().Path() + name := strings.TrimPrefix(generic.String(), pkgPath+".") + + referenced := g.pkg.Types.Scope().Lookup(name) + + if referenced == nil { + include, builtinString := g.isBuiltIn(name) + if !include { + // If we don't have the type constraint defined somewhere in the package, + // then we have to resort to using any. + return TypescriptType{ + GenericTypes: map[string]string{ + ty.Obj().Name(): "any", + }, + GenericValue: ty.Obj().Name(), + ValueType: "any", + AboveTypeLine: fmt.Sprintf("// %q is an external type, so we use any", name), + Optional: false, + }, nil + } + // Include the builtin for this type to reference + g.builtins[name] = builtinString + } + return TypescriptType{ - GenericMapping: ty.Obj().Name(), - ValueType: name, - AboveTypeLine: "", - Optional: false, + GenericTypes: map[string]string{ + ty.Obj().Name(): name, + }, + GenericValue: ty.Obj().Name(), + ValueType: name, + AboveTypeLine: "", + Optional: false, }, nil } @@ -648,6 +747,24 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { return TypescriptType{}, xerrors.Errorf("unknown type: %s", ty.String()) } +// isBuiltIn returns the string for a builtin type that we want to support +// if the name is a reserved builtin type. This is for types like 'comparable'. +// These types are not implemented in golang, so we just have to hardcode it. +func (g *Generator) isBuiltIn(name string) (bool, string) { + // Note: @emyrk If we use constraints like Ordered, we can pull those + // dynamically from their respective packages. + switch name { + case "comparable": + // To be complete, we include "any". Kinda sucks :( + return true, "export type comparable = boolean | number | string | any" + case "any": + // This is supported in typescript, we don't need to write anything + return true, "" + default: + return false, "" + } +} + func indentedComment(comment string) string { return fmt.Sprintf("%s// %s", indent, comment) } diff --git a/scripts/apitypings/main_test.go b/scripts/apitypings/main_test.go new file mode 100644 index 0000000000000..c7fabede3c4e8 --- /dev/null +++ b/scripts/apitypings/main_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGeneration(t *testing.T) { + files, err := os.ReadDir("testdata") + require.NoError(t, err, "read dir") + + for _, f := range files { + if !f.IsDir() { + // Only test directories + continue + } + f := f + t.Run(f.Name(), func(t *testing.T) { + dir := filepath.Join(".", "testdata", f.Name()) + output, err := Generate("./" + dir) + require.NoErrorf(t, err, "generate %q", dir) + + golden := filepath.Join(dir, f.Name()+".ts") + expected, err := os.ReadFile(golden) + require.NoErrorf(t, err, "read file %s", golden) + expectedString := strings.TrimSpace(string(expected)) + output = strings.TrimSpace(output) + require.Equal(t, expectedString, output, "matched output") + }) + } +} diff --git a/scripts/apitypings/testdata/generics/generics.go b/scripts/apitypings/testdata/generics/generics.go new file mode 100644 index 0000000000000..0bed9ed059928 --- /dev/null +++ b/scripts/apitypings/testdata/generics/generics.go @@ -0,0 +1,30 @@ +package generics + +import "time" + +type Single interface { + string +} + +type Custom interface { + string | bool | int | time.Duration | []string | *int +} + +// StaticGeneric has all generic fields defined in the field +type StaticGeneric struct { + Static GenericFields[string, int, time.Duration] `json:"static"` +} + +// DynamicGeneric can has some dynamic fields +type DynamicGeneric[C comparable, A any] struct { + Dynamic GenericFields[C, A, string] `json:"dynamic"` + Comparable C `json:"comparable"` +} + +type GenericFields[C comparable, A any, T Custom] struct { + Comparable C `json:"comparable"` + Any A `json:"any"` + + Custom T `json:"custom"` + Again T `json:"again"` +} diff --git a/scripts/apitypings/testdata/generics/generics.ts b/scripts/apitypings/testdata/generics/generics.ts new file mode 100644 index 0000000000000..8896afb764c62 --- /dev/null +++ b/scripts/apitypings/testdata/generics/generics.ts @@ -0,0 +1,28 @@ +// Code generated by 'make coder/scripts/apitypings/main.go'. DO NOT EDIT. + +// From codersdk/generics.go +export interface DynamicGeneric { + readonly dynamic: GenericFields + readonly comparable: C +} + +// From codersdk/generics.go +export interface GenericFields { + readonly comparable: C + readonly any: A + readonly custom: T + readonly again: T +} + +// From codersdk/generics.go +export interface StaticGeneric { + readonly static: GenericFields +} + +// From codersdk/generics.go +export type Custom = string | boolean | number | string[] | null + +// From codersdk/generics.go +export type Single = string + +export type comparable = boolean | number | string | any From fd7587262a05911740a19bf08cc2420386792232 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 23:26:28 -0500 Subject: [PATCH 09/23] fix: Handle comments better --- scripts/apitypings/main.go | 8 ++++--- site/src/api/typesGenerated.ts | 41 +++++----------------------------- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 200c6f43e8fcb..12d693ce74bf7 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -475,9 +475,6 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err } } - if tsType.AboveTypeLine != "" { - state.AboveLine = tsType.AboveTypeLine - } optional := "" if jsonOptional || tsType.Optional { optional = "?" @@ -498,6 +495,11 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err genericsUsed[name] = constraint } } + + if tsType.AboveTypeLine != "" { + // Just append these as fields. We should fix this later. + state.Fields = append(state.Fields, tsType.AboveTypeLine) + } state.Fields = append(state.Fields, fmt.Sprintf("%sreadonly %s%s: %s", indent, jsonName, optional, valueType)) } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5b3c44bd0b979..ad0e18aaecd94 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -907,10 +907,7 @@ export type LogSource = "provisioner" | "provisioner_daemon" export type LoginType = "github" | "oidc" | "password" | "token" // From codersdk/parameters.go -export type ParameterDestinationScheme = - | "environment_variable" - | "none" - | "provisioner_variable" +export type ParameterDestinationScheme = "environment_variable" | "none" | "provisioner_variable" // From codersdk/parameters.go export type ParameterScope = "import_job" | "template" | "workspace" @@ -922,13 +919,7 @@ export type ParameterSourceScheme = "data" | "none" export type ParameterTypeSystem = "hcl" | "none" // From codersdk/provisionerdaemons.go -export type ProvisionerJobStatus = - | "canceled" - | "canceling" - | "failed" - | "pending" - | "running" - | "succeeded" +export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pending" | "running" | "succeeded" // From codersdk/organizations.go export type ProvisionerStorageMethod = "file" @@ -937,15 +928,7 @@ export type ProvisionerStorageMethod = "file" export type ProvisionerType = "echo" | "terraform" // From codersdk/audit.go -export type ResourceType = - | "api_key" - | "git_ssh_key" - | "group" - | "organization" - | "template" - | "template_version" - | "user" - | "workspace" +export type ResourceType = "api_key" | "git_ssh_key" | "group" | "organization" | "template" | "template_version" | "user" | "workspace" // From codersdk/sse.go export type ServerSentEventType = "data" | "error" | "ping" @@ -960,27 +943,13 @@ export type UserStatus = "active" | "suspended" export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected" // From codersdk/workspaceapps.go -export type WorkspaceAppHealth = - | "disabled" - | "healthy" - | "initializing" - | "unhealthy" +export type WorkspaceAppHealth = "disabled" | "healthy" | "initializing" | "unhealthy" // From codersdk/workspaceapps.go export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" // From codersdk/workspacebuilds.go -export type WorkspaceStatus = - | "canceled" - | "canceling" - | "deleted" - | "deleting" - | "failed" - | "pending" - | "running" - | "starting" - | "stopped" - | "stopping" +export type WorkspaceStatus = "canceled" | "canceling" | "deleted" | "deleting" | "failed" | "pending" | "running" | "starting" | "stopped" | "stopping" // From codersdk/workspacebuilds.go export type WorkspaceTransition = "delete" | "start" | "stop" From 1510183ebdd4bdf75f9350f0e20264c4e562c8ae Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 23:31:12 -0500 Subject: [PATCH 10/23] Add some more to test --- coderd/util/slice/slice.go | 3 +-- scripts/apitypings/testdata/generics/generics.go | 15 ++++++++------- scripts/apitypings/testdata/generics/generics.ts | 9 +++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index eeda87591e180..38c6592856a34 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -25,8 +25,7 @@ func Overlap[T comparable](a []T, b []T) bool { // Unique returns a new slice with all duplicate elements removed. // This is a slow function on large lists. // TODO: Sort elements and implement a faster search algorithm if we -// -// really start to use this. +// really start to use this. func Unique[T comparable](a []T) []T { cpy := make([]T, 0, len(a)) for _, v := range a { diff --git a/scripts/apitypings/testdata/generics/generics.go b/scripts/apitypings/testdata/generics/generics.go index 0bed9ed059928..913031705fb38 100644 --- a/scripts/apitypings/testdata/generics/generics.go +++ b/scripts/apitypings/testdata/generics/generics.go @@ -12,19 +12,20 @@ type Custom interface { // StaticGeneric has all generic fields defined in the field type StaticGeneric struct { - Static GenericFields[string, int, time.Duration] `json:"static"` + Static GenericFields[string, int, time.Duration, string] `json:"static"` } // DynamicGeneric can has some dynamic fields -type DynamicGeneric[C comparable, A any] struct { - Dynamic GenericFields[C, A, string] `json:"dynamic"` - Comparable C `json:"comparable"` +type DynamicGeneric[C comparable, A any, S Single] struct { + Dynamic GenericFields[C, A, string, S] `json:"dynamic"` + Comparable C `json:"comparable"` } -type GenericFields[C comparable, A any, T Custom] struct { +type GenericFields[C comparable, A any, T Custom, S Single] struct { Comparable C `json:"comparable"` Any A `json:"any"` - Custom T `json:"custom"` - Again T `json:"again"` + Custom T `json:"custom"` + Again T `json:"again"` + SingleContraint S `json:"single_constraint"` } diff --git a/scripts/apitypings/testdata/generics/generics.ts b/scripts/apitypings/testdata/generics/generics.ts index 8896afb764c62..7b435eb2dedcd 100644 --- a/scripts/apitypings/testdata/generics/generics.ts +++ b/scripts/apitypings/testdata/generics/generics.ts @@ -1,22 +1,23 @@ // Code generated by 'make coder/scripts/apitypings/main.go'. DO NOT EDIT. // From codersdk/generics.go -export interface DynamicGeneric { - readonly dynamic: GenericFields +export interface DynamicGeneric { + readonly dynamic: GenericFields readonly comparable: C } // From codersdk/generics.go -export interface GenericFields { +export interface GenericFields { readonly comparable: C readonly any: A readonly custom: T readonly again: T + readonly single_constraint: S } // From codersdk/generics.go export interface StaticGeneric { - readonly static: GenericFields + readonly static: GenericFields } // From codersdk/generics.go From cf161602bf08417a5cf09ffd0fafda84dae41c4f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 23:32:01 -0500 Subject: [PATCH 11/23] make fmt --- site/src/api/typesGenerated.ts | 41 +++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ad0e18aaecd94..5b3c44bd0b979 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -907,7 +907,10 @@ export type LogSource = "provisioner" | "provisioner_daemon" export type LoginType = "github" | "oidc" | "password" | "token" // From codersdk/parameters.go -export type ParameterDestinationScheme = "environment_variable" | "none" | "provisioner_variable" +export type ParameterDestinationScheme = + | "environment_variable" + | "none" + | "provisioner_variable" // From codersdk/parameters.go export type ParameterScope = "import_job" | "template" | "workspace" @@ -919,7 +922,13 @@ export type ParameterSourceScheme = "data" | "none" export type ParameterTypeSystem = "hcl" | "none" // From codersdk/provisionerdaemons.go -export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pending" | "running" | "succeeded" +export type ProvisionerJobStatus = + | "canceled" + | "canceling" + | "failed" + | "pending" + | "running" + | "succeeded" // From codersdk/organizations.go export type ProvisionerStorageMethod = "file" @@ -928,7 +937,15 @@ export type ProvisionerStorageMethod = "file" export type ProvisionerType = "echo" | "terraform" // From codersdk/audit.go -export type ResourceType = "api_key" | "git_ssh_key" | "group" | "organization" | "template" | "template_version" | "user" | "workspace" +export type ResourceType = + | "api_key" + | "git_ssh_key" + | "group" + | "organization" + | "template" + | "template_version" + | "user" + | "workspace" // From codersdk/sse.go export type ServerSentEventType = "data" | "error" | "ping" @@ -943,13 +960,27 @@ export type UserStatus = "active" | "suspended" export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected" // From codersdk/workspaceapps.go -export type WorkspaceAppHealth = "disabled" | "healthy" | "initializing" | "unhealthy" +export type WorkspaceAppHealth = + | "disabled" + | "healthy" + | "initializing" + | "unhealthy" // From codersdk/workspaceapps.go export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" // From codersdk/workspacebuilds.go -export type WorkspaceStatus = "canceled" | "canceling" | "deleted" | "deleting" | "failed" | "pending" | "running" | "starting" | "stopped" | "stopping" +export type WorkspaceStatus = + | "canceled" + | "canceling" + | "deleted" + | "deleting" + | "failed" + | "pending" + | "running" + | "starting" + | "stopped" + | "stopping" // From codersdk/workspacebuilds.go export type WorkspaceTransition = "delete" | "start" | "stop" From a0fed885d063aec27063a4e0d4d93bd4fe9e06db Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 23:39:13 -0500 Subject: [PATCH 12/23] Add more comments --- scripts/apitypings/main.go | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 12d693ce74bf7..a9d8306042375 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -13,14 +13,13 @@ import ( "strings" "text/template" - "github.com/coder/coder/coderd/util/slice" - "github.com/fatih/structtag" "golang.org/x/tools/go/packages" "golang.org/x/xerrors" "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" + "github.com/coder/coder/coderd/util/slice" ) const ( @@ -511,14 +510,39 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err return data.String(), nil } +c := TypescriptType { + ValueType: "comparable", + GenericValue: "C", + GenericTypes: map[string]string{ + "C":"comparable" + } +} + type TypescriptType struct { - // GenericValue gives a unique character for mapping the value type - // to a generic. This is only useful if you can use generic syntax. - // This is optional, as the ValueType will have the correct constraints. - GenericValue string - // GenericTypes is a map of generic name to actual constraint + // GenericTypes is a map of generic name to actual constraint. + // We return these, so we can bubble them up if we are recursively traversing + // a nested structure. We duplicate these at the top level. // Example: 'C = comparable'. GenericTypes map[string]string + // GenericValue is the value using the Generic name, rather than the constraint. + // This is only usedful if you can use the generic syntax. Things like maps + // don't currently support this, and will use the ValueType instead. + // Example: + // Given the Golang + // type Foo[C comparable] struct { + // Bar C + // } + // The field `Bar` will return: + // TypescriptType { + // ValueType: "comparable", + // GenericValue: "C", + // GenericTypes: map[string]string{ + // "C":"comparable" + // } + // } + GenericValue string + // ValueType is the typescript value type. This is the actual type or + // generic constraint. This can **always** be used without special handling. ValueType string // AboveTypeLine lets you put whatever text you want above the typescript // type line. From 9ff3fd8ffcccacf80216d859566fd48900a7fefe Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 23:41:04 -0500 Subject: [PATCH 13/23] Linting --- scripts/apitypings/main_test.go | 2 ++ scripts/apitypings/testdata/generics/generics.go | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/apitypings/main_test.go b/scripts/apitypings/main_test.go index c7fabede3c4e8..c68aebc0b29aa 100644 --- a/scripts/apitypings/main_test.go +++ b/scripts/apitypings/main_test.go @@ -10,6 +10,7 @@ import ( ) func TestGeneration(t *testing.T) { + t.Parallel() files, err := os.ReadDir("testdata") require.NoError(t, err, "read dir") @@ -20,6 +21,7 @@ func TestGeneration(t *testing.T) { } f := f t.Run(f.Name(), func(t *testing.T) { + t.Parallel() dir := filepath.Join(".", "testdata", f.Name()) output, err := Generate("./" + dir) require.NoErrorf(t, err, "generate %q", dir) diff --git a/scripts/apitypings/testdata/generics/generics.go b/scripts/apitypings/testdata/generics/generics.go index 913031705fb38..38524e5ee222f 100644 --- a/scripts/apitypings/testdata/generics/generics.go +++ b/scripts/apitypings/testdata/generics/generics.go @@ -25,7 +25,7 @@ type GenericFields[C comparable, A any, T Custom, S Single] struct { Comparable C `json:"comparable"` Any A `json:"any"` - Custom T `json:"custom"` - Again T `json:"again"` - SingleContraint S `json:"single_constraint"` + Custom T `json:"custom"` + Again T `json:"again"` + SingleConstraint S `json:"single_constraint"` } From 66a456c20170ebc872ae47437db971ee73a60f6c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 23:46:27 -0500 Subject: [PATCH 14/23] Add unit test readme --- scripts/apitypings/main.go | 10 +--------- scripts/apitypings/testdata/README.md | 5 +++++ scripts/apitypings/testdata/enums/enums.go | 10 ++++++++++ scripts/apitypings/testdata/enums/enums.ts | 4 ++++ 4 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 scripts/apitypings/testdata/README.md create mode 100644 scripts/apitypings/testdata/enums/enums.go create mode 100644 scripts/apitypings/testdata/enums/enums.ts diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index a9d8306042375..3992dc7d5c271 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -510,14 +510,6 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err return data.String(), nil } -c := TypescriptType { - ValueType: "comparable", - GenericValue: "C", - GenericTypes: map[string]string{ - "C":"comparable" - } -} - type TypescriptType struct { // GenericTypes is a map of generic name to actual constraint. // We return these, so we can bubble them up if we are recursively traversing @@ -543,7 +535,7 @@ type TypescriptType struct { GenericValue string // ValueType is the typescript value type. This is the actual type or // generic constraint. This can **always** be used without special handling. - ValueType string + ValueType string // AboveTypeLine lets you put whatever text you want above the typescript // type line. AboveTypeLine string diff --git a/scripts/apitypings/testdata/README.md b/scripts/apitypings/testdata/README.md new file mode 100644 index 0000000000000..4a8a6c0874e86 --- /dev/null +++ b/scripts/apitypings/testdata/README.md @@ -0,0 +1,5 @@ +# How to add a unit test + +1. Create a new directory in `testdata` +2. Name a go file `.go`. This file will generate the typescript. +3. Name the expected typescript file `.ts`. This is the unit test's expected output. diff --git a/scripts/apitypings/testdata/enums/enums.go b/scripts/apitypings/testdata/enums/enums.go new file mode 100644 index 0000000000000..7e6a5e71673f3 --- /dev/null +++ b/scripts/apitypings/testdata/enums/enums.go @@ -0,0 +1,10 @@ +package enums + +type Enum string + +const ( + EnumFoo Enum = "foo" + EnumBar Enum = "bar" + EnumBaz Enum = "baz" + EnumQux Enum = "qux" +) diff --git a/scripts/apitypings/testdata/enums/enums.ts b/scripts/apitypings/testdata/enums/enums.ts new file mode 100644 index 0000000000000..da07185cbd701 --- /dev/null +++ b/scripts/apitypings/testdata/enums/enums.ts @@ -0,0 +1,4 @@ +// Code generated by 'make coder/scripts/apitypings/main.go'. DO NOT EDIT. + +// From codersdk/enums.go +export type Enum = "bar" | "baz" | "foo" | "qux" From e653cf560513911a8b0afbace663221418a95e52 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Oct 2022 23:51:24 -0500 Subject: [PATCH 15/23] Linting --- scripts/apitypings/main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 3992dc7d5c271..070f013507b7e 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -768,9 +768,11 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // isBuiltIn returns the string for a builtin type that we want to support // if the name is a reserved builtin type. This is for types like 'comparable'. // These types are not implemented in golang, so we just have to hardcode it. -func (g *Generator) isBuiltIn(name string) (bool, string) { +func (_ Generator) isBuiltIn(name string) (bool, string) { // Note: @emyrk If we use constraints like Ordered, we can pull those - // dynamically from their respective packages. + // dynamically from their respective packages. This is a method on Generator + // so if someone wants to implement that, they can find the respective package + // and type. switch name { case "comparable": // To be complete, we include "any". Kinda sucks :( From 2474f4419315832666170d6f47ad2864a31696d7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Oct 2022 00:00:35 -0500 Subject: [PATCH 16/23] Final linting... --- scripts/apitypings/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 070f013507b7e..101eec520eb09 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -768,7 +768,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // isBuiltIn returns the string for a builtin type that we want to support // if the name is a reserved builtin type. This is for types like 'comparable'. // These types are not implemented in golang, so we just have to hardcode it. -func (_ Generator) isBuiltIn(name string) (bool, string) { +func (Generator) isBuiltIn(name string) (bool, string) { // Note: @emyrk If we use constraints like Ordered, we can pull those // dynamically from their respective packages. This is a method on Generator // so if someone wants to implement that, they can find the respective package From 6ddb71f2c85759d07436a60ccf69124281a617e3 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Oct 2022 00:02:51 -0500 Subject: [PATCH 17/23] Do not run tests on windows --- scripts/apitypings/main_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/apitypings/main_test.go b/scripts/apitypings/main_test.go index c68aebc0b29aa..d777f18950f17 100644 --- a/scripts/apitypings/main_test.go +++ b/scripts/apitypings/main_test.go @@ -1,3 +1,9 @@ +//go:build !windows +// +build !windows + +// Windows tests fail because the \n\r vs \n. It's not worth trying +// to replace newlines for os tests. If people start using this tool on windows +// and are seeing problems, then we can add build tags and figure it out. package main import ( From a18fe72911edc00499e671c58dace3f8fe40d36c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Oct 2022 01:28:30 -0500 Subject: [PATCH 18/23] Ensure constant ordering --- scripts/apitypings/main.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 101eec520eb09..6404952b767d7 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -481,7 +481,9 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err valueType := tsType.ValueType if tsType.GenericValue != "" { valueType = tsType.GenericValue - for name, constraint := range tsType.GenericTypes { + + for _, value := range tsType.GenericTypes { + name, constraint := value.Name, value.Constraint if _, ok := genericsUsed[name]; ok { // Don't add a generic twice // TODO: We should probably check that the generic mapping is @@ -510,12 +512,19 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err return data.String(), nil } +type GenericType struct { + // Name is the generic name, eg "C" + Name string + // Constraint is the interface constraint, eg "comparable" + Constraint string +} + type TypescriptType struct { - // GenericTypes is a map of generic name to actual constraint. + // GenericTypes are a list of the generic names to actual constraint. // We return these, so we can bubble them up if we are recursively traversing // a nested structure. We duplicate these at the top level. - // Example: 'C = comparable'. - GenericTypes map[string]string + // Example: '{Name: "C", Constraint: "comparable"}'. + GenericTypes []GenericType // GenericValue is the value using the Generic name, rather than the constraint. // This is only usedful if you can use the generic syntax. Things like maps // don't currently support this, and will use the ValueType instead. @@ -647,7 +656,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // we generate. name := n.Obj().Name() genericName := "" - genericTypes := make(map[string]string) + var genericTypes []GenericType if obj := g.pkg.Types.Scope().Lookup(name); obj != nil { // Sweet! Using other typescript types as fields. This could be an // enum or another struct @@ -664,7 +673,10 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // Using a generic defined by the parent. gname := param.Obj().Name() genericNames = append(genericNames, gname) - genericTypes[gname] = genType.ValueType + genericTypes = append(genericTypes, GenericType{ + Name: gname, + Constraint: genType.ValueType, + }) } else { // Defining a generic genericNames = append(genericNames, genType.ValueType) @@ -736,9 +748,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // If we don't have the type constraint defined somewhere in the package, // then we have to resort to using any. return TypescriptType{ - GenericTypes: map[string]string{ - ty.Obj().Name(): "any", - }, + GenericTypes: []GenericType{{Name: ty.Obj().Name(), Constraint: "any"}}, GenericValue: ty.Obj().Name(), ValueType: "any", AboveTypeLine: fmt.Sprintf("// %q is an external type, so we use any", name), @@ -750,9 +760,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { } return TypescriptType{ - GenericTypes: map[string]string{ - ty.Obj().Name(): name, - }, + GenericTypes: []GenericType{{Name: ty.Obj().Name(), Constraint: name}}, GenericValue: ty.Obj().Name(), ValueType: name, AboveTypeLine: "", From bc84463bbf8d06ed7716453a7016646f8af55057 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Oct 2022 01:48:25 -0500 Subject: [PATCH 19/23] Revert "Ensure constant ordering" This reverts commit a18fe72911edc00499e671c58dace3f8fe40d36c. --- scripts/apitypings/main.go | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 6404952b767d7..101eec520eb09 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -481,9 +481,7 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err valueType := tsType.ValueType if tsType.GenericValue != "" { valueType = tsType.GenericValue - - for _, value := range tsType.GenericTypes { - name, constraint := value.Name, value.Constraint + for name, constraint := range tsType.GenericTypes { if _, ok := genericsUsed[name]; ok { // Don't add a generic twice // TODO: We should probably check that the generic mapping is @@ -512,19 +510,12 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err return data.String(), nil } -type GenericType struct { - // Name is the generic name, eg "C" - Name string - // Constraint is the interface constraint, eg "comparable" - Constraint string -} - type TypescriptType struct { - // GenericTypes are a list of the generic names to actual constraint. + // GenericTypes is a map of generic name to actual constraint. // We return these, so we can bubble them up if we are recursively traversing // a nested structure. We duplicate these at the top level. - // Example: '{Name: "C", Constraint: "comparable"}'. - GenericTypes []GenericType + // Example: 'C = comparable'. + GenericTypes map[string]string // GenericValue is the value using the Generic name, rather than the constraint. // This is only usedful if you can use the generic syntax. Things like maps // don't currently support this, and will use the ValueType instead. @@ -656,7 +647,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // we generate. name := n.Obj().Name() genericName := "" - var genericTypes []GenericType + genericTypes := make(map[string]string) if obj := g.pkg.Types.Scope().Lookup(name); obj != nil { // Sweet! Using other typescript types as fields. This could be an // enum or another struct @@ -673,10 +664,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // Using a generic defined by the parent. gname := param.Obj().Name() genericNames = append(genericNames, gname) - genericTypes = append(genericTypes, GenericType{ - Name: gname, - Constraint: genType.ValueType, - }) + genericTypes[gname] = genType.ValueType } else { // Defining a generic genericNames = append(genericNames, genType.ValueType) @@ -748,7 +736,9 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { // If we don't have the type constraint defined somewhere in the package, // then we have to resort to using any. return TypescriptType{ - GenericTypes: []GenericType{{Name: ty.Obj().Name(), Constraint: "any"}}, + GenericTypes: map[string]string{ + ty.Obj().Name(): "any", + }, GenericValue: ty.Obj().Name(), ValueType: "any", AboveTypeLine: fmt.Sprintf("// %q is an external type, so we use any", name), @@ -760,7 +750,9 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { } return TypescriptType{ - GenericTypes: []GenericType{{Name: ty.Obj().Name(), Constraint: name}}, + GenericTypes: map[string]string{ + ty.Obj().Name(): name, + }, GenericValue: ty.Obj().Name(), ValueType: name, AboveTypeLine: "", From 3a183898eb5a64a5b1a425f321853300e5907ba9 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Oct 2022 01:58:08 -0500 Subject: [PATCH 20/23] Add field ordering --- scripts/apitypings/main.go | 34 ++++++++++++++++++- .../apitypings/testdata/generics/generics.go | 18 ++++++++-- .../apitypings/testdata/generics/generics.ts | 18 ++++++++-- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 101eec520eb09..05a545e33ead8 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -490,7 +490,6 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err // go, so I'm going to ignore this for now. continue } - state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", name, constraint)) genericsUsed[name] = constraint } } @@ -502,6 +501,39 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err state.Fields = append(state.Fields, fmt.Sprintf("%sreadonly %s%s: %s", indent, jsonName, optional, valueType)) } + // This is implemented to ensure the correct order of generics on the + // top level structure. Ordering of generic fields is important, and + // we want to match the same order as Golang. The gathering of generic types + // from our fields does not guarantee the order. + if named, ok := obj.(*types.TypeName); ok { + if namedType, ok := named.Type().(*types.Named); ok { + // Ensure proper generic param ordering + params := namedType.TypeParams() + for i := 0; i < params.Len(); i++ { + param := params.At(i) + name := param.String() + + constraint, ok := genericsUsed[param.String()] + if ok { + state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", name, constraint)) + } else { + // If this error is thrown, it is because you have defined a + // generic field on a structure, but did not use it in your + // fields. If this happens, remove the unused generic on + // the top level structure. We **technically** can implement + // this still, but it's not a case we need to support. + // Example: + // type Foo[A any] struct { + // Bar string + // } + return "", xerrors.Errorf("generic param %q missing on %q, fix your data structure", name, obj.Name()) + } + } + } + } else { + return "", xerrors.Errorf("generic param ordering undefined on %q", obj.Name()) + } + data := bytes.NewBuffer(make([]byte, 0)) err = tpl.Execute(data, state) if err != nil { diff --git a/scripts/apitypings/testdata/generics/generics.go b/scripts/apitypings/testdata/generics/generics.go index 38524e5ee222f..a842cb5693c9b 100644 --- a/scripts/apitypings/testdata/generics/generics.go +++ b/scripts/apitypings/testdata/generics/generics.go @@ -16,9 +16,17 @@ type StaticGeneric struct { } // DynamicGeneric can has some dynamic fields -type DynamicGeneric[C comparable, A any, S Single] struct { - Dynamic GenericFields[C, A, string, S] `json:"dynamic"` - Comparable C `json:"comparable"` +type DynamicGeneric[A any, S Single] struct { + Dynamic GenericFields[bool, A, string, S] `json:"dynamic"` + Comparable bool `json:"comparable"` +} + +type ComplexGeneric[C comparable, S Single, T Custom] struct { + Dynamic GenericFields[C, bool, string, S] `json:"dynamic"` + Order GenericFieldsDiffOrder[C, string, S, T] `json:"order"` + Comparable C `json:"comparable"` + Single S `json:"single"` + Static StaticGeneric `json:"static"` } type GenericFields[C comparable, A any, T Custom, S Single] struct { @@ -29,3 +37,7 @@ type GenericFields[C comparable, A any, T Custom, S Single] struct { Again T `json:"again"` SingleConstraint S `json:"single_constraint"` } + +type GenericFieldsDiffOrder[A any, C comparable, S Single, T Custom] struct { + GenericFields[C, A, T, S] +} diff --git a/scripts/apitypings/testdata/generics/generics.ts b/scripts/apitypings/testdata/generics/generics.ts index 7b435eb2dedcd..bd770148430fa 100644 --- a/scripts/apitypings/testdata/generics/generics.ts +++ b/scripts/apitypings/testdata/generics/generics.ts @@ -1,9 +1,18 @@ // Code generated by 'make coder/scripts/apitypings/main.go'. DO NOT EDIT. // From codersdk/generics.go -export interface DynamicGeneric { - readonly dynamic: GenericFields +export interface ComplexGeneric { + readonly dynamic: GenericFields + readonly order: GenericFieldsDiffOrder readonly comparable: C + readonly single: S + readonly static: StaticGeneric +} + +// From codersdk/generics.go +export interface DynamicGeneric { + readonly dynamic: GenericFields + readonly comparable: boolean } // From codersdk/generics.go @@ -15,6 +24,11 @@ export interface GenericFields { + readonly GenericFields: GenericFields +} + // From codersdk/generics.go export interface StaticGeneric { readonly static: GenericFields From 3d4cd7c63307b020e0ae4bcf0a7d55ee43e9e406 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Oct 2022 02:07:19 -0500 Subject: [PATCH 21/23] Ignore mac unit test --- scripts/apitypings/main.go | 62 +++++++++++++++++++-------------- scripts/apitypings/main_test.go | 6 ++-- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 05a545e33ead8..5de65f1028e66 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -481,6 +481,12 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err valueType := tsType.ValueType if tsType.GenericValue != "" { valueType = tsType.GenericValue + // This map we are building is just gathering all the generics used + // by our fields. We will use this map for our export type line. + // This isn't actually required since we can get it from the obj + // itself, but this ensures we actually use all the generic fields + // we place in the export line. If we are missing one from this map, + // that is a developer error. And we might as well catch it. for name, constraint := range tsType.GenericTypes { if _, ok := genericsUsed[name]; ok { // Don't add a generic twice @@ -505,35 +511,39 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err // top level structure. Ordering of generic fields is important, and // we want to match the same order as Golang. The gathering of generic types // from our fields does not guarantee the order. - if named, ok := obj.(*types.TypeName); ok { - if namedType, ok := named.Type().(*types.Named); ok { - // Ensure proper generic param ordering - params := namedType.TypeParams() - for i := 0; i < params.Len(); i++ { - param := params.At(i) - name := param.String() - - constraint, ok := genericsUsed[param.String()] - if ok { - state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", name, constraint)) - } else { - // If this error is thrown, it is because you have defined a - // generic field on a structure, but did not use it in your - // fields. If this happens, remove the unused generic on - // the top level structure. We **technically** can implement - // this still, but it's not a case we need to support. - // Example: - // type Foo[A any] struct { - // Bar string - // } - return "", xerrors.Errorf("generic param %q missing on %q, fix your data structure", name, obj.Name()) - } - } - } - } else { + named, ok := obj.(*types.TypeName) + if !ok { return "", xerrors.Errorf("generic param ordering undefined on %q", obj.Name()) } + namedType, ok := named.Type().(*types.Named) + if !ok { + return "", xerrors.Errorf("generic param %q unexpected type %q", obj.Name(), named.Type().String()) + } + + // Ensure proper generic param ordering + params := namedType.TypeParams() + for i := 0; i < params.Len(); i++ { + param := params.At(i) + name := param.String() + + constraint, ok := genericsUsed[param.String()] + if ok { + state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", name, constraint)) + } else { + // If this error is thrown, it is because you have defined a + // generic field on a structure, but did not use it in your + // fields. If this happens, remove the unused generic on + // the top level structure. We **technically** can implement + // this still, but it's not a case we need to support. + // Example: + // type Foo[A any] struct { + // Bar string + // } + return "", xerrors.Errorf("generic param %q missing on %q, fix your data structure", name, obj.Name()) + } + } + data := bytes.NewBuffer(make([]byte, 0)) err = tpl.Execute(data, state) if err != nil { diff --git a/scripts/apitypings/main_test.go b/scripts/apitypings/main_test.go index d777f18950f17..0e0c43c6a5bf2 100644 --- a/scripts/apitypings/main_test.go +++ b/scripts/apitypings/main_test.go @@ -1,9 +1,11 @@ -//go:build !windows -// +build !windows +//go:build !windows && !darwin +// +build !windows,!darwin // Windows tests fail because the \n\r vs \n. It's not worth trying // to replace newlines for os tests. If people start using this tool on windows // and are seeing problems, then we can add build tags and figure it out. + +// Darwin builds fail because of the "/" direction. package main import ( From 0f07e2815d5fb4dac00067c01964cd9e38e24aa9 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Oct 2022 02:08:47 -0500 Subject: [PATCH 22/23] Fix darwin unit test --- scripts/apitypings/main.go | 4 +++- scripts/apitypings/main_test.go | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 5de65f1028e66..4e5802e66ec90 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -6,6 +6,7 @@ import ( "fmt" "go/types" "os" + "path" "path/filepath" "reflect" "regexp" @@ -340,7 +341,8 @@ func (g *Generator) generateOne(m *Maps, obj types.Object) error { func (g *Generator) posLine(obj types.Object) string { file := g.pkg.Fset.File(obj.Pos()) - return fmt.Sprintf("// From %s\n", filepath.Join("codersdk", filepath.Base(file.Name()))) + // Do not use filepath, as that changes behavior based on OS + return fmt.Sprintf("// From %s\n", path.Join("codersdk", filepath.Base(file.Name()))) } // buildStruct just prints the typescript def for a type. diff --git a/scripts/apitypings/main_test.go b/scripts/apitypings/main_test.go index 0e0c43c6a5bf2..d777f18950f17 100644 --- a/scripts/apitypings/main_test.go +++ b/scripts/apitypings/main_test.go @@ -1,11 +1,9 @@ -//go:build !windows && !darwin -// +build !windows,!darwin +//go:build !windows +// +build !windows // Windows tests fail because the \n\r vs \n. It's not worth trying // to replace newlines for os tests. If people start using this tool on windows // and are seeing problems, then we can add build tags and figure it out. - -// Darwin builds fail because of the "/" direction. package main import ( From 824273793ab890e8bd0a32111738ac1c1a25b0e9 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Oct 2022 02:13:17 -0500 Subject: [PATCH 23/23] More linting, some branching refactoring --- scripts/apitypings/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 4e5802e66ec90..720a828f100bf 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -530,9 +530,7 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err name := param.String() constraint, ok := genericsUsed[param.String()] - if ok { - state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", name, constraint)) - } else { + if !ok { // If this error is thrown, it is because you have defined a // generic field on a structure, but did not use it in your // fields. If this happens, remove the unused generic on @@ -544,6 +542,8 @@ func (g *Generator) buildStruct(obj types.Object, st *types.Struct) (string, err // } return "", xerrors.Errorf("generic param %q missing on %q, fix your data structure", name, obj.Name()) } + + state.Generics = append(state.Generics, fmt.Sprintf("%s extends %s", name, constraint)) } data := bytes.NewBuffer(make([]byte, 0))