package main import ( "errors" "go/ast" "go/constant" "go/importer" "go/parser" "go/token" "go/types" "io/fs" "log" "os" "strings" "gopkg.in/yaml.v3" ) func main() { if _, err := os.Stat("./go.mod"); errors.Is(err, os.ErrNotExist) { log.Fatalln("go.mod not found") } if err := generateYAML("./internal/domain"); err != nil { log.Fatalln(err) } } func generateYAML(root string) error { m, err := getErrorCodesFromFolder(root) if err != nil { return err } var description strings.Builder enum := make([]string, 0, len(m)) for c, d := range m { if d != "" { if description.Len() > 0 { description.WriteRune('\n') } description.WriteString("* `") description.WriteString(c) description.WriteString("` - ") description.WriteString(d) } enum = append(enum, c) } return yaml.NewEncoder(os.Stdout).Encode(struct { Type string `yaml:"type"` Description string `yaml:"description"` Enum []string `yaml:"enum"` }{ Type: "string", Description: description.String(), Enum: enum, }) } //nolint:gocyclo func getErrorCodesFromFolder(root string) (map[string]string, error) { fset := token.NewFileSet() astDomainPkg, err := parseDomainPackage(fset, root) if err != nil { return nil, err } conf := types.Config{Importer: importer.Default()} files := make([]*ast.File, 0, len(astDomainPkg.Files)) for _, f := range astDomainPkg.Files { files = append(files, f) } domainPkg, err := conf.Check(astDomainPkg.Name, fset, files, nil) if err != nil { return nil, err } scope := domainPkg.Scope() m := make(map[string]string) for _, n := range scope.Names() { if !strings.HasPrefix(n, "errorCode") { continue } obj := scope.Lookup(n) c, ok := obj.(*types.Const) if !ok || c.Val().Kind() != constant.String { continue } f := astDomainPkg.Files[fset.File(c.Pos()).Name()] position := fset.Position(c.Pos()) commentMap := ast.NewCommentMap(fset, f, f.Comments) var desc string for _, decl := range f.Decls { declPosition := fset.Position(decl.Pos()) if declPosition.Filename != position.Filename || declPosition.Line != position.Line { continue } if comments := commentMap.Filter(decl).Comments(); len(comments) > 0 { desc = strings.TrimSpace(comments[0].Text()) } break } if strings.Contains(desc, "errorcode:ignore") { continue } m[constant.StringVal(c.Val())] = desc } return m, nil } func parseDomainPackage(fset *token.FileSet, root string) (*ast.Package, error) { pkgs, err := parser.ParseDir(fset, root, func(info fs.FileInfo) bool { if info.IsDir() && info.Name() != root { return false } if info.IsDir() || strings.HasSuffix(info.Name(), "_test.go") { return false } return true }, parser.AllErrors|parser.ParseComments) if err != nil { return nil, err } astDomainPkg, ok := pkgs["domain"] if !ok { return nil, errors.New("domain pkg not found") } return astDomainPkg, nil }