// exportable typescript generated from golang // Copyright (C) 2022 Fabio Prada package tsrpc import ( "bufio" "fmt" "go/ast" "go/doc" "io" "log" "os" "strings" "golang.org/x/tools/go/packages" ) type TSInfoPakage struct { structs map[string]TSStruct types map[string]TSType enums map[string]TSEnum consts map[string]TSConst decs map[string]TSDec endpoints map[string]TSEndpoint imports map[string][]string isUsed bool } type TSDec struct { Name string Value string } type TSType struct { Name string Type string TsType string Typescript bool dependOn bool } type TSInfo struct { Packages map[string]TSInfoPakage } type TSConst struct { Name string Value string } type TSEnum struct { Name string Info []TSEnumInfo } type TSEnumInfo struct { Key string Value string } // / exporter type TSModuleInfo struct { structs map[string]string types map[string]string } type TSSourceLine struct { Pos int End int Line int Source string } type TSSourceFile struct { Source string Name string Lines []TSSourceLine Len int } func (ts *TSInfo) findStruct(p string, n string) bool { if _, ok := ts.Packages[p]; ok { if n == "" || IsNativeType(n) { return true } n = strings.TrimPrefix(n, "[]") n = strings.TrimPrefix(n, "*") if _, ok := ts.Packages[p].structs[n]; ok { return true } } return false } func (ts *TSInfo) findType(p string, n string) bool { if _, ok := ts.Packages[p]; ok { if n == "" || IsNativeType(n) { return true } n = strings.TrimPrefix(n, "[]") n = strings.TrimPrefix(n, "*") if _, ok := ts.Packages[p].types[n]; ok { return true } } return false } func (ts TSInfo) setTypescript(p string, n string, v bool) bool { if !ts.findStruct(p, n) { return false } if s, ok := ts.Packages[p].structs[n]; ok { s.Typescript = v ts.Packages[p].structs[n] = s // write back return true } return false } func (ts TSInfo) find(p string, n string) bool { return ts.findType(p, n) || ts.findStruct(p, n) } // popola TsInfo con tutte le definizioni dei tipi func (i *TSInfo) getConst(p string, c *doc.Value) { var isTypescript = strings.HasPrefix(c.Doc, "Typescript:") if isTypescript { command := strings.TrimPrefix(c.Doc, "Typescript:") command = strings.TrimSpace(command) command = strings.Trim(command, "\n") if strings.Contains(command, "type") { fmt.Printf("TypeScript type declaration found in const s, but type parsing is not implemented yet\n") } if strings.Contains(command, "enum=") { enumName := strings.TrimPrefix(command, "enum=") enum := TSEnum{ Name: enumName, Info: []TSEnumInfo{}, } d := c.Decl iota := false iotaValue := 0 for _, s := range d.Specs { v := s.(*ast.ValueSpec) // safe because decl.Tok == token.CONST if len(v.Values) > 0 { be, ok := v.Values[0].(*ast.BinaryExpr) if ok { x := be.X.(*ast.BasicLit) exitOnError(fmt.Errorf("enum binary expression not implemented %s %s %s", x.Value, be.Op.String(), be.Y)) } ident, ok := v.Values[0].(*ast.Ident) if ok { if ident.Name == "iota" { iota = true iotaValue = v.Names[0].Obj.Data.(int) enum.Info = append(enum.Info, TSEnumInfo{Key: v.Names[0].Name, Value: fmt.Sprintf("%d", iotaValue)}) } } list, ok := v.Values[0].(*ast.BasicLit) if ok { enum.Info = append(enum.Info, TSEnumInfo{Key: v.Names[0].Name, Value: list.Value}) } } else { for _, name := range v.Names { if iota { iotaValue++ enum.Info = append(enum.Info, TSEnumInfo{Key: name.Name, Value: fmt.Sprintf("%d", iotaValue)}) } } } } i.Packages[p].enums[enumName] = enum t1 := TSType{ Name: enumName, Typescript: false, Type: "", TsType: fmt.Sprintf("typeof Enum%s[keyof typeof Enum%s] ", enumName, enumName), //getFieldTsInfo(expr.Type), dependOn: false, } i.Packages[p].types[enumName] = t1 } if strings.Contains(command, "const") { d := c.Decl for _, s := range d.Specs { v := s.(*ast.ValueSpec) if len(v.Names) == 0 || len(v.Values) == 0 { continue } c := TSConst{ Name: v.Names[0].Name, Value: v.Values[0].(*ast.BasicLit).Value, } i.Packages[p].consts[c.Name] = c } } if strings.Contains(command, "type") { fmt.Printf("TypeScript type declaration found in const s, but type parsing is not implemented yet\n") } } } func (i *TSInfo) getType(p string, t *doc.Type) { var isTypescript = strings.HasPrefix(t.Doc, "Typescript:") if isTypescript { command := strings.TrimPrefix(t.Doc, "Typescript:") command = strings.TrimSpace(command) command = strings.Trim(command, "\n") if strings.Contains(command, "interface") { fmt.Printf("TypeScript interface declaration found in type s, but interface parsing is not implemented yet\n") } if strings.Contains(command, "type") { fmt.Printf("TypeScript type declaration found in type s, but type parsing is not implemented yet\n") } if strings.Contains(command, "enum=") { fmt.Printf("TypeScript enum declaration found in type s, but enum parsing is not implemented yet\n") } } for _, spec := range t.Decl.Specs { /* if len(t.Consts) > 0 { i.getConst(p, t.Consts[0]) continue } */ switch spec.(type) { case *ast.TypeSpec: typeSpec := spec.(*ast.TypeSpec) switch typeSpec.Type.(type) { case *ast.StructType: // if isType && command != "interface" { // exitOnError(fmt.Errorf("mismatch delaration for interface %s AT: %s", t.Doc, getSourceInfo(int(typeSpec.Name.NamePos), src))) // } v := TSStruct{ Name: typeSpec.Name.Name, Typescript: false, Fields: []TSSField{}, SourceInfo: "", } v.getStruct(typeSpec) i.Packages[p].structs[typeSpec.Name.Name] = v default: // if isType && command != "interface" { // exitOnError(fmt.Errorf("mismatch delaration for interface %s AT: %s", t.Doc, getSourceInfo(int(typeSpec.Name.NamePos), src))) // } tsInfo := getFieldTsInfo(typeSpec.Type) t := TSType{ Name: typeSpec.Name.Name, Typescript: false, Type: getFieldInfo(typeSpec.Type), TsType: tsInfo, dependOn: toBeImported(typeSpec.Type), } //fmt.Printf("Type found: %s Type: %s TsType: %s AT: %s\n", t.Name, t.Type, t.TsType, t.SourceInfo) i.Packages[p].types[typeSpec.Name.Name] = t } } } } func parseTypescriptDeclarations(n string, pkg TSInfoPakage) { f, err := os.OpenFile(n, os.O_RDONLY, os.ModePerm) if err != nil { log.Fatalf("open file error: %v", err) return } defer f.Close() l := 1 dat := "" lines := []TSSourceLine{} rd := bufio.NewReader(f) for { line, err := rd.ReadString('\n') if err != nil { if err == io.EOF { break } log.Fatalf("read file line error: %v", err) return } lines = append(lines, TSSourceLine{Pos: l, End: l + len(line), Line: l, Source: line}) l++ dat += line if strings.Contains(line, "// Typescript:") { if strings.Contains(line, "TStype=") { p := strings.Index(line, "TStype=") s := strings.Trim(line[p+len("TStype="):], " ") a := strings.Split(s, "=") if len(a) == 2 { t := TSType{ Name: strings.Trim(a[0], " "), Typescript: true, Type: "UserDefined", TsType: strings.Trim(a[1], " "), dependOn: false, } pkg.types[strings.Trim(a[0], " ")] = t } } if strings.Contains(line, "TSDeclaration=") { p := strings.Index(line, "TSDeclaration=") s := strings.Trim(line[p+len("TSDeclaration="):], " ") a := strings.Split(s, "=") if len(a) == 2 { t := TSDec{ Name: strings.Trim(a[0], " "), Value: strings.Trim(a[1], " "), } pkg.decs[strings.Trim(a[0], " ")] = t } } if strings.Contains(line, "TSEndpoint= ") { e := ParseEndpoint(line, n, l) if _, ok := pkg.endpoints[e.Name]; ok { exitOnError(fmt.Errorf("enpoint name %s allready in use: %s", e.Name, line)) } pkg_info := pkg pkg_info.endpoints[e.Name] = e pkg_info.imports = e.Imports pkg_info.isUsed = true pkg = pkg_info } } } } func (i *TSInfo) Populate(path string) { i.Packages = make(map[string]TSInfoPakage) cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedSyntax, Dir: path, } pkgs, err := packages.Load(cfg, "./...") if err != nil { log.Fatal(err) } if packages.PrintErrors(pkgs) > 0 { log.Fatal("package loading failed") } //to get line info for endpoints and typescript declarations for _, loadedPkg := range pkgs { pkg := loadedPkg.Name // skip packages if pkg == "typescript" || pkg == "tsrpc" { continue } // initialize package info if not exists if _, ok := i.Packages[pkg]; !ok { i.Packages[pkg] = TSInfoPakage{structs: make(map[string]TSStruct), types: make(map[string]TSType), enums: make(map[string]TSEnum), consts: make(map[string]TSConst), decs: make(map[string]TSDec), endpoints: make(map[string]TSEndpoint)} } // start parsing files for _, n := range loadedPkg.CompiledGoFiles { // search for declarative // Typescript: declarations and endpoints parseTypescriptDeclarations(n, i.Packages[pkg]) } // parse doc to get all types and consts docPkg, err := doc.NewFromFiles(loadedPkg.Fset, loadedPkg.Syntax, loadedPkg.PkgPath) if err != nil { exitOnError(err) } for _, t := range docPkg.Types { i.getType(pkg, t) } for _, c := range docPkg.Consts { i.getConst(pkg, c) } } } func (ts *TSInfo) findAvailableStruct(pkg string, n string, deep int) (int, bool) { a := strings.Split(n, ".") if n == "" || IsNativeType(n) { return deep, true } deep++ if deep > 10 { exitOnError(fmt.Errorf("too much deep for struct %s", n)) } if len(a) == 1 { n = strings.TrimPrefix(n, "[]") n = strings.TrimPrefix(n, "*") if _, ok := ts.Packages[pkg].structs[n]; ok { s := ts.Packages[pkg].structs[n] s.Typescript = true ts.Packages[pkg].structs[n] = s // write back for _, v := range s.Fields { deep, _ = ts.findAvailableStruct(pkg, v.Type, deep) } deep-- return deep, true } } if len(a) == 2 { a[1] = strings.TrimPrefix(a[1], "[]") a[1] = strings.TrimPrefix(a[1], "*") if _, ok := ts.Packages[a[0]]; ok { if _, ok := ts.Packages[a[0]].structs[a[1]]; ok { s := ts.Packages[a[0]].structs[a[1]] if s.Typescript { deep-- return deep, true } s.Typescript = true ts.Packages[a[0]].structs[a[1]] = s // write back for _, v := range s.Fields { ts.findAvailableStruct(pkg, v.Type, deep) } deep-- return deep, true } } } return deep, false } func (i *TSInfo) TestEndpoints() { for p := range i.Packages { for _, v1 := range i.Packages[p].endpoints { _, f := i.findAvailableStruct(p, v1.Request, 0) if !f { fmt.Printf("??Endpoint: request %s not found\n", v1.Request) } _, f = i.findAvailableStruct(p, v1.Response, 0) if !f { fmt.Printf("??Endpoint: response %s not found\n", v1.Response) } } } }