409 lines
9.9 KiB
Go
409 lines
9.9 KiB
Go
package tygoja
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"strings"
|
|
)
|
|
|
|
type groupContext struct {
|
|
isGroupedDeclaration bool
|
|
doc *ast.CommentGroup
|
|
groupValue string
|
|
groupType string
|
|
iotaValue int
|
|
iotaOffset int
|
|
}
|
|
|
|
// Writing of function declarations, which are expressions like
|
|
// "func Count() int"
|
|
// or
|
|
// "func (s *Counter) total() int"
|
|
func (g *PackageGenerator) writeFuncDecl(s *strings.Builder, decl *ast.FuncDecl, depth int) {
|
|
if decl.Name == nil || len(decl.Name.Name) == 0 || decl.Name.Name[0] < 'A' || decl.Name.Name[0] > 'Z' {
|
|
return // unexported function/method
|
|
}
|
|
|
|
originalMethodName := decl.Name.Name
|
|
methodName := originalMethodName
|
|
if g.conf.MethodNameFormatter != nil {
|
|
methodName = g.conf.MethodNameFormatter(methodName)
|
|
}
|
|
|
|
if decl.Recv == nil {
|
|
if !g.conf.WithPackageFunctions {
|
|
return // skip package level functions
|
|
}
|
|
|
|
if !g.isTypeAllowed(originalMethodName) {
|
|
return
|
|
} else {
|
|
g.markTypeAsGenerated(originalMethodName)
|
|
}
|
|
|
|
g.writeStartModifier(s, depth)
|
|
s.WriteString("interface ")
|
|
|
|
if isReservedIdentifier(methodName) {
|
|
s.WriteString("_" + methodName)
|
|
} else {
|
|
s.WriteString(methodName)
|
|
}
|
|
|
|
if decl.Type.TypeParams != nil {
|
|
g.writeTypeParamsFields(s, decl.Type.TypeParams.List)
|
|
}
|
|
|
|
s.WriteString(" {\n")
|
|
if decl.Doc != nil {
|
|
g.writeCommentGroup(s, decl.Doc, depth+1)
|
|
}
|
|
g.writeIndent(s, depth+1)
|
|
g.writeType(s, decl.Type, depth+1)
|
|
s.WriteString("\n")
|
|
g.writeIndent(s, depth)
|
|
s.WriteString("}\n")
|
|
} else if len(decl.Recv.List) == 1 {
|
|
// write struct method as new interface method
|
|
// (note that TS will "merge" the definitions of multiple interfaces with the same name)
|
|
|
|
// treat pointer and value receivers the same
|
|
recvType := decl.Recv.List[0].Type
|
|
if p, isPointer := recvType.(*ast.StarExpr); isPointer {
|
|
recvType = p.X
|
|
}
|
|
|
|
var recvName string
|
|
switch recv := recvType.(type) {
|
|
case *ast.Ident:
|
|
recvName = recv.Name
|
|
case *ast.IndexExpr:
|
|
case *ast.IndexListExpr:
|
|
if v, ok := recv.X.(*ast.Ident); ok {
|
|
recvName = v.Name
|
|
}
|
|
}
|
|
|
|
if !g.isTypeAllowed(recvName) {
|
|
return
|
|
} else {
|
|
g.markTypeAsGenerated(recvName)
|
|
}
|
|
|
|
g.writeStartModifier(s, depth)
|
|
s.WriteString("interface ")
|
|
|
|
g.writeType(s, recvType, depth)
|
|
|
|
s.WriteString(" {\n")
|
|
if decl.Doc != nil {
|
|
g.writeCommentGroup(s, decl.Doc, depth+1)
|
|
}
|
|
g.writeIndent(s, depth+1)
|
|
s.WriteString(methodName)
|
|
g.writeType(s, decl.Type, depth+1)
|
|
s.WriteString("\n")
|
|
g.writeIndent(s, depth)
|
|
s.WriteString("}\n")
|
|
}
|
|
}
|
|
|
|
func (g *PackageGenerator) writeGroupDecl(s *strings.Builder, decl *ast.GenDecl, depth int) {
|
|
// We need a bit of state to handle syntax like
|
|
// const (
|
|
// X SomeType = iota
|
|
// _
|
|
// Y
|
|
// Foo string = "Foo"
|
|
// _
|
|
// AlsoFoo
|
|
// )
|
|
group := &groupContext{
|
|
isGroupedDeclaration: len(decl.Specs) > 1,
|
|
doc: decl.Doc,
|
|
groupType: "",
|
|
groupValue: "",
|
|
iotaValue: -1,
|
|
}
|
|
|
|
for _, spec := range decl.Specs {
|
|
g.writeSpec(s, spec, group, depth)
|
|
}
|
|
}
|
|
|
|
func (g *PackageGenerator) writeSpec(s *strings.Builder, spec ast.Spec, group *groupContext, depth int) {
|
|
// e.g. "type Foo struct {}" or "type Bar = string"
|
|
ts, ok := spec.(*ast.TypeSpec)
|
|
if ok {
|
|
g.writeTypeSpec(s, ts, group, depth)
|
|
}
|
|
|
|
// e.g. "const Foo = 123"
|
|
vs, ok := spec.(*ast.ValueSpec)
|
|
if ok && g.conf.WithConstants {
|
|
g.writeValueSpec(s, vs, group, depth)
|
|
}
|
|
}
|
|
|
|
// Writing of type specs, which are expressions like
|
|
// "type X struct { ... }"
|
|
// or
|
|
// "type Bar = string"
|
|
func (g *PackageGenerator) writeTypeSpec(s *strings.Builder, ts *ast.TypeSpec, group *groupContext, depth int) {
|
|
var typeName string
|
|
if ts.Name != nil {
|
|
typeName = ts.Name.Name
|
|
}
|
|
|
|
if !g.isTypeAllowed(typeName) {
|
|
return
|
|
} else {
|
|
g.markTypeAsGenerated(typeName)
|
|
}
|
|
|
|
if ts.Doc != nil {
|
|
// the spec has its own comment, which overrules the grouped comment
|
|
g.writeCommentGroup(s, ts.Doc, depth)
|
|
} else {
|
|
g.writeCommentGroup(s, group.doc, depth)
|
|
}
|
|
|
|
switch v := ts.Type.(type) {
|
|
case *ast.StructType:
|
|
// eg. "type X struct { ... }"
|
|
|
|
var extendTypeName string
|
|
|
|
// convert embeded structs to "extends SUB_TYPE" declaration
|
|
//
|
|
// note: we don't use "extends A, B, C" form but intersecion subtype
|
|
// with all embeded structs to avoid methods merge conflicts
|
|
// eg. bufio.ReadWriter has different Writer.Read() and Reader.Read()
|
|
if v.Fields != nil {
|
|
var embeds []*ast.Field
|
|
for _, f := range v.Fields.List {
|
|
if len(f.Names) == 0 || f.Names[0].Name == "" {
|
|
embeds = append(embeds, f)
|
|
}
|
|
}
|
|
|
|
if len(embeds) > 0 {
|
|
extendTypeName = "_s" + PseudorandomString(6)
|
|
|
|
genericArgs := map[string]struct{}{}
|
|
identSB := new(strings.Builder)
|
|
embedsSB := new(strings.Builder)
|
|
for i, f := range embeds {
|
|
if i > 0 {
|
|
embedsSB.WriteString("&")
|
|
}
|
|
|
|
typ := f.Type
|
|
if p, isPointer := typ.(*ast.StarExpr); isPointer {
|
|
typ = p.X
|
|
}
|
|
|
|
identSB.Reset()
|
|
g.writeType(identSB, typ, depth, optionParenthesis, optionExtends)
|
|
ident := identSB.String()
|
|
|
|
if idx := strings.Index(ident, "<"); idx > 1 { // has at least 2 characters for <>
|
|
genericArgs[ident[idx+1:len(ident)-1]] = struct{}{}
|
|
}
|
|
|
|
embedsSB.WriteString(ident)
|
|
}
|
|
|
|
if len(genericArgs) > 0 {
|
|
args := make([]string, 0, len(genericArgs))
|
|
for g := range genericArgs {
|
|
args = append(args, g)
|
|
}
|
|
extendTypeName = extendTypeName + "<" + strings.Join(args, ",") + ">"
|
|
}
|
|
|
|
g.writeIndent(s, depth)
|
|
s.WriteString("type ")
|
|
s.WriteString(extendTypeName)
|
|
s.WriteString(" = ")
|
|
s.WriteString(embedsSB.String())
|
|
s.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
g.writeStartModifier(s, depth)
|
|
s.WriteString("interface ")
|
|
s.WriteString(typeName)
|
|
|
|
if ts.TypeParams != nil {
|
|
g.writeTypeParamsFields(s, ts.TypeParams.List)
|
|
}
|
|
|
|
if extendTypeName != "" {
|
|
s.WriteString(" extends ")
|
|
s.WriteString(extendTypeName)
|
|
}
|
|
|
|
s.WriteString(" {\n")
|
|
g.writeStructFields(s, v.Fields.List, depth)
|
|
g.writeIndent(s, depth)
|
|
s.WriteString("}")
|
|
case *ast.InterfaceType:
|
|
// eg. "type X interface { ... }"
|
|
|
|
g.writeStartModifier(s, depth)
|
|
s.WriteString("interface ")
|
|
s.WriteString(typeName)
|
|
|
|
if ts.TypeParams != nil {
|
|
g.writeTypeParamsFields(s, ts.TypeParams.List)
|
|
}
|
|
|
|
s.WriteString(" {\n")
|
|
|
|
// fallback so that it doesn't report an error when attempting
|
|
// to access a field or method from a struct that implements the interface
|
|
g.writeIndent(s, depth+1)
|
|
s.WriteString("[key:string]: any;\n")
|
|
|
|
g.writeInterfaceFields(s, v.Methods.List, depth)
|
|
g.writeIndent(s, depth)
|
|
s.WriteString("}")
|
|
case *ast.FuncType:
|
|
// eg. "type Handler func() any"
|
|
|
|
g.writeStartModifier(s, depth)
|
|
s.WriteString("interface ")
|
|
s.WriteString(typeName)
|
|
|
|
if ts.TypeParams != nil {
|
|
g.writeTypeParamsFields(s, ts.TypeParams.List)
|
|
}
|
|
|
|
s.WriteString(" {")
|
|
g.writeFuncType(s, v, depth, false)
|
|
g.writeIndent(s, depth)
|
|
s.WriteString("}")
|
|
default:
|
|
// other Go type declarations like "type JsonArray []any"
|
|
// (note: we don't use "type X = Y", but "interface X extends Y" syntax to allow later defining methods to the X type)
|
|
|
|
var baseType string
|
|
subSB := new(strings.Builder)
|
|
g.writeType(subSB, ts.Type, depth, optionParenthesis, optionExtends)
|
|
switch baseType = subSB.String(); baseType {
|
|
// primitives can't be extended so we use their Object equivivalents
|
|
case "number", "string", "boolean":
|
|
baseType = strings.ToUpper(string(baseType[0])) + baseType[1:]
|
|
case "any":
|
|
baseType = BaseTypeAny
|
|
}
|
|
|
|
g.writeStartModifier(s, depth)
|
|
s.WriteString("interface ")
|
|
s.WriteString(typeName)
|
|
|
|
if ts.TypeParams != nil {
|
|
g.writeTypeParamsFields(s, ts.TypeParams.List)
|
|
}
|
|
|
|
s.WriteString(" extends ")
|
|
|
|
s.WriteString(baseType)
|
|
|
|
s.WriteString("{}")
|
|
}
|
|
|
|
if ts.Comment != nil {
|
|
s.WriteString(" // " + ts.Comment.Text())
|
|
} else {
|
|
s.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
// Writing of value specs, which are exported const expressions like
|
|
// const SomeValue = 3
|
|
func (g *PackageGenerator) writeValueSpec(s *strings.Builder, vs *ast.ValueSpec, group *groupContext, depth int) {
|
|
for i, name := range vs.Names {
|
|
group.iotaValue = group.iotaValue + 1
|
|
if name.Name == "_" {
|
|
continue
|
|
}
|
|
|
|
if !g.isTypeAllowed(name.Name) {
|
|
continue
|
|
} else {
|
|
g.markTypeAsGenerated(name.Name)
|
|
}
|
|
|
|
constName := name.Name
|
|
if isReservedIdentifier(constName) {
|
|
constName = "_" + constName
|
|
}
|
|
|
|
if vs.Doc != nil { // The spec has its own comment, which overrules the grouped comment.
|
|
g.writeCommentGroup(s, vs.Doc, depth)
|
|
} else if group.isGroupedDeclaration {
|
|
g.writeCommentGroup(s, group.doc, depth)
|
|
}
|
|
|
|
hasExplicitValue := len(vs.Values) > i
|
|
if hasExplicitValue {
|
|
group.groupType = ""
|
|
}
|
|
|
|
g.writeStartModifier(s, depth)
|
|
s.WriteString("const ")
|
|
s.WriteString(constName)
|
|
|
|
if vs.Type != nil {
|
|
s.WriteString(": ")
|
|
|
|
tempSB := &strings.Builder{}
|
|
g.writeType(tempSB, vs.Type, depth, optionParenthesis)
|
|
typeString := tempSB.String()
|
|
|
|
s.WriteString(typeString)
|
|
group.groupType = typeString
|
|
} else if group.groupType != "" && !hasExplicitValue {
|
|
s.WriteString(": ")
|
|
s.WriteString(group.groupType)
|
|
}
|
|
|
|
s.WriteString(" = ")
|
|
|
|
if hasExplicitValue {
|
|
val := vs.Values[i]
|
|
tempSB := &strings.Builder{}
|
|
g.writeType(tempSB, val, depth, optionParenthesis)
|
|
|
|
valueString := tempSB.String()
|
|
if isProbablyIotaType(valueString) {
|
|
iotaV, err := basicIotaOffsetValueParse(valueString)
|
|
if err != nil {
|
|
// fallback
|
|
group.groupValue = valueString
|
|
} else {
|
|
group.iotaOffset = iotaV
|
|
group.groupValue = "iota"
|
|
valueString = fmt.Sprint(group.iotaValue + group.iotaOffset)
|
|
}
|
|
} else {
|
|
group.groupValue = valueString
|
|
}
|
|
s.WriteString(valueString)
|
|
} else { // We must use the previous value or +1 in case of iota
|
|
valueString := group.groupValue
|
|
if group.groupValue == "iota" {
|
|
valueString = fmt.Sprint(group.iotaValue + group.iotaOffset)
|
|
}
|
|
s.WriteString(valueString)
|
|
}
|
|
|
|
if vs.Comment != nil {
|
|
s.WriteString(" // " + vs.Comment.Text())
|
|
} else {
|
|
s.WriteByte('\n')
|
|
}
|
|
}
|
|
}
|