gml/internal/build/generate_parse.go
2019-01-14 10:13:57 +01:00

412 lines
8.9 KiB
Go

/*
* GML - Go QML
*
* The MIT License (MIT)
*
* Copyright (c) 2019 Roland Singer <roland.singer[at]desertbit.com>
* Copyright (c) 2019 Sebastian Borchers <sebastian[at]desertbit.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package build
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"unicode"
"github.com/desertbit/gml/internal/utils"
)
const (
cBasePrefix = "gml_gen_"
cppBasePrefix = "GMLGen"
)
// TODO: make concurrent with multiple goroutines.
func parseDirRecursive(dir string) (gt *genTargets, err error) {
gt = &genTargets{}
// Walk through all directories and fill the slice.
var dirs []string
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip files.
if !info.IsDir() {
return nil
}
dirName := info.Name()
// Skip hidden filea and files starting with a "_".
if strings.HasPrefix(dirName, ".") || strings.HasPrefix(dirName, "_") {
return filepath.SkipDir
}
dirs = append(dirs, path)
return nil
})
if err != nil {
return
}
// Parse all directories.
for _, dir := range dirs {
err = parseDir(gt, dir)
if err != nil {
return
}
}
return
}
func parseDir(gt *genTargets, dir string) (err error) {
gp := &genPackage{
Dir: dir,
}
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dir, nil, 0)
if err != nil {
return
}
// Actually there should be only one package in the map,
// if the go source is valid and correct.
for pkgName, pkg := range pkgs {
// Set the package name.
gp.PackageName = pkgName
for _, f := range pkg.Files {
err = parseFile(gp, fset, f)
if err != nil {
return
}
}
}
// Skip if the package is empty.
if len(gp.Structs) > 0 {
gt.Packages = append(gt.Packages, gp)
}
return
}
func parseFile(gp *genPackage, fset *token.FileSet, f *ast.File) (err error) {
// Search for struct definitions.
for _, decl := range f.Decls {
// Must be a token: type
typeDecl, ok := decl.(*ast.GenDecl)
if !ok || typeDecl.Tok != token.TYPE || len(typeDecl.Specs) == 0 {
continue
}
typeSpec, ok := typeDecl.Specs[0].(*ast.TypeSpec)
if !ok {
continue
}
structDecl, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
structName := typeSpec.Name.Name
gs := &genStruct{
Name: structName,
CBaseName: cBasePrefix + structName,
CPPBaseName: cppBasePrefix + structName,
}
for _, f := range structDecl.Fields.List {
// Variable name must be "_".
if len(f.Names) == 0 || f.Names[0].Name != "_" {
continue
}
st, ok := f.Type.(*ast.StructType)
if !ok {
continue
}
err = parseInlineStruct(gs, fset, st)
if err != nil {
return
}
}
// Skip if the struct is empty.
if len(gs.Signals) > 0 || len(gs.Slots) > 0 { // TODO: add slots & properties
gp.Structs = append(gp.Structs, gs)
}
}
return
}
func parseInlineStruct(gs *genStruct, fset *token.FileSet, st *ast.StructType) (err error) {
for _, f := range st.Fields.List {
// Ensure name is set.
if len(f.Names) == 0 {
continue
}
// Extract the tag value and key.
tagValue := strings.Trim(f.Tag.Value, "`")
pos := strings.Index(tagValue, ":")
if pos < 0 {
continue
}
tagKey := tagValue[:pos]
tagValue = strings.Trim(tagValue[pos+1:], "\"")
// Tag key must match gml token.
if tagKey != "gml" {
continue
}
name := f.Names[0].Name
if len(name) == 0 {
continue
}
switch tagValue {
case "signal":
err = parseSignal(gs, fset, f, name)
if err != nil {
return
}
case "slot":
err = parseSlot(gs, fset, f, name)
if err != nil {
return
}
case "property":
// TODO:
default:
return newParseError(fset, f.Pos(), fmt.Errorf("invalid struct tag value: %v", tagValue))
}
}
return
}
func parseSignal(gs *genStruct, fset *token.FileSet, f *ast.Field, name string) (err error) {
// Must be a function/
ft, ok := f.Type.(*ast.FuncType)
if !ok {
return newParseError(fset, f.Pos(), fmt.Errorf("invalid signal: must be a function"))
}
// Ensure the function does not contain any return value.
if ft.Results != nil && len(ft.Results.List) > 0 {
return newParseError(fset, f.Pos(), fmt.Errorf("invalid signal: must not contain a return value"))
}
signal := &genSignal{
Name: name,
CPPName: utils.FirstCharToLower(name), // Qt signal names must be lower-case.
Params: make([]*genParam, 0, len(ft.Params.List)),
}
// Prepare the emit name.
// Prefix with emit and ensure it is private or public as specified.
if unicode.IsUpper(rune(signal.Name[0])) {
signal.EmitName = "Emit" + signal.Name
} else {
signal.EmitName = "emit" + utils.FirstCharToUpper(signal.Name)
}
for _, p := range ft.Params.List {
typeStr := getTypeString(p.Type)
// Ensure a parameter name is set.
if len(p.Names) == 0 {
return newParseError(fset, f.Pos(), fmt.Errorf("invalid signal function parameter: name not set"))
}
for _, n := range p.Names {
signal.Params = append(signal.Params, &genParam{
Name: n.Name,
Type: typeStr,
CType: goTypeToC(typeStr),
CPPType: goTypeToCPP(typeStr),
})
}
}
gs.Signals = append(gs.Signals, signal)
return
}
func parseSlot(gs *genStruct, fset *token.FileSet, f *ast.Field, name string) (err error) {
// Must be a function/
ft, ok := f.Type.(*ast.FuncType)
if !ok {
return newParseError(fset, f.Pos(), fmt.Errorf("invalid slot: must be a function"))
}
// TODO: Handle return types!
slot := &genSlot{
Name: name,
CPPName: utils.FirstCharToLower(name), // Qt slot names must be lower-case.
Params: make([]*genParam, 0, len(ft.Params.List)),
}
for _, p := range ft.Params.List {
typeStr := getTypeString(p.Type)
// Ensure a parameter name is set.
if len(p.Names) == 0 {
return newParseError(fset, f.Pos(), fmt.Errorf("invalid slot function parameter: name not set"))
}
for _, n := range p.Names {
slot.Params = append(slot.Params, &genParam{
Name: n.Name,
Type: typeStr,
CType: goTypeToC(typeStr),
CPPType: goTypeToCPP(typeStr),
})
}
}
gs.Slots = append(gs.Slots, slot)
return
}
// Returns "interface{}" if unknown.
func getTypeString(t ast.Expr) string {
// Check if basic type.
ident, ok := t.(*ast.Ident)
if ok {
return ident.Name
}
// Not required, because QByteArray is not supported by QML.
// check if slice
/*a, ok := t.(*ast.ArrayType)
if ok && a.Len == nil {
// Check if basic type is within the slice.
ident, ok := a.Elt.(*ast.Ident)
if ok {
return "[]" + ident.Name
}
}*/
return "interface{}"
}
func newParseError(fset *token.FileSet, p token.Pos, err error) error {
pos := fset.Position(p)
return fmt.Errorf("%s: line %v: %v", pos.Filename, pos.Line, err)
}
func goTypeToC(t string) string {
switch t {
case "bool":
return "uint8_t"
case "byte":
return "char"
case "string":
return "char*"
case "rune":
return "int32_t"
case "float32":
return "float"
case "float64":
return "double"
case "int":
return "int"
case "int8":
return "int8_t"
case "uint8":
return "uint8_t"
case "int16":
return "int16_t"
case "uint16":
return "uint16_t"
case "int32":
return "int32_t"
case "uint32":
return "uint32_t"
case "int64":
return "int64_t"
case "uint64":
return "uint64_t"
default:
return "gml_variant"
}
}
func goTypeToCPP(t string) string {
switch t {
case "bool":
return "bool"
case "byte":
return "char"
case "string":
return "QString"
case "rune":
return "QChar"
case "float32":
return "float"
case "float64":
return "double"
// QML only supports int.
case "int":
return "int"
case "int8":
return "int"
case "uint8":
return "int"
case "int16":
return "int"
case "uint16":
return "int"
case "int32":
return "int"
case "uint32":
return "int"
// TODO: support int64 & uint64.
case "int64":
panic("currently int64 is not supported")
case "uint64":
panic("currently uint64 is not supported")
default:
return "QVariant"
}
}