mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-05 05:52:33 +08:00
1295 lines
32 KiB
Go
1295 lines
32 KiB
Go
package parser
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/wailsapp/wails/v3/internal/flags"
|
|
|
|
"github.com/samber/lo"
|
|
"github.com/wailsapp/wails/v3/internal/hash"
|
|
)
|
|
|
|
type packagePath = string
|
|
type structName = string
|
|
|
|
// ErrNoBindingsFound is returned when no bound structs are found
|
|
var ErrNoBindingsFound = errors.New("no bound structs found")
|
|
|
|
type StructDef struct {
|
|
Name string
|
|
DocComments []string
|
|
Fields []*Field
|
|
}
|
|
|
|
func (s *StructDef) DefaultValueList() string {
|
|
var allFields []string
|
|
for _, field := range s.Fields {
|
|
thisFieldWithDefaultValue := fmt.Sprintf("%s = %s", field.JSName(), field.DefaultValue())
|
|
allFields = append(allFields, thisFieldWithDefaultValue)
|
|
}
|
|
return strings.Join(allFields, ", ")
|
|
}
|
|
|
|
type ParameterType struct {
|
|
Name string
|
|
IsStruct bool
|
|
IsSlice bool
|
|
IsPointer bool
|
|
IsEnum bool
|
|
MapKey *ParameterType
|
|
MapValue *ParameterType
|
|
Package string
|
|
}
|
|
|
|
type EnumDef struct {
|
|
Name string
|
|
Filename string
|
|
DocComments []string
|
|
Values []*EnumValue
|
|
}
|
|
|
|
type EnumValue struct {
|
|
Name string
|
|
Value string
|
|
DocComments []string
|
|
}
|
|
|
|
type Parameter struct {
|
|
Name string
|
|
Type *ParameterType
|
|
project *Project
|
|
}
|
|
|
|
func (p *Parameter) NamespacedStructType(pkgName string) string {
|
|
var typeName string
|
|
thisPkg := p.project.packageCache[pkgName]
|
|
pkgInfo := p.project.packageCache[p.Type.Package]
|
|
if pkgInfo.Name != "" && pkgInfo.Path != thisPkg.Path {
|
|
typeName = pkgInfo.Name
|
|
} else {
|
|
if p.Type.Package != "" && p.Type.Package != pkgName {
|
|
parts := strings.Split(p.Type.Package, "/")
|
|
typeName = parts[len(parts)-1] + "."
|
|
}
|
|
}
|
|
return typeName + p.Type.Name
|
|
}
|
|
func (p *Parameter) NamespacedStructVariable(pkgName string) string {
|
|
var typeName string
|
|
if p.Type.Package != "" && p.Type.Package != pkgName {
|
|
parts := strings.Split(p.Type.Package, "/")
|
|
typeName = parts[len(parts)-1]
|
|
}
|
|
return typeName + p.Type.Name
|
|
}
|
|
|
|
func (p *Parameter) JSType(pkgName string) string {
|
|
// Convert type to javascript equivalent type
|
|
var typeName string
|
|
switch p.Type.Name {
|
|
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "float32", "float64":
|
|
typeName = "number"
|
|
case "string":
|
|
typeName = "string"
|
|
case "bool":
|
|
typeName = "boolean"
|
|
default:
|
|
typeName = p.Type.Name
|
|
}
|
|
|
|
// if the type is a struct, we need to add the package name
|
|
if p.Type.IsStruct || p.Type.IsEnum {
|
|
typeName = p.NamespacedStructType(pkgName)
|
|
typeName = strings.ReplaceAll(typeName, ".", "")
|
|
}
|
|
|
|
// Add slice suffix
|
|
if p.Type.IsSlice {
|
|
typeName += "[]"
|
|
}
|
|
|
|
// Add pointer suffix
|
|
if p.Type.IsPointer {
|
|
typeName += " | null"
|
|
}
|
|
|
|
return typeName
|
|
}
|
|
|
|
type BoundMethod struct {
|
|
Package string
|
|
PackageDir string
|
|
Name string
|
|
DocComment string
|
|
Inputs []*Parameter
|
|
Outputs []*Parameter
|
|
ID uint32
|
|
Alias *uint32
|
|
}
|
|
|
|
func (m BoundMethod) JSInputs() []*Parameter {
|
|
if len(m.Inputs) > 0 {
|
|
if firstArg := m.Inputs[0]; isContext(firstArg) {
|
|
return m.Inputs[1:]
|
|
}
|
|
}
|
|
|
|
return m.Inputs
|
|
}
|
|
|
|
func (m BoundMethod) IDAsString() string {
|
|
return strconv.Itoa(int(m.ID))
|
|
}
|
|
|
|
type Field struct {
|
|
Name string
|
|
Type *ParameterType
|
|
Project *Project
|
|
}
|
|
|
|
func (f *Field) JSName() string {
|
|
return strings.ToLower(f.Name[0:1]) + f.Name[1:]
|
|
}
|
|
|
|
// TSBuild contains the typescript to build a field for a JS object
|
|
// via assignment for simple types or constructors for structs
|
|
func (f *Field) TSBuild(pkg string) string {
|
|
if !f.Type.IsStruct {
|
|
return fmt.Sprintf("safeSource['%s']", f.JSName())
|
|
}
|
|
|
|
if f.Type.Package == "" || f.Type.Package == pkg {
|
|
return fmt.Sprintf("%s.createFrom(source['%s'])", f.Type.Name, f.JSName())
|
|
}
|
|
|
|
return fmt.Sprintf("%s.%s.createFrom(source['%s'])", pkgAlias(f.Type.Package), f.Type.Name, f.JSName())
|
|
}
|
|
|
|
func (f *Field) JSDef(pkg string) string {
|
|
var jsType string
|
|
switch f.Type.Name {
|
|
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "float32", "float64":
|
|
jsType = "number"
|
|
case "string":
|
|
jsType = "string"
|
|
case "bool":
|
|
jsType = "boolean"
|
|
default:
|
|
jsType = f.Type.Name
|
|
}
|
|
|
|
var result string
|
|
isExternalStruct := f.Type.Package != "" && f.Type.Package != pkg && f.Type.IsStruct
|
|
if f.Type.Package == "" || f.Type.Package == pkg || !isExternalStruct {
|
|
result += fmt.Sprintf("%s: %s;", f.JSName(), jsType)
|
|
} else {
|
|
parts := strings.Split(f.Type.Package, "/")
|
|
result += fmt.Sprintf("%s: %s.%s;", f.JSName(), parts[len(parts)-1], jsType)
|
|
}
|
|
|
|
if !ast.IsExported(f.Name) {
|
|
result += " // Warning: this is unexported in the Go struct."
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (f *Field) JSDocType(pkg string) string {
|
|
var jsType string
|
|
switch f.Type.Name {
|
|
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "float32", "float64":
|
|
jsType = "number"
|
|
case "string":
|
|
jsType = "string"
|
|
case "bool":
|
|
jsType = "boolean"
|
|
default:
|
|
jsType = f.Type.Name
|
|
}
|
|
|
|
// If we are the same package, just return the type
|
|
externalPkgInfo := f.Project.packageCache[f.Type.Package]
|
|
if externalPkgInfo.Name == pkg {
|
|
return jsType
|
|
}
|
|
|
|
var result string
|
|
isExternalStruct := f.Type.Package != "" && f.Type.Package != pkg && f.Type.IsStruct
|
|
if f.Type.Package == "" || !isExternalStruct {
|
|
if f.Type.IsStruct || f.Type.IsEnum {
|
|
// get the relative package directory
|
|
result = fmt.Sprintf("%s%s", externalPkgInfo.Name, f.Type.Name)
|
|
} else {
|
|
result = jsType
|
|
}
|
|
} else {
|
|
// get the relative package directory
|
|
result = fmt.Sprintf("%s%s", externalPkgInfo.Name, f.Type.Name)
|
|
}
|
|
|
|
if !ast.IsExported(f.Name) {
|
|
result += " // Warning: this is unexported in the Go struct."
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (f *Field) DefaultValue() string {
|
|
// Return the default value of the typescript version of the type as a string
|
|
switch f.Type.Name {
|
|
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uintptr", "float32", "float64", "uint64":
|
|
return "0"
|
|
case "string":
|
|
return `""`
|
|
case "bool":
|
|
return "false"
|
|
default:
|
|
return "null"
|
|
}
|
|
}
|
|
|
|
type ConstDef struct {
|
|
Name string
|
|
DocComments []string
|
|
Value string
|
|
}
|
|
|
|
type TypeDef struct {
|
|
Name string
|
|
DocComments []string
|
|
Type string
|
|
Consts []*ConstDef
|
|
ShouldGenerate bool
|
|
}
|
|
|
|
func (t *TypeDef) GeneratedName() string {
|
|
return t.Name + "Enum"
|
|
}
|
|
|
|
type ParsedPackage struct {
|
|
Pkg *ast.Package
|
|
Name string
|
|
Path string
|
|
Dir string
|
|
StructCache map[structName]*StructDef
|
|
TypeCache map[string]*TypeDef
|
|
}
|
|
|
|
type Project struct {
|
|
packageCache map[string]*ParsedPackage
|
|
outputDirectory string
|
|
Path string
|
|
BoundMethods map[packagePath]map[structName][]*BoundMethod
|
|
Models map[packagePath]map[structName]*StructDef
|
|
Types map[packagePath]map[structName]*TypeDef
|
|
anonymousStructIDCounter int
|
|
Stats Stats
|
|
}
|
|
|
|
type Stats struct {
|
|
NumPackages int
|
|
NumStructs int
|
|
NumMethods int
|
|
NumEnums int
|
|
NumModels int
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
}
|
|
|
|
func ParseProject(projectPath string) (*Project, error) {
|
|
absPath, err := filepath.Abs(projectPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := &Project{
|
|
Path: absPath,
|
|
BoundMethods: make(map[packagePath]map[structName][]*BoundMethod),
|
|
packageCache: make(map[string]*ParsedPackage),
|
|
}
|
|
result.Stats.StartTime = time.Now()
|
|
pkgs, err := result.parseDirectory(projectPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = result.findApplicationNewCalls(pkgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, pkg := range result.packageCache {
|
|
if len(pkg.StructCache) > 0 {
|
|
if result.Models == nil {
|
|
result.Models = make(map[packagePath]map[structName]*StructDef)
|
|
}
|
|
result.Models[pkg.Path] = pkg.StructCache
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func GenerateBindingsAndModels(options *flags.GenerateBindingsOptions) (*Project, error) {
|
|
p, err := ParseProject(options.ProjectDirectory)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
|
|
if p.BoundMethods == nil {
|
|
return p, nil
|
|
}
|
|
err = os.MkdirAll(options.OutputDirectory, 0755)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
p.outputDirectory = options.OutputDirectory
|
|
for _, pkg := range p.BoundMethods {
|
|
for _, boundMethods := range pkg {
|
|
p.Stats.NumMethods += len(boundMethods)
|
|
}
|
|
}
|
|
generatedMethods := p.GenerateBindings(p.BoundMethods, options.UseIDs, options.TS)
|
|
for pkg, structs := range generatedMethods {
|
|
// Write the directory
|
|
err = os.MkdirAll(filepath.Join(options.OutputDirectory, pkg), 0755)
|
|
if err != nil && !os.IsExist(err) {
|
|
return p, err
|
|
}
|
|
// Write the files
|
|
for structName, text := range structs {
|
|
p.Stats.NumStructs++
|
|
filename := structName + ".js"
|
|
if options.TS {
|
|
filename = structName + ".ts"
|
|
}
|
|
err = os.WriteFile(filepath.Join(options.OutputDirectory, pkg, filename), []byte(text), 0644)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
}
|
|
}
|
|
|
|
p.Stats.NumModels = len(p.Models)
|
|
p.Stats.NumEnums = len(p.Types)
|
|
|
|
// Generate Models
|
|
if len(p.Models) > 0 {
|
|
generatedModels, err := p.GenerateModels(p.Models, p.Types, options)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
for pkg, text := range generatedModels {
|
|
// Get directory for package
|
|
pkgInfo := p.packageCache[pkg]
|
|
relativePackageDir := p.RelativeBindingsDir(pkgInfo, pkgInfo)
|
|
// Write the directory
|
|
err = os.WriteFile(filepath.Join(options.OutputDirectory, relativePackageDir, options.ModelsFilename), []byte(text), 0644)
|
|
}
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
}
|
|
|
|
p.Stats.EndTime = time.Now()
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func (p *Project) parseDirectory(dir string) (map[string]*ParsedPackage, error) {
|
|
if p.packageCache[dir] != nil {
|
|
return map[string]*ParsedPackage{dir: p.packageCache[dir]}, nil
|
|
}
|
|
// Parse the directory
|
|
fset := token.NewFileSet()
|
|
if dir == "." || dir == "" {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dir = cwd
|
|
}
|
|
pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result = make(map[string]*ParsedPackage)
|
|
p.Stats.NumPackages = len(pkgs)
|
|
for packageName, pkg := range pkgs {
|
|
parsedPackage := &ParsedPackage{
|
|
Pkg: pkg,
|
|
Name: packageName,
|
|
Path: packageName,
|
|
Dir: getDirectoryForPackage(pkg),
|
|
StructCache: make(map[structName]*StructDef),
|
|
TypeCache: make(map[string]*TypeDef),
|
|
}
|
|
p.parseTypes(map[string]*ParsedPackage{packageName: parsedPackage})
|
|
p.packageCache[packageName] = parsedPackage
|
|
result[packageName] = parsedPackage
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (p *Project) findApplicationNewCalls(pkgs map[string]*ParsedPackage) (err error) {
|
|
|
|
var callFound bool
|
|
|
|
p.parseTypes(pkgs)
|
|
|
|
for _, pkg := range pkgs {
|
|
thisPackage := pkg.Pkg
|
|
// Iterate through the package's files
|
|
for _, file := range thisPackage.Files {
|
|
// Use an ast.Inspector to find the calls to application.New
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
// Check for const declaration
|
|
genDecl, ok := n.(*ast.GenDecl)
|
|
if ok {
|
|
switch genDecl.Tok {
|
|
case token.TYPE:
|
|
var comments []string
|
|
if genDecl.Doc != nil {
|
|
comments = CommentGroupToText(genDecl.Doc)
|
|
}
|
|
for _, spec := range genDecl.Specs {
|
|
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
|
|
p.parseTypeDeclaration(typeSpec, pkg, comments)
|
|
}
|
|
}
|
|
case token.CONST:
|
|
p.parseConstDeclaration(genDecl, pkg)
|
|
default:
|
|
}
|
|
return true
|
|
|
|
}
|
|
|
|
// Check if the node is a call expression
|
|
callExpr, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// Check if the function being called is "application.New"
|
|
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if selExpr.Sel.Name != "New" {
|
|
return true
|
|
}
|
|
if id, ok := selExpr.X.(*ast.Ident); !ok || id.Name != "application" {
|
|
return true
|
|
}
|
|
|
|
// Check there is only 1 argument
|
|
if len(callExpr.Args) != 1 {
|
|
return true
|
|
}
|
|
|
|
// Check argument 1 is a struct literal
|
|
structLit, ok := callExpr.Args[0].(*ast.CompositeLit)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
// Check struct literal is of type "application.Options"
|
|
selectorExpr, ok := structLit.Type.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if selectorExpr.Sel.Name != "Options" {
|
|
return true
|
|
}
|
|
if id, ok := selectorExpr.X.(*ast.Ident); !ok || id.Name != "application" {
|
|
return true
|
|
}
|
|
|
|
for _, elt := range structLit.Elts {
|
|
// Find the "Bind" field
|
|
kvExpr, ok := elt.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if id, ok := kvExpr.Key.(*ast.Ident); !ok || id.Name != "Bind" {
|
|
continue
|
|
}
|
|
// Check the value is a slice of interfaces
|
|
sliceExpr, ok := kvExpr.Value.(*ast.CompositeLit)
|
|
if !ok {
|
|
continue
|
|
}
|
|
var arrayType *ast.ArrayType
|
|
if arrayType, ok = sliceExpr.Type.(*ast.ArrayType); !ok {
|
|
continue
|
|
}
|
|
|
|
// Check array type is of type "interface{}"
|
|
_, isInterfaceType := arrayType.Elt.(*ast.InterfaceType)
|
|
if !isInterfaceType {
|
|
// Check it's an "any" type
|
|
ident, isAnyType := arrayType.Elt.(*ast.Ident)
|
|
if !isAnyType {
|
|
continue
|
|
}
|
|
if ident.Name != "any" {
|
|
continue
|
|
}
|
|
}
|
|
callFound = true
|
|
// Iterate through the slice elements
|
|
for _, elt := range sliceExpr.Elts {
|
|
result, shouldContinue := p.parseBoundExpression(elt, pkg)
|
|
if shouldContinue {
|
|
continue
|
|
}
|
|
return result
|
|
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
p.addTypes(pkg.Path, pkg.TypeCache)
|
|
if !callFound {
|
|
return ErrNoBindingsFound
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Project) parseBoundUnaryExpression(unaryExpr *ast.UnaryExpr, pkg *ParsedPackage) (bool, bool) {
|
|
// Check the unary expression is a composite lit
|
|
|
|
switch t := unaryExpr.X.(type) {
|
|
case *ast.CompositeLit:
|
|
return p.parseBoundCompositeLit(t, pkg)
|
|
}
|
|
return false, true
|
|
|
|
}
|
|
|
|
func (p *Project) addBoundMethods(packagePath string, name string, boundMethods []*BoundMethod) {
|
|
_, ok := p.BoundMethods[packagePath]
|
|
if !ok {
|
|
p.BoundMethods[packagePath] = make(map[structName][]*BoundMethod)
|
|
}
|
|
p.BoundMethods[packagePath][name] = boundMethods
|
|
}
|
|
|
|
func (p *Project) parseBoundStructMethods(name string, pkg *ParsedPackage) error {
|
|
var methods []*BoundMethod
|
|
// Iterate over all files in the package
|
|
for _, file := range pkg.Pkg.Files {
|
|
// Iterate over all declarations in the file
|
|
for _, decl := range file.Decls {
|
|
// Check if the declaration is a type declaration
|
|
if funcDecl, ok := decl.(*ast.FuncDecl); ok && funcDecl.Recv != nil && funcDecl.Name.IsExported() {
|
|
var ident *ast.Ident
|
|
var ok bool
|
|
|
|
switch funcDecl.Recv.List[0].Type.(type) {
|
|
case *ast.StarExpr:
|
|
recv := funcDecl.Recv.List[0].Type.(*ast.StarExpr)
|
|
ident, ok = recv.X.(*ast.Ident)
|
|
case *ast.Ident:
|
|
ident, ok = funcDecl.Recv.List[0].Type.(*ast.Ident)
|
|
}
|
|
if ok && ident.Name == name {
|
|
fqn := fmt.Sprintf("%s.%s.%s", pkg.Path, name, funcDecl.Name.Name)
|
|
|
|
var alias *uint32
|
|
var err error
|
|
// Check for the text `wails:methodID <integer>`
|
|
if funcDecl.Doc != nil {
|
|
for _, docstring := range funcDecl.Doc.List {
|
|
if strings.Contains(docstring.Text, "//wails:methodID") {
|
|
idString := strings.TrimSpace(strings.TrimPrefix(docstring.Text, "//wails:methodID"))
|
|
parsedID, err := strconv.ParseUint(idString, 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid value in `wails:methodID` directive: '%s'. Expected a valid uint32 value", idString)
|
|
}
|
|
alias = lo.ToPtr(uint32(parsedID))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
id, err := hash.Fnv(fqn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
method := &BoundMethod{
|
|
Package: pkg.Path,
|
|
PackageDir: pkg.Dir,
|
|
ID: id,
|
|
Name: funcDecl.Name.Name,
|
|
DocComment: strings.TrimSpace(funcDecl.Doc.Text()),
|
|
Alias: alias,
|
|
}
|
|
|
|
if funcDecl.Type.Params != nil {
|
|
method.Inputs = p.parseParameters(funcDecl.Type.Params, pkg)
|
|
}
|
|
if funcDecl.Type.Results != nil {
|
|
method.Outputs = p.parseParameters(funcDecl.Type.Results, pkg)
|
|
}
|
|
|
|
methods = append(methods, method)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
p.addBoundMethods(pkg.Path, name, methods)
|
|
return nil
|
|
}
|
|
|
|
func (p *Project) addTypes(packagePath string, types map[string]*TypeDef) {
|
|
if types == nil || len(types) == 0 {
|
|
return
|
|
}
|
|
if p.Types == nil {
|
|
p.Types = make(map[string]map[string]*TypeDef)
|
|
}
|
|
_, ok := p.Types[packagePath]
|
|
if !ok {
|
|
p.Types[packagePath] = make(map[string]*TypeDef)
|
|
}
|
|
p.Types[packagePath] = types
|
|
}
|
|
|
|
func (p *Project) parseParameters(params *ast.FieldList, pkg *ParsedPackage) []*Parameter {
|
|
var result []*Parameter
|
|
for _, field := range params.List {
|
|
var theseFields []*Parameter
|
|
if len(field.Names) > 0 {
|
|
for _, name := range field.Names {
|
|
theseFields = append(theseFields, &Parameter{
|
|
Name: name.Name,
|
|
})
|
|
}
|
|
} else {
|
|
theseFields = append(theseFields, &Parameter{
|
|
Name: "",
|
|
})
|
|
}
|
|
// loop over fields
|
|
for _, thisField := range theseFields {
|
|
thisField.Type = p.parseParameterType(field, pkg)
|
|
result = append(result, thisField)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (p *Project) parseParameterType(field *ast.Field, pkg *ParsedPackage) *ParameterType {
|
|
result := &ParameterType{
|
|
Package: pkg.Path,
|
|
}
|
|
result.Name = getTypeString(field.Type)
|
|
switch t := field.Type.(type) {
|
|
case *ast.Ident:
|
|
result.IsStruct = isStructType(t)
|
|
if !result.IsStruct {
|
|
// Check if it's a type alias
|
|
typeDef, ok := pkg.TypeCache[t.Name]
|
|
if ok {
|
|
typeDef.ShouldGenerate = true
|
|
result.IsEnum = true
|
|
}
|
|
}
|
|
case *ast.StarExpr:
|
|
result = p.parseParameterType(&ast.Field{Type: t.X}, pkg)
|
|
result.IsPointer = true
|
|
case *ast.StructType:
|
|
result.IsStruct = true
|
|
if result.Name == "" {
|
|
// Anonymous struct
|
|
result.Name = p.anonymousStructID()
|
|
// Create a new struct definition
|
|
result := &StructDef{
|
|
Name: result.Name,
|
|
DocComments: CommentGroupToText(field.Doc),
|
|
}
|
|
pkg.StructCache[result.Name] = result
|
|
// Parse the fields
|
|
result.Fields = p.parseStructFields(&ast.StructType{
|
|
Fields: t.Fields,
|
|
}, pkg)
|
|
_ = result
|
|
}
|
|
case *ast.SelectorExpr:
|
|
extPackage, err := p.getParsedPackageFromName(t.X.(*ast.Ident).Name, pkg)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
result.IsStruct = p.getStructDef(t.Sel.Name, extPackage)
|
|
if !result.IsStruct {
|
|
// Check if it's a type alias
|
|
typeDef, ok := extPackage.TypeCache[t.Sel.Name]
|
|
if ok {
|
|
typeDef.ShouldGenerate = true
|
|
result.IsEnum = true
|
|
}
|
|
}
|
|
result.Package = extPackage.Path
|
|
case *ast.ArrayType:
|
|
result.IsSlice = true
|
|
result.IsStruct = isStructType(t.Elt)
|
|
case *ast.MapType:
|
|
tempfield := &ast.Field{Type: t.Key}
|
|
result.MapKey = p.parseParameterType(tempfield, pkg)
|
|
tempfield.Type = t.Value
|
|
result.MapValue = p.parseParameterType(tempfield, pkg)
|
|
default:
|
|
}
|
|
if result.IsStruct {
|
|
p.getStructDef(result.Name, pkg)
|
|
if result.Package == "" {
|
|
result.Package = pkg.Path
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (p *Project) getStructDef(name string, pkg *ParsedPackage) bool {
|
|
_, ok := pkg.StructCache[name]
|
|
if ok {
|
|
return true
|
|
}
|
|
// Iterate over all files in the package
|
|
for _, file := range pkg.Pkg.Files {
|
|
// Iterate over all declarations in the file
|
|
for _, decl := range file.Decls {
|
|
// Check if the declaration is a type declaration
|
|
if typeDecl, ok := decl.(*ast.GenDecl); ok {
|
|
// Check if the type declaration is a struct type
|
|
if typeDecl.Tok == token.TYPE {
|
|
for _, spec := range typeDecl.Specs {
|
|
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
|
|
if structType, ok := typeSpec.Type.(*ast.StructType); ok {
|
|
if typeSpec.Name.Name == name {
|
|
result := &StructDef{
|
|
Name: name,
|
|
DocComments: CommentGroupToText(typeDecl.Doc),
|
|
}
|
|
pkg.StructCache[name] = result
|
|
result.Fields = p.parseStructFields(structType, pkg)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Project) parseStructFields(structType *ast.StructType, pkg *ParsedPackage) []*Field {
|
|
var result []*Field
|
|
for _, field := range structType.Fields.List {
|
|
var theseFields []*Field
|
|
if len(field.Names) > 0 {
|
|
for _, name := range field.Names {
|
|
theseFields = append(theseFields, &Field{
|
|
Project: p,
|
|
Name: name.Name,
|
|
})
|
|
}
|
|
} else {
|
|
theseFields = append(theseFields, &Field{
|
|
Project: p,
|
|
Name: "",
|
|
})
|
|
}
|
|
// loop over fields
|
|
for _, thisField := range theseFields {
|
|
paramType := p.parseParameterType(field, pkg)
|
|
if paramType.IsStruct {
|
|
_, ok := pkg.StructCache[paramType.Name]
|
|
if !ok {
|
|
p.getStructDef(paramType.Name, pkg)
|
|
}
|
|
}
|
|
if paramType.Package == "" {
|
|
paramType.Package = pkg.Path
|
|
}
|
|
thisField.Type = paramType
|
|
result = append(result, thisField)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (p *Project) getParsedPackageFromName(packageName string, currentPackage *ParsedPackage) (*ParsedPackage, error) {
|
|
for _, file := range currentPackage.Pkg.Files {
|
|
for _, imp := range file.Imports {
|
|
path, err := strconv.Unquote(imp.Path.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, lastPathElement := filepath.Split(path)
|
|
if imp.Name != nil && imp.Name.Name == packageName || lastPathElement == packageName {
|
|
// Get the directory for the package
|
|
dir, err := getPackageDir(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkg, err := p.getPackageFromPath(dir, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := &ParsedPackage{
|
|
Pkg: pkg,
|
|
Name: packageName,
|
|
Path: path,
|
|
Dir: dir,
|
|
StructCache: make(map[string]*StructDef),
|
|
TypeCache: make(map[string]*TypeDef),
|
|
}
|
|
p.packageCache[path] = result
|
|
|
|
// Parse types
|
|
p.parseTypes(map[string]*ParsedPackage{path: result})
|
|
|
|
return result, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("package %s not found in %s", packageName, currentPackage.Name)
|
|
}
|
|
|
|
func getPackageDir(importPath string) (string, error) {
|
|
pkg, err := build.Import(importPath, "", build.FindOnly)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return pkg.Dir, nil
|
|
}
|
|
|
|
func (p *Project) getPackageFromPath(packagedir string, packagepath string) (*ast.Package, error) {
|
|
impPkg, err := parser.ParseDir(token.NewFileSet(), packagedir, nil, parser.AllErrors|parser.ParseComments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for impName, impPkg := range impPkg {
|
|
if impName == "main" {
|
|
continue
|
|
}
|
|
return impPkg, nil
|
|
}
|
|
return nil, fmt.Errorf("package not found in imported package %s", packagepath)
|
|
}
|
|
|
|
func (p *Project) anonymousStructID() string {
|
|
p.anonymousStructIDCounter++
|
|
return fmt.Sprintf("anon%d", p.anonymousStructIDCounter)
|
|
}
|
|
|
|
func (p *Project) parseBoundExpression(elt ast.Expr, pkg *ParsedPackage) (bool, bool) {
|
|
|
|
switch t := elt.(type) {
|
|
case *ast.UnaryExpr:
|
|
return p.parseBoundUnaryExpression(t, pkg)
|
|
case *ast.Ident:
|
|
return p.parseBoundIdent(t, pkg)
|
|
case *ast.CallExpr:
|
|
return p.parseBoundCallExpression(t, pkg)
|
|
default:
|
|
println("unhandled expression type", reflect.TypeOf(t).String())
|
|
}
|
|
|
|
return false, false
|
|
}
|
|
|
|
func (p *Project) parseBoundIdent(ident *ast.Ident, pkg *ParsedPackage) (bool, bool) {
|
|
if ident.Obj == nil {
|
|
return false, true
|
|
}
|
|
switch t := ident.Obj.Decl.(type) {
|
|
//case *ast.StructType:
|
|
// return p.parseBoundStruct(t, pkg)
|
|
case *ast.TypeSpec:
|
|
return p.parseBoundTypeSpec(t, pkg)
|
|
case *ast.AssignStmt:
|
|
return p.parseBoundAssignment(t, pkg)
|
|
default:
|
|
println("unhandled ident type", reflect.TypeOf(t).String())
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func (p *Project) parseBoundAssignment(assign *ast.AssignStmt, pkg *ParsedPackage) (bool, bool) {
|
|
return p.parseBoundExpression(assign.Rhs[0], pkg)
|
|
}
|
|
|
|
func (p *Project) parseBoundCompositeLit(lit *ast.CompositeLit, pkg *ParsedPackage) (bool, bool) {
|
|
|
|
switch t := lit.Type.(type) {
|
|
case *ast.StructType:
|
|
//return p.parseBoundStructType(t, pkg)
|
|
return false, true
|
|
case *ast.Ident:
|
|
err := p.parseBoundStructMethods(t.Name, pkg)
|
|
if err != nil {
|
|
return true, false
|
|
}
|
|
return false, true
|
|
case *ast.SelectorExpr:
|
|
return p.parseBoundSelectorExpression(t, pkg)
|
|
}
|
|
|
|
return false, true
|
|
}
|
|
|
|
func (p *Project) parseBoundSelectorExpression(selector *ast.SelectorExpr, pkg *ParsedPackage) (bool, bool) {
|
|
|
|
switch t := selector.X.(type) {
|
|
case *ast.Ident:
|
|
// Look up the package
|
|
var parsedPackage *ParsedPackage
|
|
parsedPackage, err := p.getParsedPackageFromName(t.Name, pkg)
|
|
if err != nil {
|
|
return true, false
|
|
}
|
|
err = p.parseBoundStructMethods(selector.Sel.Name, parsedPackage)
|
|
if err != nil {
|
|
return true, false
|
|
}
|
|
return false, true
|
|
default:
|
|
println("unhandled selector type", reflect.TypeOf(t).String())
|
|
}
|
|
return false, true
|
|
}
|
|
|
|
func (p *Project) parseBoundCallExpression(callExpr *ast.CallExpr, pkg *ParsedPackage) (bool, bool) {
|
|
|
|
// Check if this call returns a struct pointer
|
|
switch t := callExpr.Fun.(type) {
|
|
case *ast.Ident:
|
|
if t.Obj == nil {
|
|
return false, true
|
|
}
|
|
switch t := t.Obj.Decl.(type) {
|
|
case *ast.FuncDecl:
|
|
return p.parseBoundFuncDecl(t, pkg)
|
|
}
|
|
|
|
case *ast.SelectorExpr:
|
|
// Get package for selector
|
|
var parsedPackage *ParsedPackage
|
|
parsedPackage, err := p.getParsedPackageFromName(t.X.(*ast.Ident).Name, pkg)
|
|
if err != nil {
|
|
return true, false
|
|
}
|
|
// Get function from package
|
|
var extFundDecl *ast.FuncDecl
|
|
extFundDecl, err = p.getFunctionFromName(t.Sel.Name, parsedPackage)
|
|
if err != nil {
|
|
return true, false
|
|
}
|
|
return p.parseBoundFuncDecl(extFundDecl, parsedPackage)
|
|
default:
|
|
println("unhandled call type", reflect.TypeOf(t).String())
|
|
}
|
|
|
|
return false, true
|
|
}
|
|
|
|
func (p *Project) parseBoundFuncDecl(t *ast.FuncDecl, pkg *ParsedPackage) (bool, bool) {
|
|
if t.Type.Results == nil {
|
|
return false, true
|
|
}
|
|
if len(t.Type.Results.List) != 1 {
|
|
return false, true
|
|
}
|
|
switch t := t.Type.Results.List[0].Type.(type) {
|
|
case *ast.StarExpr:
|
|
return p.parseBoundExpression(t.X, pkg)
|
|
default:
|
|
println("Unhandled funcdecl type", reflect.TypeOf(t).String())
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func (p *Project) parseBoundTypeSpec(typeSpec *ast.TypeSpec, pkg *ParsedPackage) (bool, bool) {
|
|
switch t := typeSpec.Type.(type) {
|
|
case *ast.StructType:
|
|
err := p.parseBoundStructMethods(typeSpec.Name.Name, pkg)
|
|
if err != nil {
|
|
return true, false
|
|
}
|
|
default:
|
|
println("unhandled type spec type", reflect.TypeOf(t).String())
|
|
}
|
|
return false, true
|
|
}
|
|
|
|
func (p *Project) getFunctionFromName(name string, parsedPackage *ParsedPackage) (*ast.FuncDecl, error) {
|
|
for _, f := range parsedPackage.Pkg.Files {
|
|
for _, decl := range f.Decls {
|
|
switch t := decl.(type) {
|
|
case *ast.FuncDecl:
|
|
if t.Name.Name == name {
|
|
return t, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("function not found")
|
|
}
|
|
|
|
func (p *Project) parseTypeDeclaration(decl *ast.TypeSpec, pkg *ParsedPackage, comments []string) {
|
|
switch t := decl.Type.(type) {
|
|
case *ast.Ident:
|
|
switch t.Name {
|
|
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64",
|
|
"uintptr", "float32", "float64", "string", "bool":
|
|
// Store this in the type cache
|
|
pkg.TypeCache[decl.Name.Name] = &TypeDef{
|
|
Name: decl.Name.Name,
|
|
Type: t.Name,
|
|
DocComments: TrimSlice(comments),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Project) parseConstDeclaration(decl *ast.GenDecl, pkg *ParsedPackage) {
|
|
// Check if the type of the constant is in the type cache and if it doesn't exist, return
|
|
var latestValues []ast.Expr
|
|
|
|
for _, spec := range decl.Specs {
|
|
valueSpec, ok := spec.(*ast.ValueSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
// Extract the Type
|
|
typeString := getTypeString(valueSpec.Type)
|
|
if typeString == "" {
|
|
continue
|
|
}
|
|
// Check if the type is in the type cache
|
|
typeDecl, ok := pkg.TypeCache[typeString]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Get the latest values
|
|
if len(valueSpec.Values) > 0 {
|
|
latestValues = valueSpec.Values
|
|
}
|
|
|
|
// Iterate over the names
|
|
for index, name := range valueSpec.Names {
|
|
constDecl := &ConstDef{
|
|
Name: name.Name,
|
|
Value: typeString,
|
|
}
|
|
|
|
// Get the value
|
|
if len(latestValues) > 0 {
|
|
switch t := latestValues[index].(type) {
|
|
case *ast.BasicLit:
|
|
constDecl.Value = t.Value
|
|
case *ast.Ident:
|
|
constDecl.Value = t.Name
|
|
}
|
|
}
|
|
|
|
if valueSpec.Doc != nil {
|
|
constDecl.DocComments = CommentGroupToText(valueSpec.Doc)
|
|
}
|
|
typeDecl.Consts = append(typeDecl.Consts, constDecl)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Project) RelativePackageDir(path string) string {
|
|
|
|
// Get the package details
|
|
pkgInfo, ok := p.packageCache[path]
|
|
if !ok {
|
|
panic("package not found: " + path)
|
|
}
|
|
|
|
result := filepath.ToSlash(strings.TrimPrefix(pkgInfo.Dir, p.Path))
|
|
if result == "" {
|
|
return "main"
|
|
}
|
|
// Remove the leading slash
|
|
if result[0] == '/' || result[0] == '\\' {
|
|
result = result[1:]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (p *Project) parseTypes(pkgs map[string]*ParsedPackage) {
|
|
for _, pkg := range pkgs {
|
|
thisPackage := pkg.Pkg
|
|
// Iterate through the package's files
|
|
for _, file := range thisPackage.Files {
|
|
// Use an ast.Inspector to find the calls to application.New
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
// Check for const declaration
|
|
genDecl, ok := n.(*ast.GenDecl)
|
|
if ok {
|
|
switch genDecl.Tok {
|
|
case token.TYPE:
|
|
var comments []string
|
|
if genDecl.Doc != nil {
|
|
comments = CommentGroupToText(genDecl.Doc)
|
|
}
|
|
for _, spec := range genDecl.Specs {
|
|
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
|
|
p.parseTypeDeclaration(typeSpec, pkg, comments)
|
|
}
|
|
}
|
|
case token.CONST:
|
|
p.parseConstDeclaration(genDecl, pkg)
|
|
default:
|
|
}
|
|
return true
|
|
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
p.addTypes(pkg.Path, pkg.TypeCache)
|
|
}
|
|
}
|
|
|
|
func (p *Project) RelativeBindingsDir(dir *ParsedPackage, dir2 *ParsedPackage) string {
|
|
|
|
if dir.Dir == dir2.Dir {
|
|
return "."
|
|
}
|
|
|
|
// Calculate the relative path from the bindings directory to the package directory
|
|
absoluteSourceDir := dir.Dir
|
|
if absoluteSourceDir == p.Path {
|
|
absoluteSourceDir = filepath.Join(p.Path, p.outputDirectory, "main")
|
|
} else {
|
|
absoluteSourceDir = dir.Dir
|
|
}
|
|
targetRelativeDir := strings.TrimPrefix(dir2.Dir, p.Path)
|
|
targetBindingsDir := filepath.Join(p.Path, p.outputDirectory, targetRelativeDir)
|
|
// Calculate the relative path from the source directory to the target directory
|
|
relativePath, err := filepath.Rel(absoluteSourceDir, targetBindingsDir)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return filepath.ToSlash(relativePath)
|
|
}
|
|
|
|
type ImportDef struct {
|
|
Name string
|
|
Path string
|
|
VarName string
|
|
PackageName string
|
|
}
|
|
|
|
func (p *Project) calculateImports(pkg string, m map[structName]*StructDef) []*ImportDef {
|
|
var result []*ImportDef
|
|
for _, structDef := range m {
|
|
for _, field := range structDef.Fields {
|
|
if field.Type.Package != pkg {
|
|
// Find the relative path from the source directory to the target directory
|
|
fieldPkgInfo := p.packageCache[field.Type.Package]
|
|
relativePath := p.RelativeBindingsDir(p.packageCache[pkg], fieldPkgInfo)
|
|
result = append(result, &ImportDef{
|
|
PackageName: fieldPkgInfo.Name,
|
|
Name: field.Name,
|
|
Path: relativePath,
|
|
VarName: fieldPkgInfo.Name + field.Name,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func getTypeString(expr ast.Expr) string {
|
|
switch t := expr.(type) {
|
|
case *ast.Ident:
|
|
return t.Name
|
|
case *ast.StarExpr:
|
|
return getTypeString(t.X)
|
|
case *ast.ArrayType:
|
|
return getTypeString(t.Elt)
|
|
case *ast.MapType:
|
|
return "map"
|
|
case *ast.SelectorExpr:
|
|
return getTypeString(t.Sel)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func isStructType(expr ast.Expr) bool {
|
|
switch e := expr.(type) {
|
|
case *ast.StructType:
|
|
return true
|
|
case *ast.StarExpr:
|
|
return isStructType(e.X)
|
|
case *ast.SelectorExpr:
|
|
return isStructType(e.Sel)
|
|
case *ast.ArrayType:
|
|
return isStructType(e.Elt)
|
|
case *ast.SliceExpr:
|
|
return isStructType(e.X)
|
|
case *ast.Ident:
|
|
if e.Obj != nil && e.Obj.Kind == ast.Typ {
|
|
return isStructType(e.Obj.Decl.(*ast.TypeSpec).Type)
|
|
}
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getDirectoryForPackage(pkg *ast.Package) string {
|
|
for filename := range pkg.Files {
|
|
path := filepath.Dir(filename)
|
|
abs, err := filepath.Abs(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return abs
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func CommentGroupToText(comments *ast.CommentGroup) []string {
|
|
if comments == nil {
|
|
return nil
|
|
}
|
|
var result []string
|
|
for _, comment := range comments.List {
|
|
result = append(result, strings.TrimSpace(comment.Text))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func TrimSlice(comments []string) []string {
|
|
for i, comment := range comments {
|
|
comments[i] = strings.TrimSpace(comment)
|
|
}
|
|
return comments
|
|
}
|