461 lines
11 KiB
Go
461 lines
11 KiB
Go
package tygoja
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"go/ast"
|
|
"go/token"
|
|
)
|
|
|
|
// Options for the writeType() method that can be used for extra context
|
|
// to determine the format of the return type.
|
|
const (
|
|
optionExtends = "extends"
|
|
optionParenthesis = "parenthesis"
|
|
optionFunctionReturn = "func_return"
|
|
)
|
|
|
|
func (g *PackageGenerator) writeIndent(s *strings.Builder, depth int) {
|
|
for i := 0; i < depth; i++ {
|
|
s.WriteString(g.conf.Indent)
|
|
}
|
|
}
|
|
|
|
func (g *PackageGenerator) writeStartModifier(s *strings.Builder, depth int) {
|
|
g.writeIndent(s, depth)
|
|
|
|
if g.conf.StartModifier != "" {
|
|
s.WriteString(g.conf.StartModifier)
|
|
s.WriteString(" ")
|
|
}
|
|
}
|
|
|
|
func (g *PackageGenerator) writeType(s *strings.Builder, t ast.Expr, depth int, options ...string) {
|
|
switch t := t.(type) {
|
|
case *ast.StarExpr:
|
|
if hasOption(optionParenthesis, options) {
|
|
s.WriteByte('(')
|
|
}
|
|
|
|
g.writeType(s, t.X, depth)
|
|
|
|
// allow undefined union only when not used in an "extends" expression or as return type
|
|
if !hasOption(optionExtends, options) && !hasOption(optionFunctionReturn, options) {
|
|
s.WriteString(" | undefined")
|
|
}
|
|
|
|
if hasOption(optionParenthesis, options) {
|
|
s.WriteByte(')')
|
|
}
|
|
case *ast.Ellipsis:
|
|
if v, ok := t.Elt.(*ast.Ident); ok && v.String() == "byte" {
|
|
s.WriteString("string")
|
|
break
|
|
}
|
|
|
|
// wrap variadic args with parenthesis to support function declarations
|
|
// (eg. "...callbacks: (() => number)[]")
|
|
_, isFunc := t.Elt.(*ast.FuncType)
|
|
if isFunc {
|
|
s.WriteString("(")
|
|
}
|
|
|
|
g.writeType(s, t.Elt, depth, optionParenthesis)
|
|
|
|
if isFunc {
|
|
s.WriteString(")")
|
|
}
|
|
|
|
s.WriteString("[]")
|
|
case *ast.ArrayType:
|
|
if v, ok := t.Elt.(*ast.Ident); ok && v.String() == "byte" && !hasOption(optionExtends, options) {
|
|
// union type with string since depending where it is used
|
|
// goja auto converts string to []byte if the field expect that
|
|
s.WriteString("string|")
|
|
}
|
|
|
|
s.WriteString("Array<")
|
|
g.writeType(s, t.Elt, depth, optionParenthesis)
|
|
s.WriteString(">")
|
|
case *ast.StructType:
|
|
s.WriteString("{\n")
|
|
g.writeStructFields(s, t.Fields.List, depth+1)
|
|
g.writeIndent(s, depth+1)
|
|
s.WriteByte('}')
|
|
case *ast.Ident:
|
|
v := t.String()
|
|
|
|
mappedType, ok := g.conf.TypeMappings[v]
|
|
if ok {
|
|
// use the mapped type
|
|
v = mappedType
|
|
} else {
|
|
// try to find a matching js equivalent
|
|
switch v {
|
|
case "string":
|
|
v = "string"
|
|
case "bool":
|
|
v = "boolean"
|
|
case "int", "int8", "int16", "int32", "int64",
|
|
"uint", "uint8", "uint16", "uint32", "uint64",
|
|
"float32", "float64",
|
|
"complex64", "complex128",
|
|
"uintptr", "byte", "rune":
|
|
v = "number"
|
|
case "error":
|
|
v = "Error"
|
|
default:
|
|
g.unknownTypes[v] = struct{}{}
|
|
}
|
|
}
|
|
|
|
s.WriteString(v)
|
|
case *ast.SelectorExpr:
|
|
// e.g. `unsafe.Pointer` or `unsafe.*`
|
|
fullType := fmt.Sprintf("%s.%s", t.X, t.Sel)
|
|
fullTypeWildcard := fmt.Sprintf("%s.*", t.X)
|
|
|
|
if v, ok := g.conf.TypeMappings[fullType]; ok {
|
|
s.WriteString(v)
|
|
} else if v, ok := g.conf.TypeMappings[fullTypeWildcard]; ok {
|
|
s.WriteString(v)
|
|
} else {
|
|
g.unknownTypes[fullType] = struct{}{}
|
|
s.WriteString(fullType)
|
|
}
|
|
case *ast.MapType:
|
|
s.WriteString("_TygojaDict")
|
|
case *ast.BasicLit:
|
|
s.WriteString(t.Value)
|
|
case *ast.ParenExpr:
|
|
s.WriteByte('(')
|
|
g.writeType(s, t.X, depth)
|
|
s.WriteByte(')')
|
|
case *ast.BinaryExpr:
|
|
g.writeType(s, t.X, depth)
|
|
s.WriteByte(' ')
|
|
s.WriteString(t.Op.String())
|
|
s.WriteByte(' ')
|
|
g.writeType(s, t.Y, depth)
|
|
case *ast.InterfaceType:
|
|
s.WriteString("{\n")
|
|
g.writeInterfaceFields(s, t.Methods.List, depth)
|
|
g.writeIndent(s, depth+1)
|
|
s.WriteByte('}')
|
|
case *ast.FuncType:
|
|
g.writeFuncType(s, t, depth, hasOption(optionParenthesis, options))
|
|
case *ast.UnaryExpr:
|
|
if t.Op == token.TILDE {
|
|
// we just ignore the tilde token, in Typescript extended types are
|
|
// put into the generic typing itself, which we can't support yet.
|
|
g.writeType(s, t.X, depth)
|
|
} else {
|
|
// only log for now
|
|
log.Printf("unhandled unary expr: %v\n %T\n", t, t)
|
|
}
|
|
case *ast.IndexListExpr:
|
|
g.writeType(s, t.X, depth)
|
|
s.WriteByte('<')
|
|
for i, index := range t.Indices {
|
|
g.writeType(s, index, depth)
|
|
if i != len(t.Indices)-1 {
|
|
s.WriteString(", ")
|
|
}
|
|
}
|
|
s.WriteByte('>')
|
|
case *ast.IndexExpr:
|
|
g.writeType(s, t.X, depth)
|
|
s.WriteByte('<')
|
|
g.writeType(s, t.Index, depth)
|
|
s.WriteByte('>')
|
|
case *ast.CallExpr, *ast.ChanType, *ast.CompositeLit:
|
|
s.WriteString("undefined")
|
|
default:
|
|
s.WriteString("any")
|
|
}
|
|
}
|
|
|
|
func (g *PackageGenerator) writeTypeParamsFields(s *strings.Builder, fields []*ast.Field) {
|
|
// extract params
|
|
names := []string{}
|
|
for _, f := range fields {
|
|
for _, ident := range f.Names {
|
|
names = append(names, ident.Name)
|
|
|
|
// disable extends for now as it complicates the interfaces merge
|
|
//
|
|
// s.WriteString(" extends ")
|
|
// g.writeType(s, f.Type, 0, true)
|
|
// if i != len(fields)-1 || j != len(f.Names)-1 {
|
|
// s.WriteString(", ")
|
|
// }
|
|
}
|
|
}
|
|
|
|
if len(names) == 0 {
|
|
return
|
|
}
|
|
|
|
s.WriteByte('<')
|
|
|
|
for i, name := range names {
|
|
if i > 0 {
|
|
s.WriteString(",")
|
|
}
|
|
s.WriteString(name)
|
|
}
|
|
|
|
s.WriteByte('>')
|
|
}
|
|
|
|
func (g *PackageGenerator) writeInterfaceFields(s *strings.Builder, fields []*ast.Field, depth int) {
|
|
for _, f := range fields {
|
|
g.writeCommentGroup(s, f.Doc, depth+1)
|
|
|
|
var methodName string
|
|
if len(f.Names) != 0 && f.Names[0] != nil && len(f.Names[0].Name) != 0 {
|
|
methodName = f.Names[0].Name
|
|
}
|
|
if len(methodName) == 0 || 'A' > methodName[0] || methodName[0] > 'Z' {
|
|
continue
|
|
}
|
|
|
|
if g.conf.MethodNameFormatter != nil {
|
|
methodName = g.conf.MethodNameFormatter(methodName)
|
|
}
|
|
|
|
g.writeIndent(s, depth+1)
|
|
s.WriteString(methodName)
|
|
g.writeType(s, f.Type, depth)
|
|
|
|
if f.Comment != nil {
|
|
s.WriteString(" // ")
|
|
s.WriteString(f.Comment.Text())
|
|
} else {
|
|
s.WriteByte('\n')
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *PackageGenerator) writeStructFields(s *strings.Builder, fields []*ast.Field, depth int) {
|
|
for _, f := range fields {
|
|
var fieldName string
|
|
if len(f.Names) != 0 && f.Names[0] != nil && len(f.Names[0].Name) != 0 {
|
|
fieldName = f.Names[0].Name
|
|
}
|
|
if len(fieldName) == 0 || 'A' > fieldName[0] || fieldName[0] > 'Z' {
|
|
continue
|
|
}
|
|
|
|
if g.conf.FieldNameFormatter != nil {
|
|
fieldName = g.conf.FieldNameFormatter(fieldName)
|
|
}
|
|
|
|
g.writeCommentGroup(s, f.Doc, depth+1)
|
|
|
|
g.writeIndent(s, depth+1)
|
|
quoted := !isValidJSName(fieldName)
|
|
if quoted {
|
|
s.WriteByte('\'')
|
|
}
|
|
s.WriteString(fieldName)
|
|
if quoted {
|
|
s.WriteByte('\'')
|
|
}
|
|
|
|
// check if it is nil-able, aka. optional
|
|
switch t := f.Type.(type) {
|
|
case *ast.StarExpr:
|
|
f.Type = t.X
|
|
s.WriteByte('?')
|
|
}
|
|
|
|
s.WriteString(": ")
|
|
g.writeType(s, f.Type, depth, optionParenthesis)
|
|
|
|
if f.Comment != nil {
|
|
// Line comment is present, that means a comment after the field.
|
|
s.WriteString(" // ")
|
|
s.WriteString(f.Comment.Text())
|
|
} else {
|
|
s.WriteByte('\n')
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *PackageGenerator) writeFuncType(s *strings.Builder, t *ast.FuncType, depth int, returnAsProp bool) {
|
|
s.WriteString("(")
|
|
|
|
if t.Params != nil {
|
|
g.writeFuncParams(s, t.Params.List, depth)
|
|
}
|
|
|
|
if returnAsProp {
|
|
s.WriteString(") => ")
|
|
} else {
|
|
s.WriteString("): ")
|
|
}
|
|
|
|
// (from https://pkg.go.dev/github.com/dop251/goja)
|
|
// Functions with multiple return values return an Array.
|
|
// If the last return value is an `error` it is not returned but converted into a JS exception.
|
|
// If the error is *Exception, it is thrown as is, otherwise it's wrapped in a GoEerror.
|
|
// Note that if there are exactly two return values and the last is an `error`,
|
|
// the function returns the first value as is, not an Array.
|
|
if t.Results == nil || len(t.Results.List) == 0 {
|
|
s.WriteString("void")
|
|
} else {
|
|
// remove the last return error type
|
|
lastReturn, ok := t.Results.List[len(t.Results.List)-1].Type.(*ast.Ident)
|
|
if ok && lastReturn.Name == "error" {
|
|
t.Results.List = t.Results.List[:len(t.Results.List)-1]
|
|
}
|
|
|
|
if len(t.Results.List) == 0 {
|
|
s.WriteString("void")
|
|
} else {
|
|
// multiple and shortened return type values must be wrapped in []
|
|
// (combined/shortened return values from the same type are part of a single ast.Field but with different names)
|
|
hasMultipleReturnValues := len(t.Results.List) > 1 || len(t.Results.List[0].Names) > 1
|
|
if hasMultipleReturnValues {
|
|
s.WriteRune('[')
|
|
}
|
|
|
|
for i, f := range t.Results.List {
|
|
totalNames := max(len(f.Names), 1)
|
|
for j := range totalNames {
|
|
if i > 0 || j > 0 {
|
|
s.WriteString(", ")
|
|
}
|
|
|
|
g.writeType(s, f.Type, 0, optionParenthesis, optionFunctionReturn)
|
|
}
|
|
}
|
|
|
|
if hasMultipleReturnValues {
|
|
s.WriteRune(']')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *PackageGenerator) writeFuncParams(s *strings.Builder, params []*ast.Field, depth int) {
|
|
for i, f := range params {
|
|
// normalize params iteration
|
|
// (params with omitted types will be part of a single ast.Field but with different names)
|
|
names := make([]string, 0, len(f.Names))
|
|
for j, ident := range f.Names {
|
|
name := ident.Name
|
|
if name == "" || isReservedIdentifier(name) {
|
|
name = fmt.Sprintf("_arg%d%d", i, j)
|
|
}
|
|
names = append(names, name)
|
|
}
|
|
if len(names) == 0 {
|
|
// ommitted param name (eg. func(string))
|
|
names = append(names, fmt.Sprintf("_arg%d", i))
|
|
}
|
|
|
|
for j, fieldName := range names {
|
|
if i > 0 || j > 0 {
|
|
s.WriteString(", ")
|
|
}
|
|
|
|
var isVariadic bool
|
|
|
|
switch t := f.Type.(type) {
|
|
case *ast.StarExpr:
|
|
f.Type = t.X
|
|
case *ast.Ellipsis:
|
|
isVariadic = true
|
|
}
|
|
|
|
g.writeCommentGroup(s, f.Doc, depth+2)
|
|
if isVariadic {
|
|
s.WriteString("...")
|
|
}
|
|
s.WriteString(fieldName)
|
|
|
|
s.WriteString(": ")
|
|
|
|
g.writeType(s, f.Type, depth, optionParenthesis)
|
|
|
|
if f.Comment != nil {
|
|
// Line comment is present, that means a comment after the field.
|
|
s.WriteString(" /* ")
|
|
s.WriteString(f.Comment.Text())
|
|
s.WriteString(" */ ")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// see https://es5.github.io/#x7.6.1.1
|
|
var reservedIdentifiers = map[string]struct{}{
|
|
"break": {},
|
|
"do": {},
|
|
"instanceof": {},
|
|
"typeof": {},
|
|
"case": {},
|
|
"else": {},
|
|
"new": {},
|
|
"var": {},
|
|
"catch": {},
|
|
"finally": {},
|
|
"return": {},
|
|
"void": {},
|
|
"continue": {},
|
|
"for": {},
|
|
"switch": {},
|
|
"while": {},
|
|
"debugger": {},
|
|
"function": {},
|
|
"this": {},
|
|
"with": {},
|
|
"default": {},
|
|
"if": {},
|
|
"throw": {},
|
|
"delete": {},
|
|
"in": {},
|
|
"try": {},
|
|
"class": {},
|
|
"enum": {},
|
|
"extends": {},
|
|
"super": {},
|
|
"const": {},
|
|
"export": {},
|
|
"import": {},
|
|
"implements": {},
|
|
"let": {},
|
|
"private": {},
|
|
"public": {},
|
|
"yield": {},
|
|
"interface": {},
|
|
"package": {},
|
|
"protected": {},
|
|
"static": {},
|
|
}
|
|
|
|
func isReservedIdentifier(name string) bool {
|
|
_, ok := reservedIdentifiers[name]
|
|
return ok
|
|
}
|
|
|
|
var isValidJSNameRegexp = regexp.MustCompile(`(?m)^[\pL_][\pL\pN_]*$`)
|
|
|
|
func isValidJSName(name string) bool {
|
|
return isReservedIdentifier(name) || isValidJSNameRegexp.MatchString(name)
|
|
}
|
|
|
|
func hasOption(opt string, options []string) bool {
|
|
for _, o := range options {
|
|
if o == opt {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|