264 lines
5.7 KiB
Go
264 lines
5.7 KiB
Go
package tygoja
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/packages"
|
|
)
|
|
|
|
// Tygoja is a generator for one or more input packages,
|
|
// responsible for linking them together if necessary.
|
|
type Tygoja struct {
|
|
conf *Config
|
|
|
|
parent *Tygoja
|
|
implicitPackages map[string][]string
|
|
generatedTypes map[string][]string
|
|
generatedPackageDocs map[string]struct{}
|
|
}
|
|
|
|
// New initializes a new Tygoja generator from the specified config.
|
|
func New(config Config) *Tygoja {
|
|
config.InitDefaults()
|
|
|
|
return &Tygoja{
|
|
conf: &config,
|
|
implicitPackages: map[string][]string{},
|
|
generatedTypes: map[string][]string{},
|
|
generatedPackageDocs: map[string]struct{}{},
|
|
}
|
|
}
|
|
|
|
// Generate executes the generator and produces the related TS files.
|
|
func (g *Tygoja) Generate() (string, error) {
|
|
// extract config packages
|
|
configPackages := make([]string, 0, len(g.conf.Packages))
|
|
for p, types := range g.conf.Packages {
|
|
if len(types) == 0 {
|
|
continue // no typings
|
|
}
|
|
configPackages = append(configPackages, p)
|
|
}
|
|
|
|
// load packages info
|
|
pkgs, err := packages.Load(&packages.Config{
|
|
Mode: packages.NeedSyntax | packages.NeedFiles | packages.NeedDeps | packages.NeedImports | packages.NeedTypes,
|
|
}, configPackages...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var s strings.Builder
|
|
|
|
// Heading
|
|
if g.parent == nil {
|
|
s.WriteString("// GENERATED CODE - DO NOT MODIFY BY HAND\n")
|
|
|
|
if g.conf.Heading != "" {
|
|
s.WriteString(g.conf.Heading)
|
|
}
|
|
|
|
// write base types
|
|
// ---
|
|
s.WriteString("type ")
|
|
s.WriteString(BaseTypeDict)
|
|
s.WriteString(" = { [key:string | number | symbol]: any; }\n")
|
|
|
|
s.WriteString("type ")
|
|
s.WriteString(BaseTypeAny)
|
|
s.WriteString(" = any\n")
|
|
// ---
|
|
}
|
|
|
|
for i, pkg := range pkgs {
|
|
if len(pkg.Errors) > 0 {
|
|
return "", fmt.Errorf("%+v", pkg.Errors)
|
|
}
|
|
|
|
if len(pkg.GoFiles) == 0 {
|
|
return "", fmt.Errorf("no input go files for package index %d", i)
|
|
}
|
|
|
|
if len(g.conf.Packages[pkg.ID]) == 0 {
|
|
// ignore the package as it has no typings
|
|
continue
|
|
}
|
|
|
|
pkgGen := &PackageGenerator{
|
|
conf: g.conf,
|
|
pkg: pkg,
|
|
types: g.conf.Packages[pkg.ID],
|
|
withPkgDoc: !g.isPackageDocGenerated(pkg.ID),
|
|
generatedTypes: map[string]struct{}{},
|
|
unknownTypes: map[string]struct{}{},
|
|
imports: map[string][]string{},
|
|
}
|
|
|
|
code, err := pkgGen.Generate()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
g.generatedPackageDocs[pkg.ID] = struct{}{}
|
|
|
|
for t := range pkgGen.generatedTypes {
|
|
g.generatedTypes[pkg.ID] = append(g.generatedTypes[pkg.ID], t)
|
|
}
|
|
|
|
for t := range pkgGen.unknownTypes {
|
|
parts := strings.Split(t, ".")
|
|
var tPkg string
|
|
var tName string
|
|
|
|
if len(parts) == 0 {
|
|
continue
|
|
}
|
|
|
|
if len(parts) == 2 {
|
|
// type from external package
|
|
tPkg = parts[0]
|
|
tName = parts[1]
|
|
} else {
|
|
// unexported type from the current package
|
|
tName = parts[0]
|
|
|
|
// already mapped for export
|
|
if pkgGen.isTypeAllowed(tName) {
|
|
continue
|
|
}
|
|
|
|
tPkg = packageNameFromPath(pkg.ID)
|
|
|
|
// add to self import later
|
|
pkgGen.imports[pkg.ID] = []string{tPkg}
|
|
}
|
|
|
|
for p, aliases := range pkgGen.imports {
|
|
for _, alias := range aliases {
|
|
if tName != "" && alias == tPkg && !g.isTypeGenerated(p, tName) && !exists(g.implicitPackages[p], tName) {
|
|
if g.implicitPackages[p] == nil {
|
|
g.implicitPackages[p] = []string{}
|
|
}
|
|
g.implicitPackages[p] = append(g.implicitPackages[p], tName)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
s.WriteString(code)
|
|
}
|
|
|
|
// recursively try to generate the found unknown types
|
|
if len(g.implicitPackages) > 0 {
|
|
subConfig := *g.conf
|
|
subConfig.Heading = ""
|
|
if (subConfig.TypeMappings) == nil {
|
|
subConfig.TypeMappings = map[string]string{}
|
|
}
|
|
|
|
// extract the nonempty package definitions
|
|
subConfig.Packages = make(map[string][]string, len(g.implicitPackages))
|
|
for p, types := range g.implicitPackages {
|
|
if len(types) == 0 {
|
|
continue
|
|
}
|
|
subConfig.Packages[p] = types
|
|
}
|
|
|
|
subGenerator := New(subConfig)
|
|
subGenerator.parent = g
|
|
subResult, err := subGenerator.Generate()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
s.WriteString(subResult)
|
|
}
|
|
|
|
return s.String(), nil
|
|
}
|
|
|
|
func (g *Tygoja) isPackageDocGenerated(pkgId string) bool {
|
|
_, ok := g.generatedPackageDocs[pkgId]
|
|
if ok {
|
|
return true
|
|
}
|
|
|
|
if g.parent != nil {
|
|
return g.parent.isPackageDocGenerated(pkgId)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (g *Tygoja) isTypeGenerated(pkg string, name string) bool {
|
|
if g.parent != nil && g.parent.isTypeGenerated(pkg, name) {
|
|
return true
|
|
}
|
|
|
|
if len(g.generatedTypes[pkg]) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, t := range g.generatedTypes[pkg] {
|
|
if t == name {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// isTypeAllowed checks whether the provided type name is allowed by the generator "types".
|
|
func (g *PackageGenerator) isTypeAllowed(name string) bool {
|
|
name = strings.TrimSpace(name)
|
|
|
|
if name == "" {
|
|
return false
|
|
}
|
|
|
|
for _, t := range g.types {
|
|
if t == name || t == "*" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (g *PackageGenerator) markTypeAsGenerated(t string) {
|
|
g.generatedTypes[t] = struct{}{}
|
|
}
|
|
|
|
var versionRegex = regexp.MustCompile(`^v\d+$`)
|
|
|
|
// packageNameFromPath extracts and normalizes the imported package identifier.
|
|
//
|
|
// For example:
|
|
//
|
|
// "github.com/labstack/echo/v5" -> "echo"
|
|
// "github.com/go-ozzo/ozzo-validation/v4" -> "ozzo_validation"
|
|
func packageNameFromPath(path string) string {
|
|
name := filepath.Base(strings.Trim(path, `"' `))
|
|
|
|
if versionRegex.MatchString(name) {
|
|
name = filepath.Base(filepath.Dir(path))
|
|
}
|
|
|
|
return strings.ReplaceAll(name, "-", "_")
|
|
}
|
|
|
|
// exists checks if search exists in list.
|
|
func exists[T comparable](list []T, search T) bool {
|
|
for _, v := range list {
|
|
if v == search {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|